From d285906b7bb5a1ffd54ba22085f4f088167d86cf Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Tue, 15 Jul 2025 00:11:42 -0600 Subject: [PATCH 1/8] =?UTF-8?q?docs(#8):=20Crear=20plan=20detallado=20para?= =?UTF-8?q?=20gesti=C3=B3n=20de=20pruebas=20y=20resultados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Definición de 10 tareas principales - Modelos: lims.test, lims.test.parameter, lims.result - Flujo de validación de dos pasos - Generación de reportes PDF - Sistema de permisos por roles - Integración con órdenes y muestras existentes --- documents/plans/ISSUE8_PLAN.md | 225 +++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 documents/plans/ISSUE8_PLAN.md diff --git a/documents/plans/ISSUE8_PLAN.md b/documents/plans/ISSUE8_PLAN.md new file mode 100644 index 0000000..ea1634e --- /dev/null +++ b/documents/plans/ISSUE8_PLAN.md @@ -0,0 +1,225 @@ +# Plan de Implementación - Issue #8: Gestión de Pruebas y Resultados + +## Objetivo +Implementar el sistema completo de gestión de pruebas de laboratorio, incluyendo definición de parámetros, entrada de resultados, validación y generación de reportes. + +## Análisis de Requisitos + +### Funcionalidad Esperada +1. **Catálogo de Pruebas con Parámetros**: Cada análisis debe tener parámetros configurables con unidades y rangos de referencia +2. **Entrada de Resultados**: Interfaz dinámica para captura de resultados según el tipo de análisis +3. **Validación de Resultados**: Flujo de dos pasos (técnico ingresa, administrador valida) +4. **Generación de Reportes**: PDF con formato profesional y resultados fuera de rango resaltados +5. **Control de Acceso**: Permisos basados en roles para técnicos y administradores + +### Modelos de Datos Requeridos +1. **lims.test**: Representa una prueba de laboratorio en progreso +2. **lims.test.parameter**: Define parámetros para cada tipo de prueba +3. **lims.result**: Almacena los resultados reales de las pruebas + +## Tareas de Implementación + +### 1. Crear modelo lims.test.parameter +**Archivo:** `lims_management/models/test_parameter.py` +- [ ] Definir modelo con campos: + ```python + name = fields.Char(string='Parámetro', required=True) + product_id = fields.Many2one('product.product', string='Análisis', + domain=[('is_analysis', '=', True)]) + unit = fields.Char(string='Unidad de Medida') + value_type = fields.Selection([ + ('numeric', 'Numérico'), + ('text', 'Texto'), + ('selection', 'Selección'), + ('boolean', 'Sí/No') + ], string='Tipo de Valor', required=True) + normal_min = fields.Float(string='Valor Normal Mínimo') + normal_max = fields.Float(string='Valor Normal Máximo') + selection_options = fields.Text(string='Opciones (para tipo selección)') + sequence = fields.Integer(string='Secuencia', default=10) + ``` +- [ ] Agregar validaciones y constraints +- [ ] Crear vista de configuración + +### 2. Crear modelo lims.test +**Archivo:** `lims_management/models/lims_test.py` +- [ ] Definir modelo principal: + ```python + name = fields.Char(string='Código de Prueba', required=True, readonly=True) + sale_order_id = fields.Many2one('sale.order', string='Orden de Laboratorio') + order_line_id = fields.Many2one('sale.order.line', string='Línea de Orden') + product_id = fields.Many2one('product.product', string='Análisis') + patient_id = fields.Many2one('res.partner', string='Paciente') + sample_id = fields.Many2one('stock.lot', string='Muestra') + state = fields.Selection([ + ('pending', 'Pendiente'), + ('in_process', 'En Proceso'), + ('result_entered', 'Resultado Ingresado'), + ('validated', 'Validado'), + ('cancelled', 'Cancelado') + ], string='Estado', default='pending') + technician_id = fields.Many2one('res.users', string='Técnico') + validator_id = fields.Many2one('res.users', string='Validador') + validation_date = fields.Datetime(string='Fecha de Validación') + ``` +- [ ] Implementar generación automática de código de prueba +- [ ] Agregar métodos de transición de estados +- [ ] Implementar creación automática desde orden confirmada + +### 3. Crear modelo lims.result +**Archivo:** `lims_management/models/lims_result.py` +- [ ] Definir modelo de resultados: + ```python + test_id = fields.Many2one('lims.test', string='Prueba', required=True) + parameter_id = fields.Many2one('lims.test.parameter', string='Parámetro') + value_numeric = fields.Float(string='Valor Numérico') + value_text = fields.Text(string='Valor de Texto') + value_selection = fields.Char(string='Valor de Selección') + value_boolean = fields.Boolean(string='Valor Sí/No') + is_normal = fields.Boolean(string='Dentro de Rango Normal', compute='_compute_is_normal') + observations = fields.Text(string='Observaciones') + attachment_ids = fields.Many2many('ir.attachment', string='Archivos Adjuntos') + ``` +- [ ] Implementar cálculo automático de is_normal +- [ ] Agregar validaciones según tipo de valor + +### 4. Implementar generación automática de pruebas +**Archivo:** `lims_management/models/sale_order.py` (extender) +- [ ] Al confirmar orden con análisis, crear registros lims.test +- [ ] Vincular con muestras generadas automáticamente +- [ ] Notificar en el chatter sobre pruebas creadas + +### 5. Crear vistas de entrada de resultados +**Archivo:** `lims_management/views/lims_test_views.xml` +- [ ] Vista kanban por estado para gestión de pruebas +- [ ] Vista formulario con: + - Información del paciente y muestra (readonly) + - Lista editable de resultados por parámetro + - Destacar valores fuera de rango con color rojo + - Botones de acción según estado +- [ ] Vista lista con filtros y agrupaciones + +### 6. Implementar flujo de validación +**Archivo:** `lims_management/models/lims_test.py` (extender) +- [ ] Método action_enter_results() - marca como resultado ingresado +- [ ] Método action_validate() - valida resultados (solo administradores) +- [ ] Método action_request_review() - solicita revisión +- [ ] Agregar validaciones de permisos + +### 7. Crear informe PDF de resultados +**Archivos:** +- `lims_management/report/test_results_report.xml` +- `lims_management/report/test_results_template.xml` +- [ ] Diseñar plantilla QWeb con: + - Encabezado con logo del laboratorio + - Información del paciente y orden + - Tabla de resultados con formato: + | Parámetro | Resultado | Unidad | Rango de Referencia | + - Resaltar valores anormales + - Sección de observaciones + - Firma del validador +- [ ] Configurar paper format A4 + +### 8. Implementar seguridad y permisos +**Archivos:** +- `lims_management/security/ir.model.access.csv` (actualizar) +- `lims_management/security/security.xml` (actualizar) +- [ ] Definir grupos: + - Técnico de Laboratorio: crear/editar resultados + - Administrador de Laboratorio: validar resultados +- [ ] Crear reglas de registro para cada modelo +- [ ] Implementar campo-nivel de seguridad para validación + +### 9. Crear datos de demostración +**Archivo:** `lims_management/demo/test_parameters_demo.xml` +- [ ] Parámetros para hemograma completo +- [ ] Parámetros para química sanguínea +- [ ] Parámetros para uroanálisis +- [ ] Crear algunas pruebas de ejemplo con resultados + +### 10. Implementar notificaciones +**Archivo:** `lims_management/models/lims_test.py` (extender) +- [ ] Email al paciente cuando resultados están validados +- [ ] Notificación al médico referente +- [ ] Plantillas de email profesionales + +## Consideraciones Técnicas + +### Performance +- Usar compute fields con store=True donde sea apropiado +- Índices en campos de búsqueda frecuente (patient_id, state) +- Lazy loading para attachments + +### Usabilidad +- Interfaz intuitiva para entrada rápida de resultados +- Atajos de teclado para navegación entre campos +- Autocompletado donde sea aplicable +- Vista previa del PDF antes de enviar + +### Validación de Datos +- Validar rangos numéricos según parámetros +- Prevenir modificación de resultados validados +- Log de auditoría para cambios en resultados + +### Integración +- API REST para consulta de resultados (futuro) +- Webhook para notificaciones externas +- Exportación de resultados en formato HL7 (opcional) + +## Flujo de Trabajo + +```mermaid +graph TD + A[Orden Confirmada] --> B[Generar Pruebas] + B --> C[Estado: Pendiente] + C --> D[Muestra Recibida] + D --> E[Estado: En Proceso] + E --> F[Técnico Ingresa Resultados] + F --> G[Estado: Resultado Ingresado] + G --> H{Validación por Admin} + H -->|Aprobado| I[Estado: Validado] + H -->|Rechazado| J[Solicitar Corrección] + J --> F + I --> K[Generar PDF] + K --> L[Enviar Notificaciones] +``` + +## Criterios de Aceptación + +1. [ ] Los análisis tienen parámetros configurables con unidades y rangos +2. [ ] La entrada de resultados es dinámica según el tipo de análisis +3. [ ] Los valores fuera de rango se destacan visualmente +4. [ ] El flujo de validación respeta los permisos por rol +5. [ ] El PDF generado tiene formato profesional y es descargable +6. [ ] Las notificaciones se envían automáticamente +7. [ ] Existe trazabilidad completa de cambios +8. [ ] La interfaz es intuitiva y eficiente + +## Estimación de Tiempo + +- Tareas 1-3: 3-4 horas (modelos y estructura) +- Tarea 4: 1 hora (integración con órdenes) +- Tarea 5: 2-3 horas (vistas complejas) +- Tarea 6: 2 horas (flujo de validación) +- Tarea 7: 3-4 horas (reporte PDF) +- Tarea 8: 1-2 horas (seguridad) +- Tareas 9-10: 2 horas (demo y notificaciones) + +**Total estimado: 15-18 horas** + +## Dependencias + +- Issue #31: Configuración inicial del módulo ✓ +- Issue #32: Generación automática de muestras ✓ +- Módulos de Odoo: sale, stock, mail, report + +## Riesgos y Mitigaciones + +1. **Riesgo**: Complejidad en la entrada dinámica de resultados + - **Mitigación**: Comenzar con tipos básicos y expandir gradualmente + +2. **Riesgo**: Performance con muchos parámetros por prueba + - **Mitigación**: Implementar paginación y carga diferida + +3. **Riesgo**: Formato de PDF no cumple expectativas + - **Mitigación**: Revisar ejemplos tempranos con usuario \ No newline at end of file From 7e2dfb64659fb050aa525dbdb780b5928b7b63d6 Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Tue, 15 Jul 2025 00:22:29 -0600 Subject: [PATCH 2/8] =?UTF-8?q?docs(#8):=20Actualizar=20plan=20para=20enfo?= =?UTF-8?q?carse=20en=20requisitos=20espec=C3=ADficos=20del=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reducir alcance a lo especificado en Issue #8 - Eliminar funcionalidades de otros issues (PDF, seguridad avanzada) - Agregar validación opcional configurable - Actualizar estimación a 9 horas - 7 tareas principales enfocadas en modelos e interfaz --- CLAUDE.md | 6 + documents/plans/ISSUE8_PLAN.md | 258 +++++++++++++-------------------- gitea_cli_helper.py | 60 ++++++++ 3 files changed, 164 insertions(+), 160 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 064b638..182134f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -94,6 +94,12 @@ python gitea_cli_helper.py comment-issue --issue-number 123 --body "Comment text # Close issue python gitea_cli_helper.py close-issue --issue-number 123 + +# Get issue details and comments +python gitea_cli_helper.py get-issue --issue-number 8 + +# List all open issues +python gitea_cli_helper.py list-open-issues ``` ## Mandatory Reading diff --git a/documents/plans/ISSUE8_PLAN.md b/documents/plans/ISSUE8_PLAN.md index ea1634e..40edc6c 100644 --- a/documents/plans/ISSUE8_PLAN.md +++ b/documents/plans/ISSUE8_PLAN.md @@ -1,225 +1,163 @@ # Plan de Implementación - Issue #8: Gestión de Pruebas y Resultados ## Objetivo -Implementar el sistema completo de gestión de pruebas de laboratorio, incluyendo definición de parámetros, entrada de resultados, validación y generación de reportes. +Implementar los modelos y la interfaz básica para la gestión de pruebas y resultados de laboratorio, específicamente los modelos `lims.test` y `lims.result` con entrada dinámica de resultados. ## Análisis de Requisitos -### Funcionalidad Esperada -1. **Catálogo de Pruebas con Parámetros**: Cada análisis debe tener parámetros configurables con unidades y rangos de referencia -2. **Entrada de Resultados**: Interfaz dinámica para captura de resultados según el tipo de análisis -3. **Validación de Resultados**: Flujo de dos pasos (técnico ingresa, administrador valida) -4. **Generación de Reportes**: PDF con formato profesional y resultados fuera de rango resaltados -5. **Control de Acceso**: Permisos basados en roles para técnicos y administradores +### Funcionalidad Esperada (según Issue #8) +1. **Modelo lims.test**: Representar la ejecución de un análisis con estados +2. **Modelo lims.result**: Almacenar cada valor de resultado con soporte para múltiples tipos +3. **Interfaz de entrada dinámica**: Vista formulario con lista editable de resultados +4. **Resaltado visual**: Mostrar en rojo los resultados fuera de rango +5. **Validación opcional**: Permitir configurar si se requiere validación por administrador -### Modelos de Datos Requeridos -1. **lims.test**: Representa una prueba de laboratorio en progreso -2. **lims.test.parameter**: Define parámetros para cada tipo de prueba -3. **lims.result**: Almacena los resultados reales de las pruebas +### Modelos de Datos Requeridos (según Issue #8) +1. **lims.test**: Representa la ejecución de un análisis +2. **lims.result**: Almacena cada valor de resultado +3. **lims.test.parameter**: Modelo referenciado (asumimos ya existe o se creará) ## Tareas de Implementación -### 1. Crear modelo lims.test.parameter -**Archivo:** `lims_management/models/test_parameter.py` -- [ ] Definir modelo con campos: - ```python - name = fields.Char(string='Parámetro', required=True) - product_id = fields.Many2one('product.product', string='Análisis', - domain=[('is_analysis', '=', True)]) - unit = fields.Char(string='Unidad de Medida') - value_type = fields.Selection([ - ('numeric', 'Numérico'), - ('text', 'Texto'), - ('selection', 'Selección'), - ('boolean', 'Sí/No') - ], string='Tipo de Valor', required=True) - normal_min = fields.Float(string='Valor Normal Mínimo') - normal_max = fields.Float(string='Valor Normal Máximo') - selection_options = fields.Text(string='Opciones (para tipo selección)') - sequence = fields.Integer(string='Secuencia', default=10) - ``` -- [ ] Agregar validaciones y constraints -- [ ] Crear vista de configuración - -### 2. Crear modelo lims.test +### 1. Crear modelo lims.test **Archivo:** `lims_management/models/lims_test.py` -- [ ] Definir modelo principal: +- [ ] Definir modelo según especificación del issue: ```python - name = fields.Char(string='Código de Prueba', required=True, readonly=True) - sale_order_id = fields.Many2one('sale.order', string='Orden de Laboratorio') - order_line_id = fields.Many2one('sale.order.line', string='Línea de Orden') - product_id = fields.Many2one('product.product', string='Análisis') - patient_id = fields.Many2one('res.partner', string='Paciente') + sale_order_line_id = fields.Many2one('sale.order.line', string='Línea de Orden') + patient_id = fields.Many2one('res.partner', string='Paciente', + related='sale_order_line_id.order_id.partner_id') + product_id = fields.Many2one('product.product', string='Análisis', + related='sale_order_line_id.product_id') sample_id = fields.Many2one('stock.lot', string='Muestra') state = fields.Selection([ - ('pending', 'Pendiente'), + ('draft', 'Borrador'), ('in_process', 'En Proceso'), ('result_entered', 'Resultado Ingresado'), ('validated', 'Validado'), ('cancelled', 'Cancelado') - ], string='Estado', default='pending') - technician_id = fields.Many2one('res.users', string='Técnico') + ], string='Estado', default='draft') validator_id = fields.Many2one('res.users', string='Validador') validation_date = fields.Datetime(string='Fecha de Validación') + require_validation = fields.Boolean(string='Requiere Validación', + compute='_compute_require_validation') ``` -- [ ] Implementar generación automática de código de prueba +- [ ] Implementar _compute_require_validation basado en configuración - [ ] Agregar métodos de transición de estados -- [ ] Implementar creación automática desde orden confirmada -### 3. Crear modelo lims.result +### 2. Crear modelo lims.result **Archivo:** `lims_management/models/lims_result.py` -- [ ] Definir modelo de resultados: +- [ ] Definir modelo según especificación: ```python - test_id = fields.Many2one('lims.test', string='Prueba', required=True) + test_id = fields.Many2one('lims.test', string='Prueba', required=True, ondelete='cascade') parameter_id = fields.Many2one('lims.test.parameter', string='Parámetro') value_numeric = fields.Float(string='Valor Numérico') - value_text = fields.Text(string='Valor de Texto') - value_selection = fields.Char(string='Valor de Selección') - value_boolean = fields.Boolean(string='Valor Sí/No') - is_normal = fields.Boolean(string='Dentro de Rango Normal', compute='_compute_is_normal') - observations = fields.Text(string='Observaciones') - attachment_ids = fields.Many2many('ir.attachment', string='Archivos Adjuntos') + value_text = fields.Char(string='Valor de Texto') + value_selection = fields.Selection([], string='Valor de Selección') + is_out_of_range = fields.Boolean(string='Fuera de Rango', compute='_compute_is_out_of_range') + notes = fields.Text(string='Notas del Técnico') ``` -- [ ] Implementar cálculo automático de is_normal -- [ ] Agregar validaciones según tipo de valor +- [ ] Implementar _compute_is_out_of_range para detectar valores anormales +- [ ] Agregar validación para asegurar que solo un tipo de valor esté lleno -### 4. Implementar generación automática de pruebas -**Archivo:** `lims_management/models/sale_order.py` (extender) -- [ ] Al confirmar orden con análisis, crear registros lims.test -- [ ] Vincular con muestras generadas automáticamente -- [ ] Notificar en el chatter sobre pruebas creadas - -### 5. Crear vistas de entrada de resultados +### 3. Desarrollar interfaz de ingreso de resultados **Archivo:** `lims_management/views/lims_test_views.xml` -- [ ] Vista kanban por estado para gestión de pruebas -- [ ] Vista formulario con: - - Información del paciente y muestra (readonly) - - Lista editable de resultados por parámetro - - Destacar valores fuera de rango con color rojo - - Botones de acción según estado -- [ ] Vista lista con filtros y agrupaciones +- [ ] Crear vista formulario para lims.test con: + - Información de cabecera (paciente, análisis, muestra) + - Lista editable (One2many) de lims.result + - Campos dinámicos según parámetros del análisis +- [ ] Implementar widget o CSS para resaltar en rojo valores fuera de rango +- [ ] Agregar botones de acción según estado -### 6. Implementar flujo de validación -**Archivo:** `lims_management/models/lims_test.py` (extender) -- [ ] Método action_enter_results() - marca como resultado ingresado -- [ ] Método action_validate() - valida resultados (solo administradores) -- [ ] Método action_request_review() - solicita revisión -- [ ] Agregar validaciones de permisos +### 4. Implementar lógica visual para valores fuera de rango +**Archivo:** `lims_management/static/src/` (CSS/JS) +- [ ] Crear CSS para clase .out-of-range con color rojo +- [ ] Implementar widget o computed field que aplique la clase +- [ ] Asegurar que funcione en vista formulario y lista -### 7. Crear informe PDF de resultados -**Archivos:** -- `lims_management/report/test_results_report.xml` -- `lims_management/report/test_results_template.xml` -- [ ] Diseñar plantilla QWeb con: - - Encabezado con logo del laboratorio - - Información del paciente y orden - - Tabla de resultados con formato: - | Parámetro | Resultado | Unidad | Rango de Referencia | - - Resaltar valores anormales - - Sección de observaciones - - Firma del validador -- [ ] Configurar paper format A4 +### 5. Agregar configuración de validación opcional +**Archivo:** `lims_management/models/res_config_settings.py` +- [ ] Agregar campo booleano lims_require_validation +- [ ] Extender res.config.settings para incluir esta configuración +- [ ] Modificar lims.test para usar esta configuración en flujo de trabajo -### 8. Implementar seguridad y permisos -**Archivos:** -- `lims_management/security/ir.model.access.csv` (actualizar) -- `lims_management/security/security.xml` (actualizar) -- [ ] Definir grupos: - - Técnico de Laboratorio: crear/editar resultados - - Administrador de Laboratorio: validar resultados -- [ ] Crear reglas de registro para cada modelo -- [ ] Implementar campo-nivel de seguridad para validación +### 6. Crear vistas básicas +**Archivo:** `lims_management/views/lims_test_views.xml` +- [ ] Vista lista de pruebas con campos básicos +- [ ] Vista kanban agrupada por estado +- [ ] Menú de acceso en Laboratorio > Pruebas -### 9. Crear datos de demostración -**Archivo:** `lims_management/demo/test_parameters_demo.xml` -- [ ] Parámetros para hemograma completo -- [ ] Parámetros para química sanguínea -- [ ] Parámetros para uroanálisis -- [ ] Crear algunas pruebas de ejemplo con resultados +### 7. Crear datos de demostración básicos +**Archivo:** `lims_management/demo/lims_test_demo.xml` +- [ ] Crear algunos registros lims.test de ejemplo +- [ ] Agregar resultados de demostración +- [ ] Incluir casos con valores dentro y fuera de rango -### 10. Implementar notificaciones -**Archivo:** `lims_management/models/lims_test.py` (extender) -- [ ] Email al paciente cuando resultados están validados -- [ ] Notificación al médico referente -- [ ] Plantillas de email profesionales ## Consideraciones Técnicas ### Performance -- Usar compute fields con store=True donde sea apropiado -- Índices en campos de búsqueda frecuente (patient_id, state) -- Lazy loading para attachments +- Usar compute fields con store=True para is_out_of_range +- Carga eficiente de parámetros relacionados ### Usabilidad -- Interfaz intuitiva para entrada rápida de resultados -- Atajos de teclado para navegación entre campos -- Autocompletado donde sea aplicable -- Vista previa del PDF antes de enviar +- Interfaz clara para entrada de resultados +- Feedback visual inmediato para valores fuera de rango +- Navegación intuitiva entre estados ### Validación de Datos -- Validar rangos numéricos según parámetros -- Prevenir modificación de resultados validados -- Log de auditoría para cambios en resultados - -### Integración -- API REST para consulta de resultados (futuro) -- Webhook para notificaciones externas -- Exportación de resultados en formato HL7 (opcional) +- Solo un tipo de valor debe estar lleno por resultado +- Validar que el parámetro corresponda al análisis +- Estados coherentes con el flujo de trabajo ## Flujo de Trabajo ```mermaid graph TD - A[Orden Confirmada] --> B[Generar Pruebas] - B --> C[Estado: Pendiente] - C --> D[Muestra Recibida] - D --> E[Estado: En Proceso] - E --> F[Técnico Ingresa Resultados] - F --> G[Estado: Resultado Ingresado] - G --> H{Validación por Admin} - H -->|Aprobado| I[Estado: Validado] - H -->|Rechazado| J[Solicitar Corrección] - J --> F - I --> K[Generar PDF] - K --> L[Enviar Notificaciones] + A[Línea de Orden] --> B[Crear lims.test] + B --> C[Estado: draft] + C --> D[Estado: in_process] + D --> E[Técnico Ingresa Resultados] + E --> F[Estado: result_entered] + F --> G{¿Requiere Validación?} + G -->|Sí| H[Esperar Validación] + G -->|No| I[Proceso Completo] + H --> J[Estado: validated] ``` -## Criterios de Aceptación +## Criterios de Aceptación (según Issue #8) -1. [ ] Los análisis tienen parámetros configurables con unidades y rangos -2. [ ] La entrada de resultados es dinámica según el tipo de análisis -3. [ ] Los valores fuera de rango se destacan visualmente -4. [ ] El flujo de validación respeta los permisos por rol -5. [ ] El PDF generado tiene formato profesional y es descargable -6. [ ] Las notificaciones se envían automáticamente -7. [ ] Existe trazabilidad completa de cambios -8. [ ] La interfaz es intuitiva y eficiente +1. [ ] Modelo lims.test creado con todos los campos especificados +2. [ ] Modelo lims.result creado con soporte para múltiples tipos de valor +3. [ ] Interfaz de formulario con lista editable de resultados +4. [ ] Valores fuera de rango se muestran en rojo +5. [ ] La validación por administrador es configurable +6. [ ] Los campos relacionados (patient_id, product_id) funcionan correctamente ## Estimación de Tiempo -- Tareas 1-3: 3-4 horas (modelos y estructura) -- Tarea 4: 1 hora (integración con órdenes) -- Tarea 5: 2-3 horas (vistas complejas) -- Tarea 6: 2 horas (flujo de validación) -- Tarea 7: 3-4 horas (reporte PDF) -- Tarea 8: 1-2 horas (seguridad) -- Tareas 9-10: 2 horas (demo y notificaciones) +- Tarea 1: 2 horas (modelo lims.test) +- Tarea 2: 1.5 horas (modelo lims.result) +- Tarea 3: 2 horas (interfaz de entrada) +- Tarea 4: 1 hora (lógica visual) +- Tarea 5: 1 hora (configuración) +- Tareas 6-7: 1.5 horas (vistas y demo) -**Total estimado: 15-18 horas** +**Total estimado: 9 horas** ## Dependencias - Issue #31: Configuración inicial del módulo ✓ - Issue #32: Generación automática de muestras ✓ -- Módulos de Odoo: sale, stock, mail, report +- Modelo lims.test.parameter (debe existir o crearse) +- Módulos de Odoo: sale, stock ## Riesgos y Mitigaciones -1. **Riesgo**: Complejidad en la entrada dinámica de resultados - - **Mitigación**: Comenzar con tipos básicos y expandir gradualmente +1. **Riesgo**: El modelo lims.test.parameter no está definido + - **Mitigación**: Crear modelo básico o usar product.product temporalmente -2. **Riesgo**: Performance con muchos parámetros por prueba - - **Mitigación**: Implementar paginación y carga diferida +2. **Riesgo**: Complejidad en la detección de valores fuera de rango + - **Mitigación**: Implementar lógica simple inicialmente -3. **Riesgo**: Formato de PDF no cumple expectativas - - **Mitigación**: Revisar ejemplos tempranos con usuario \ No newline at end of file +3. **Riesgo**: Integración con flujo existente de órdenes + - **Mitigación**: Crear pruebas manualmente en primera versión \ No newline at end of file diff --git a/gitea_cli_helper.py b/gitea_cli_helper.py index 4049758..97a2412 100644 --- a/gitea_cli_helper.py +++ b/gitea_cli_helper.py @@ -110,6 +110,60 @@ def close_issue(issue_number): _make_gitea_request("PATCH", endpoint, payload) print(f"Issue #{issue_number} cerrado exitosamente.") +def get_issue_details(issue_number): + """Gets details and comments for a specific issue.""" + # Get issue details + endpoint = f"repos/{GITEA_USERNAME}/{GITEA_REPO_NAME}/issues/{issue_number}" + print(f"Obteniendo detalles del issue #{issue_number}...") + + try: + issue = _make_gitea_request("GET", endpoint) + + # Display issue information + print(f"\n{'=' * 80}") + print(f"Issue #{issue.get('number', 'N/A')}: {issue.get('title', 'Sin título')}") + print(f"{'=' * 80}") + print(f"Estado: {'Abierto' if issue.get('state') == 'open' else 'Cerrado'}") + print(f"Autor: {issue.get('user', {}).get('login', 'Desconocido')}") + print(f"Creado: {issue.get('created_at', '').replace('T', ' ').split('+')[0] if issue.get('created_at') else 'N/A'}") + + if issue.get('closed_at'): + print(f"Cerrado: {issue.get('closed_at', '').replace('T', ' ').split('+')[0]}") + + labels = [label.get('name', '') for label in issue.get('labels', [])] + if labels: + print(f"Etiquetas: {', '.join(labels)}") + + print(f"URL: {issue.get('html_url', 'N/A')}") + + print(f"\nDescripción:") + print("-" * 40) + print(issue.get('body', 'Sin descripción')) + + # Get comments + comments_endpoint = f"{endpoint}/comments" + comments = _make_gitea_request("GET", comments_endpoint) + + if comments: + print(f"\nComentarios ({len(comments)}):") + print("-" * 40) + for i, comment in enumerate(comments, 1): + author = comment.get('user', {}).get('login', 'Desconocido') + created = comment.get('created_at', '').replace('T', ' ').split('+')[0] if comment.get('created_at') else 'N/A' + body = comment.get('body', '') + + print(f"\nComentario {i} - {author} ({created}):") + print(body) + if i < len(comments): + print("-" * 40) + else: + print(f"\nNo hay comentarios en este issue.") + + print(f"\n{'=' * 80}\n") + + except Exception as e: + print(f"Error al obtener el issue #{issue_number}: {e}") + def list_open_issues(): """Lists all open issues in the repository.""" endpoint = f"repos/{GITEA_USERNAME}/{GITEA_REPO_NAME}/issues" @@ -223,6 +277,10 @@ def main(): # Subparser para listar issues abiertos list_issues_parser = subparsers.add_parser("list-open-issues", help="Lista todos los issues abiertos del repositorio.") + + # Subparser para obtener detalles de un issue + get_issue_parser = subparsers.add_parser("get-issue", help="Obtiene detalles y comentarios de un issue específico.") + get_issue_parser.add_argument("--issue-number", type=int, required=True, help="Número del issue a consultar.") args = parser.parse_args() @@ -238,6 +296,8 @@ def main(): merge_pull_request(args.pr_number, args.merge_method) elif args.command == "list-open-issues": list_open_issues() + elif args.command == "get-issue": + get_issue_details(args.issue_number) else: parser.print_help() From a1b8f7b1de8e70b8c49a557d849e7b591d07bb4c Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Tue, 15 Jul 2025 00:36:58 -0600 Subject: [PATCH 3/8] feat(#8): Task 1 y 2 completadas - Crear modelos lims.test y lims.result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modelo lims.test con todos los campos especificados - Modelo lims.result con soporte para múltiples tipos de valor - Secuencia automática para códigos de prueba - Flujo de estados: draft -> in_process -> result_entered -> validated - Validación de un solo tipo de valor por resultado - Permisos de seguridad configurados --- CLAUDE.md | 11 + lims_management/__manifest__.py | 1 + lims_management/data/lims_sequence.xml | 15 ++ lims_management/models/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 320 -> 388 bytes .../__pycache__/product.cpython-312.pyc | Bin 2974 -> 2974 bytes .../__pycache__/sale_order.cpython-312.pyc | Bin 7204 -> 7204 bytes .../__pycache__/stock_lot.cpython-312.pyc | Bin 9318 -> 9318 bytes lims_management/models/lims_result.py | 124 +++++++++++ lims_management/models/lims_test.py | 207 ++++++++++++++++++ lims_management/security/ir.model.access.csv | 2 + 11 files changed, 362 insertions(+) create mode 100644 lims_management/data/lims_sequence.xml create mode 100644 lims_management/models/lims_result.py create mode 100644 lims_management/models/lims_test.py diff --git a/CLAUDE.md b/CLAUDE.md index 182134f..48df957 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,17 @@ After successful installation/update, the instance must remain active for user v 4. Only proceed to next task if no errors are found 5. If errors are found, fix them before continuing +### Development Workflow per Task +When implementing issues with multiple tasks, follow this workflow for EACH task: +1. **Stop instance**: `docker-compose down -v` +2. **Implement the task**: Make code changes +3. **Start instance**: `docker-compose up -d` (timeout: 300000ms) +4. **Validate logs**: Check for errors in initialization +5. **Commit & Push**: `git add -A && git commit -m "feat(#X): Task description" && git push` +6. **Comment on issue**: Update issue with task completion +7. **Mark task completed**: Update todo list +8. **Proceed to next task**: Only if no errors found + ### Database Operations #### Direct PostgreSQL Access diff --git a/lims_management/__manifest__.py b/lims_management/__manifest__.py index 27c5248..7ce03dc 100644 --- a/lims_management/__manifest__.py +++ b/lims_management/__manifest__.py @@ -23,6 +23,7 @@ 'data/ir_sequence.xml', 'data/product_category.xml', 'data/sample_types.xml', + 'data/lims_sequence.xml', 'views/partner_views.xml', 'views/analysis_views.xml', 'views/sale_order_views.xml', diff --git a/lims_management/data/lims_sequence.xml b/lims_management/data/lims_sequence.xml new file mode 100644 index 0000000..8179dbc --- /dev/null +++ b/lims_management/data/lims_sequence.xml @@ -0,0 +1,15 @@ + + + + + + + Secuencia de Pruebas de Laboratorio + lims.test + LAB-%(year)s- + 5 + + + + + \ No newline at end of file diff --git a/lims_management/models/__init__.py b/lims_management/models/__init__.py index 5b09e9e..a49eb6c 100644 --- a/lims_management/models/__init__.py +++ b/lims_management/models/__init__.py @@ -4,3 +4,5 @@ from . import product from . import partner from . import sale_order from . import stock_lot +from . import lims_test +from . import lims_result diff --git a/lims_management/models/__pycache__/__init__.cpython-312.pyc b/lims_management/models/__pycache__/__init__.cpython-312.pyc index c0fed3a2a0c9835cf86595f680a5d9766782fda5..f3a9a8e466421464c35c9ed7b6df81eb7abf49f4 100644 GIT binary patch delta 172 zcmX@W)WXbrnwOW00SHvSm1Z27$ScVhF;U%7CWRq|Er%hOEsBki0mx>z>z tL~u?#@2$fJWHAD9u@sQ_z|6?V_>@8BE`ul-N!(=+2cmlnl0`f~X#o8DCt?5q delta 84 zcmZo+KET9#nwOW00SM~%lx9qt$ScX{Fj3u+rIJmPePW9Tv!5p0#Mjc31sFZlcz}u+ efw))%NPJ*sWMq8GAaj>N6pSS9F^Cs|%mDyX?-K6- diff --git a/lims_management/models/__pycache__/product.cpython-312.pyc b/lims_management/models/__pycache__/product.cpython-312.pyc index 59f71a22718db5dffcdefd962fc77b369630336b..a2b65f234d246ac31c8174ae2efa74aff3fdba11 100644 GIT binary patch delta 19 ZcmbOyK2MzMG%qg~0}vQ}*vK`78vrn^1mFMw delta 19 ZcmbOyK2MzMG%qg~0}!~s*vK`78vro_1nmF- diff --git a/lims_management/models/__pycache__/sale_order.cpython-312.pyc b/lims_management/models/__pycache__/sale_order.cpython-312.pyc index 8e543021c868d5b78b781d2cd08821ea392ab152..50afd7af0e0aa273ce2fdea2a8ef176822a247e3 100644 GIT binary patch delta 19 ZcmZ2tvBZMwG%qg~0}vQ}*vKU%0{}Gy1jPUV delta 19 ZcmZ2tvBZMwG%qg~0}vR!*vKU%0{}GK1ib(N diff --git a/lims_management/models/__pycache__/stock_lot.cpython-312.pyc b/lims_management/models/__pycache__/stock_lot.cpython-312.pyc index 051c734003c4223d9241351bcdf4d59d08ee4312..5556171bf610f1224a4b952885692a2ad3006d61 100644 GIT binary patch delta 19 ZcmaFn@yvtkG%qg~0}vQ}*vJ*50suhx1+xGE delta 19 ZcmaFn@yvtkG%qg~0}#l(*vJ*50sufv1(g5* diff --git a/lims_management/models/lims_result.py b/lims_management/models/lims_result.py new file mode 100644 index 0000000..a123c25 --- /dev/null +++ b/lims_management/models/lims_result.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +import logging + +_logger = logging.getLogger(__name__) + + +class LimsResult(models.Model): + _name = 'lims.result' + _description = 'Resultado de Prueba de Laboratorio' + _rec_name = 'display_name' + _order = 'test_id, sequence' + + display_name = fields.Char( + string='Nombre', + compute='_compute_display_name', + store=True + ) + + test_id = fields.Many2one( + 'lims.test', + string='Prueba', + required=True, + ondelete='cascade' + ) + + # Por ahora, estos campos básicos + parameter_name = fields.Char( + string='Parámetro', + required=True + ) + + sequence = fields.Integer( + string='Secuencia', + default=10 + ) + + # TODO: Implementar parameter_id cuando exista lims.test.parameter + # parameter_id = fields.Many2one( + # 'lims.test.parameter', + # string='Parámetro' + # ) + + value_numeric = fields.Float( + string='Valor Numérico' + ) + + value_text = fields.Char( + string='Valor de Texto' + ) + + value_selection = fields.Selection( + [], # Por ahora vacío + string='Valor de Selección' + ) + + is_out_of_range = fields.Boolean( + string='Fuera de Rango', + compute='_compute_is_out_of_range', + store=True + ) + + notes = fields.Text( + string='Notas del Técnico' + ) + + # Campos para rangos normales (temporal) + normal_min = fields.Float( + string='Valor Normal Mínimo' + ) + + normal_max = fields.Float( + string='Valor Normal Máximo' + ) + + unit = fields.Char( + string='Unidad' + ) + + @api.depends('test_id', 'parameter_name') + def _compute_display_name(self): + """Calcula el nombre a mostrar.""" + for record in self: + if record.test_id and record.parameter_name: + record.display_name = f"{record.test_id.name} - {record.parameter_name}" + else: + record.display_name = record.parameter_name or _('Nuevo') + + @api.depends('value_numeric', 'normal_min', 'normal_max') + def _compute_is_out_of_range(self): + """Determina si el valor está fuera del rango normal.""" + for record in self: + if record.value_numeric and (record.normal_min or record.normal_max): + if record.normal_min and record.value_numeric < record.normal_min: + record.is_out_of_range = True + elif record.normal_max and record.value_numeric > record.normal_max: + record.is_out_of_range = True + else: + record.is_out_of_range = False + else: + record.is_out_of_range = False + + @api.constrains('value_numeric', 'value_text', 'value_selection') + def _check_single_value_type(self): + """Asegura que solo un tipo de valor esté lleno.""" + for record in self: + filled_values = 0 + if record.value_numeric: + filled_values += 1 + if record.value_text: + filled_values += 1 + if record.value_selection: + filled_values += 1 + + if filled_values > 1: + raise ValidationError( + _('Solo se puede ingresar un tipo de valor (numérico, texto o selección) por resultado.') + ) + + if filled_values == 0: + raise ValidationError( + _('Debe ingresar al menos un valor para el resultado.') + ) \ No newline at end of file diff --git a/lims_management/models/lims_test.py b/lims_management/models/lims_test.py new file mode 100644 index 0000000..cdd8464 --- /dev/null +++ b/lims_management/models/lims_test.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +import logging + +_logger = logging.getLogger(__name__) + + +class LimsTest(models.Model): + _name = 'lims.test' + _description = 'Prueba de Laboratorio' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _rec_name = 'name' + _order = 'create_date desc' + + name = fields.Char( + string='Código de Prueba', + required=True, + readonly=True, + copy=False, + default='Nuevo' + ) + + sale_order_line_id = fields.Many2one( + 'sale.order.line', + string='Línea de Orden', + required=True, + ondelete='restrict' + ) + + patient_id = fields.Many2one( + 'res.partner', + string='Paciente', + related='sale_order_line_id.order_id.partner_id', + store=True, + readonly=True + ) + + product_id = fields.Many2one( + 'product.product', + string='Análisis', + related='sale_order_line_id.product_id', + store=True, + readonly=True + ) + + sample_id = fields.Many2one( + 'stock.lot', + string='Muestra', + domain="[('is_lab_sample', '=', True)]", + tracking=True + ) + + state = fields.Selection([ + ('draft', 'Borrador'), + ('in_process', 'En Proceso'), + ('result_entered', 'Resultado Ingresado'), + ('validated', 'Validado'), + ('cancelled', 'Cancelado') + ], string='Estado', default='draft', tracking=True) + + validator_id = fields.Many2one( + 'res.users', + string='Validador', + readonly=True, + tracking=True + ) + + validation_date = fields.Datetime( + string='Fecha de Validación', + readonly=True, + tracking=True + ) + + technician_id = fields.Many2one( + 'res.users', + string='Técnico', + default=lambda self: self.env.user, + tracking=True + ) + + require_validation = fields.Boolean( + string='Requiere Validación', + compute='_compute_require_validation', + store=True + ) + + result_ids = fields.One2many( + 'lims.result', + 'test_id', + string='Resultados' + ) + + notes = fields.Text( + string='Observaciones' + ) + + company_id = fields.Many2one( + 'res.company', + string='Compañía', + required=True, + default=lambda self: self.env.company + ) + + @api.depends('company_id') + def _compute_require_validation(self): + """Calcula si la prueba requiere validación basado en configuración.""" + for record in self: + # Por ahora, siempre requiere validación + # TODO: Implementar cuando se agregue res.config.settings + record.require_validation = True + + @api.model_create_multi + def create(self, vals_list): + """Genera código único al crear.""" + for vals in vals_list: + if vals.get('name', 'Nuevo') == 'Nuevo': + vals['name'] = self.env['ir.sequence'].next_by_code('lims.test') or 'Nuevo' + return super().create(vals_list) + + def action_start_process(self): + """Inicia el proceso de análisis.""" + self.ensure_one() + if self.state != 'draft': + raise UserError(_('Solo se pueden procesar pruebas en estado borrador.')) + if not self.sample_id: + raise UserError(_('Debe asignar una muestra antes de iniciar el proceso.')) + + self.write({ + 'state': 'in_process', + 'technician_id': self.env.user.id + }) + + # Log en el chatter + self.message_post( + body=_('Prueba iniciada por %s') % self.env.user.name, + subject=_('Proceso Iniciado') + ) + + return True + + def action_enter_results(self): + """Marca como resultados ingresados.""" + self.ensure_one() + if self.state != 'in_process': + raise UserError(_('Solo se pueden ingresar resultados en pruebas en proceso.')) + + if not self.result_ids: + raise UserError(_('Debe ingresar al menos un resultado.')) + + self.state = 'result_entered' + + # Log en el chatter + self.message_post( + body=_('Resultados ingresados por %s') % self.env.user.name, + subject=_('Resultados Ingresados') + ) + + return True + + def action_validate(self): + """Valida los resultados (solo administradores).""" + self.ensure_one() + if self.state != 'result_entered': + raise UserError(_('Solo se pueden validar pruebas con resultados ingresados.')) + + # TODO: Verificar permisos cuando se implemente seguridad + + self.write({ + 'state': 'validated', + 'validator_id': self.env.user.id, + 'validation_date': fields.Datetime.now() + }) + + # Log en el chatter + self.message_post( + body=_('Resultados validados por %s') % self.env.user.name, + subject=_('Resultados Validados') + ) + + return True + + def action_cancel(self): + """Cancela la prueba.""" + self.ensure_one() + if self.state == 'validated': + raise UserError(_('No se pueden cancelar pruebas validadas.')) + + self.state = 'cancelled' + + # Log en el chatter + self.message_post( + body=_('Prueba cancelada por %s') % self.env.user.name, + subject=_('Prueba Cancelada') + ) + + return True + + def action_draft(self): + """Regresa a borrador.""" + self.ensure_one() + if self.state not in ['cancelled']: + raise UserError(_('Solo se pueden regresar a borrador pruebas canceladas.')) + + self.state = 'draft' + + return True \ No newline at end of file diff --git a/lims_management/security/ir.model.access.csv b/lims_management/security/ir.model.access.csv index 9d946ac..b2d9cb9 100644 --- a/lims_management/security/ir.model.access.csv +++ b/lims_management/security/ir.model.access.csv @@ -2,3 +2,5 @@ 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 access_sale_order_receptionist,sale.order.receptionist,sale.model_sale_order,group_lims_receptionist,1,1,1,0 access_stock_lot_user,stock.lot.user,stock.model_stock_lot,base.group_user,1,1,1,1 +access_lims_test_user,lims.test.user,model_lims_test,base.group_user,1,1,1,1 +access_lims_result_user,lims.result.user,model_lims_result,base.group_user,1,1,1,1 From 8851a8994acb2a4ed10985768cdd515b70889d5e Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Tue, 15 Jul 2025 00:45:46 -0600 Subject: [PATCH 4/8] feat(#8): Task 3 completada - Interfaz de ingreso de resultados MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vista formulario con lista editable de resultados - Campos dinámicos que se ocultan según tipo de valor - Botones de acción según estado de la prueba - Vista lista, kanban y búsqueda implementadas - Menú agregado en sección Laboratorio - decoration-danger para valores fuera de rango preparado --- lims_management/__manifest__.py | 1 + lims_management/views/lims_test_views.xml | 213 ++++++++++++++++++++++ lims_management/views/menus.xml | 7 + 3 files changed, 221 insertions(+) create mode 100644 lims_management/views/lims_test_views.xml diff --git a/lims_management/__manifest__.py b/lims_management/__manifest__.py index 7ce03dc..f14c46a 100644 --- a/lims_management/__manifest__.py +++ b/lims_management/__manifest__.py @@ -28,6 +28,7 @@ 'views/analysis_views.xml', 'views/sale_order_views.xml', 'views/stock_lot_views.xml', + 'views/lims_test_views.xml', 'views/menus.xml', ], 'demo': [ diff --git a/lims_management/views/lims_test_views.xml b/lims_management/views/lims_test_views.xml new file mode 100644 index 0000000..4588e15 --- /dev/null +++ b/lims_management/views/lims_test_views.xml @@ -0,0 +1,213 @@ + + + + + + + lims.test.form + lims.test + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + + lims.test.list + lims.test + + + + + + + + + + + + + + + + + lims.test.kanban + lims.test + + + + + + + + + + +
+
+
+
+ + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ + + + lims.test.search + lims.test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Pruebas de Laboratorio + lims.test + list,kanban,form + + {'search_default_my_tests': 1} + +

+ Crear primera prueba de laboratorio +

+

+ Aquí podrá gestionar las pruebas de laboratorio, + ingresar resultados y validarlos. +

+
+
+ +
+
\ No newline at end of file diff --git a/lims_management/views/menus.xml b/lims_management/views/menus.xml index c7f58cd..b2123d5 100644 --- a/lims_management/views/menus.xml +++ b/lims_management/views/menus.xml @@ -101,6 +101,13 @@ parent="lims_menu_root" action="action_lims_lab_sample" sequence="16"/> + + + Date: Tue, 15 Jul 2025 00:51:12 -0600 Subject: [PATCH 5/8] feat(#8): Task 4 completada - Resaltado visual para valores fuera de rango MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CSS personalizado para resaltar valores en rojo - Integración con decoration-danger de Odoo 18 - Estilos aplicados a vistas lista y formulario - Assets backend configurados en manifest --- lims_management/__manifest__.py | 5 +++++ lims_management/static/src/css/lims_test.css | 21 ++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 lims_management/static/src/css/lims_test.css diff --git a/lims_management/__manifest__.py b/lims_management/__manifest__.py index f14c46a..91f86ee 100644 --- a/lims_management/__manifest__.py +++ b/lims_management/__manifest__.py @@ -17,6 +17,11 @@ 'category': 'Industries', 'version': '18.0.1.0.0', 'depends': ['base', 'product', 'sale'], + 'assets': { + 'web.assets_backend': [ + 'lims_management/static/src/css/lims_test.css', + ], + }, 'data': [ 'security/lims_security.xml', 'security/ir.model.access.csv', diff --git a/lims_management/static/src/css/lims_test.css b/lims_management/static/src/css/lims_test.css new file mode 100644 index 0000000..f0ef335 --- /dev/null +++ b/lims_management/static/src/css/lims_test.css @@ -0,0 +1,21 @@ +/* Estilos para pruebas de laboratorio LIMS */ + +/* Resaltar valores fuera de rango con decoration-danger */ +.o_list_view .o_data_row td[name="value_numeric"].text-danger, +.o_list_view .o_data_row td[name="value_numeric"] .text-danger { + color: #dc3545 !important; + font-weight: bold; +} + +/* Asegurar que funcione con el decoration-danger de Odoo 18 */ +.o_list_renderer tbody tr td.o_list_number.text-danger, +.o_list_renderer tbody tr td .o_field_number.text-danger { + color: #dc3545 !important; + font-weight: bold; +} + +/* Para campos en vista formulario también */ +.o_form_sheet .o_field_widget[name="value_numeric"].text-danger input { + color: #dc3545 !important; + font-weight: bold; +} \ No newline at end of file From 619c6a3afa6ae00bff3886c4ff2589567bf64b74 Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Tue, 15 Jul 2025 00:59:52 -0600 Subject: [PATCH 6/8] =?UTF-8?q?feat(#8):=20Task=205=20completada=20-=20Con?= =?UTF-8?q?figuraci=C3=B3n=20de=20validaci=C3=B3n=20opcional?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modelo res.config.settings con parámetro lims_require_validation - Método _compute_require_validation usa la configuración - Auto-validación cuando no se requiere validación manual - Vista de configuración en Laboratorio > Configuración > Ajustes - Instancia validada sin errores --- lims_management/__manifest__.py | 3 +- lims_management/models/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 388 -> 431 bytes lims_management/models/lims_test.py | 30 +++++++++----- lims_management/models/res_config_settings.py | 20 ++++++++++ lims_management/views/menus.xml | 7 ++++ .../views/res_config_settings_views.xml | 37 ++++++++++++++++++ 7 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 lims_management/models/res_config_settings.py create mode 100644 lims_management/views/res_config_settings_views.xml diff --git a/lims_management/__manifest__.py b/lims_management/__manifest__.py index 91f86ee..3aecd8d 100644 --- a/lims_management/__manifest__.py +++ b/lims_management/__manifest__.py @@ -16,7 +16,7 @@ 'website': "https://gitea.grupoconsiti.com/luis_portillo/clinical_laboratory", 'category': 'Industries', 'version': '18.0.1.0.0', - 'depends': ['base', 'product', 'sale'], + 'depends': ['base', 'product', 'sale', 'base_setup'], 'assets': { 'web.assets_backend': [ 'lims_management/static/src/css/lims_test.css', @@ -34,6 +34,7 @@ 'views/sale_order_views.xml', 'views/stock_lot_views.xml', 'views/lims_test_views.xml', + 'views/res_config_settings_views.xml', 'views/menus.xml', ], 'demo': [ diff --git a/lims_management/models/__init__.py b/lims_management/models/__init__.py index a49eb6c..78eb9f2 100644 --- a/lims_management/models/__init__.py +++ b/lims_management/models/__init__.py @@ -6,3 +6,4 @@ from . import sale_order from . import stock_lot from . import lims_test from . import lims_result +from . import res_config_settings diff --git a/lims_management/models/__pycache__/__init__.cpython-312.pyc b/lims_management/models/__pycache__/__init__.cpython-312.pyc index f3a9a8e466421464c35c9ed7b6df81eb7abf49f4..8a440fc10171c501afda3c2007c5402dd34b4fbf 100644 GIT binary patch delta 131 zcmZo+UeC;XnwOW00SK1-D$RH{kyny2WukhdKng<&M-D?SM-&GmLnWss*Tj|-DNV*( z!bPdY@yYplX_@Kq#i=DFnR)5Oewv&UU#l_|aZToB^w#GG8pR02#j-%+12ZEd<5LEi Xy9}aWBypEP9E>FIF(?-C0#yJ2R9Ygu delta 70 zcmZ3_+``OznwOW00SHvSm1Z27$ScVhF;P8|rIJIFbK-;)W + + + diff --git a/lims_management/views/res_config_settings_views.xml b/lims_management/views/res_config_settings_views.xml new file mode 100644 index 0000000..1e7e15a --- /dev/null +++ b/lims_management/views/res_config_settings_views.xml @@ -0,0 +1,37 @@ + + + + + + + res.config.settings.view.form.inherit.lims + res.config.settings + + + + + + + + + + + + + + + + + + + + Configuración + ir.actions.act_window + res.config.settings + form + inline + {'module' : 'lims_management'} + + + + \ No newline at end of file From 80c70b67d974a0a751f39e02990d856d2e3b23a5 Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Tue, 15 Jul 2025 01:07:05 -0600 Subject: [PATCH 7/8] =?UTF-8?q?feat(#8):=20Task=207=20completada=20-=20Dat?= =?UTF-8?q?os=20de=20demostraci=C3=B3n=20b=C3=A1sicos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Script Python para crear datos de demostración - Crea pruebas con diferentes estados: en proceso, ingresado, validado - Incluye resultados con valores dentro y fuera de rango - Mezcla tipos de valor: numérico y texto - Integrado en el proceso de inicialización - Instancia validada sin errores --- docker-compose.yml | 1 + init_odoo.py | 34 +++++ test/create_test_demo_data.py | 225 ++++++++++++++++++++++++++++++++++ 3 files changed, 260 insertions(+) create mode 100644 test/create_test_demo_data.py diff --git a/docker-compose.yml b/docker-compose.yml index 51252a8..87c8b34 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,7 @@ services: - ./odoo.conf:/etc/odoo/odoo.conf - ./init_odoo.py:/app/init_odoo.py - ./create_lab_requests.py:/app/create_lab_requests.py + - ./test:/app/test command: ["/usr/bin/python3", "/app/init_odoo.py"] environment: HOST: db diff --git a/init_odoo.py b/init_odoo.py index 7ab37ef..923571a 100644 --- a/init_odoo.py +++ b/init_odoo.py @@ -94,6 +94,40 @@ EOF sys.exit(result.returncode) print("Solicitudes de laboratorio de demostración creadas exitosamente.") + + # --- Crear datos de demostración de pruebas --- + print("\nCreando datos de demostración de pruebas de laboratorio...") + sys.stdout.flush() + + if os.path.exists("/app/test/create_test_demo_data.py"): + with open("/app/test/create_test_demo_data.py", "r") as f: + test_script_content = f.read() + + create_tests_command = f""" + odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF' +{test_script_content} +EOF + """ + + result = subprocess.run( + create_tests_command, + shell=True, + capture_output=True, + text=True, + check=False + ) + + print("--- Create Test Demo Data stdout ---") + print(result.stdout) + print("--- Create Test Demo Data stderr ---") + print(result.stderr) + sys.stdout.flush() + + if result.returncode == 0: + print("Datos de demostración de pruebas creados exitosamente.") + else: + print(f"Advertencia: Fallo al crear datos de demostración de pruebas (código {result.returncode})") + sys.exit(0) except Exception as e: diff --git a/test/create_test_demo_data.py b/test/create_test_demo_data.py new file mode 100644 index 0000000..4952c24 --- /dev/null +++ b/test/create_test_demo_data.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +import odoo +from datetime import datetime, timedelta + +def create_test_demo_data(cr): + """Crea datos de demostración para lims.test y lims.result""" + env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {}) + + # Buscar algunos pacientes y análisis existentes + patients = env['res.partner'].search([('is_patient', '=', True)], limit=3) + if not patients: + print("No se encontraron pacientes para crear pruebas de demostración") + return + + # Buscar análisis disponibles + hemograma = env.ref('lims_management.analysis_hemograma', raise_if_not_found=False) + glucosa = env.ref('lims_management.analysis_glucosa', raise_if_not_found=False) + + if not hemograma or not glucosa: + print("No se encontraron análisis de demostración") + return + + # Buscar o crear una orden de laboratorio simple + lab_order = env['sale.order'].search([ + ('is_lab_request', '=', True), + ('state', '=', 'sale') + ], limit=1) + + if not lab_order: + # Crear una orden básica si no existe + lab_order = env['sale.order'].create({ + 'partner_id': patients[0].id, + 'is_lab_request': True, + 'order_line': [(0, 0, { + 'product_id': hemograma.product_variant_id.id, + 'product_uom_qty': 1 + }), (0, 0, { + 'product_id': glucosa.product_variant_id.id, + 'product_uom_qty': 1 + })] + }) + lab_order.action_confirm() + + # Obtener las líneas de orden + order_lines = lab_order.order_line + if not order_lines: + print("No se encontraron líneas de orden") + return + + # Buscar muestras existentes + samples = env['stock.lot'].search([ + ('is_lab_sample', '=', True), + ('patient_id', '=', lab_order.partner_id.id) + ], limit=2) + + if not samples: + print("No se encontraron muestras de laboratorio") + return + + # Crear prueba 1: Hemograma en proceso + test1 = env['lims.test'].create({ + 'sale_order_line_id': order_lines[0].id, + 'sample_id': samples[0].id, + 'state': 'draft' + }) + + # Iniciar proceso + test1.action_start_process() + + # Crear resultados para hemograma + results_data = [ + { + 'test_id': test1.id, + 'parameter_name': 'Glóbulos Rojos', + 'sequence': 10, + 'value_numeric': 4.5, + 'unit': '10^6/µL', + 'normal_min': 4.2, + 'normal_max': 5.4 + }, + { + 'test_id': test1.id, + 'parameter_name': 'Glóbulos Blancos', + 'sequence': 20, + 'value_numeric': 12.5, # Fuera de rango + 'unit': '10^3/µL', + 'normal_min': 4.5, + 'normal_max': 11.0, + 'notes': 'Valor elevado - posible infección' + }, + { + 'test_id': test1.id, + 'parameter_name': 'Hemoglobina', + 'sequence': 30, + 'value_numeric': 14.2, + 'unit': 'g/dL', + 'normal_min': 12.0, + 'normal_max': 16.0 + }, + { + 'test_id': test1.id, + 'parameter_name': 'Plaquetas', + 'sequence': 40, + 'value_numeric': 250, + 'unit': '10^3/µL', + 'normal_min': 150, + 'normal_max': 400 + } + ] + + for result_data in results_data: + env['lims.result'].create(result_data) + + print(f"Creada prueba {test1.name} con 4 resultados") + + # Crear prueba 2: Glucosa con resultado ingresado + if len(order_lines) > 1: + test2 = env['lims.test'].create({ + 'sale_order_line_id': order_lines[1].id, + 'sample_id': samples[0].id, + 'state': 'draft' + }) + + test2.action_start_process() + + # Crear resultado de glucosa + env['lims.result'].create({ + 'test_id': test2.id, + 'parameter_name': 'Glucosa en Ayunas', + 'sequence': 10, + 'value_numeric': 125, # Fuera de rango + 'unit': 'mg/dL', + 'normal_min': 70, + 'normal_max': 110, + 'notes': 'Valor elevado - prediabetes' + }) + + # Marcar resultados como ingresados + test2.action_enter_results() + + print(f"Creada prueba {test2.name} con resultado ingresado") + + # Crear prueba 3: Uroanálisis con valores mixtos (si hay más pacientes) + if len(patients) > 1 and len(samples) > 1: + # Crear una orden adicional + urine_analysis = env['product.template'].search([ + ('is_analysis', '=', True), + ('name', 'ilike', 'orina') + ], limit=1) + + if urine_analysis: + lab_order2 = env['sale.order'].create({ + 'partner_id': patients[1].id, + 'is_lab_request': True, + 'order_line': [(0, 0, { + 'product_id': urine_analysis.product_variant_id.id, + 'product_uom_qty': 1 + })] + }) + lab_order2.action_confirm() + + test3 = env['lims.test'].create({ + 'sale_order_line_id': lab_order2.order_line[0].id, + 'sample_id': samples[1].id, + 'state': 'draft' + }) + + test3.action_start_process() + + # Crear resultados mixtos + urine_results = [ + { + 'test_id': test3.id, + 'parameter_name': 'Color', + 'sequence': 10, + 'value_text': 'Amarillo claro' + }, + { + 'test_id': test3.id, + 'parameter_name': 'pH', + 'sequence': 20, + 'value_numeric': 6.5, + 'normal_min': 4.6, + 'normal_max': 8.0 + }, + { + 'test_id': test3.id, + 'parameter_name': 'Densidad', + 'sequence': 30, + 'value_numeric': 1.020, + 'normal_min': 1.005, + 'normal_max': 1.030 + }, + { + 'test_id': test3.id, + 'parameter_name': 'Proteínas', + 'sequence': 40, + 'value_text': 'Negativo' + }, + { + 'test_id': test3.id, + 'parameter_name': 'Glucosa', + 'sequence': 50, + 'value_text': 'Negativo' + } + ] + + for result_data in urine_results: + env['lims.result'].create(result_data) + + # Ingresar y validar resultados + test3.action_enter_results() + if test3.state == 'result_entered': + test3.action_validate() + + print(f"Creada prueba {test3.name} validada con resultados mixtos") + + print("\nDatos de demostración de pruebas creados exitosamente") + +if __name__ == '__main__': + db_name = 'lims_demo' + registry = odoo.registry(db_name) + with registry.cursor() as cr: + create_test_demo_data(cr) + cr.commit() \ No newline at end of file From 820c05ffa9114284298601de55c6e48f5a511499 Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Tue, 15 Jul 2025 01:17:37 -0600 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20Implementar=20generaci=C3=B3n=20auto?= =?UTF-8?q?m=C3=A1tica=20de=20pruebas=20y=20mejorar=20selecci=C3=B3n=20de?= =?UTF-8?q?=20muestras?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Agregar método _generate_lab_tests() en sale.order para crear pruebas automáticamente al confirmar orden - Agregar método _find_sample_for_analysis() para asociar muestras con análisis según tipo - Mejorar dominio de sample_id en lims.test para filtrar por paciente y estado (collected/in_analysis) - Agregar método _onchange_sale_order_line() para actualizar dominio de muestra dinámicamente - Las pruebas ahora se crean automáticamente con la muestra correcta asignada Esto resuelve el problema reportado donde las órdenes aprobadas no generaban pruebas y las muestras no estaban disponibles para selección manual. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../__pycache__/sale_order.cpython-312.pyc | Bin 7204 -> 10422 bytes lims_management/models/lims_test.py | 32 ++++++- lims_management/models/sale_order.py | 81 ++++++++++++++++-- 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/lims_management/models/__pycache__/sale_order.cpython-312.pyc b/lims_management/models/__pycache__/sale_order.cpython-312.pyc index 50afd7af0e0aa273ce2fdea2a8ef176822a247e3..b4b681664a49a97e4f3ca138d394e42d08680818 100644 GIT binary patch delta 3444 zcmai0TTB~g7M_3X@dZ41usyzT%W#i#fh8n_+(?rEfi^&sKpO(YG1zHMjE!bYQ!Epw z(yAg_q(bK*(O$I8s#OzZrKUpKCh9|z?XHq)ch#9f*OnEv>Avkt<&f-+eb_x`j3Hg^ zZhyh&|8mZmbI$zdJM(VquZ|b}j%5od7+?Hr>eWA0FBE-Ee!uYT$IgKJhtz3nzyrw5 zQm>QK#V09m$>Ji}PZLqj>Uv4Fd{AFUCuq5ncgT%q(`G`=)#|APIZ0JhF$>znQA3Gm zXLPZm+=`;ci{*PJp3w3F=P>+)3g}|I{IR~;>&gpk{Kh=_c|d=YgvU;+5wax2qA?l5j363Q;i-6oplKuP`+s z-(kuOjg#V3a8zDmT8+~}Zf-K{BCOnIXtUdWuLL7O(H{%?!v0a8W;Sm4IVqD%j8){f z@&RL=*J8HKc4X;7)|Te@6t`97wk~tqwYgX2ykBx#vqq4~mdTflp3d$EwV!YKY|Dc( zW#HxIQ$xz|$kM4Z%F8~b;_Nc#&l;#=XNs#*xvGbrk}O5aPLL-QDLF|V4xCa3PcID& zJ=T$uVfmr4wA2mxxFH;3>3gjCa0_$KdB}OViTQOCbGT)ZGo8@2u9}XBLGV2w#O;FQ zpO_2>zmo+0{;6==s(ybs)V8V1?EEb{EU-jFjU-z?r&*oBABBXrA; zi`zN0C+6; zoXb_tqvhT|>j2}rhK7a=0pxddDZJ@;@ zi~%mMQw$SK0ff_I`xz5wLBKfI0BXj7J-7Pa_#AoH62^_NOLLy=ehyIPKM-%gNKE{6QJ9ztO39e$7oep5LNE+=KD`zWp_}!! zQoXoAV|2zQ`NKeB*a3X#J(PdOoa059#@ReuqLV_r=s( z*Dmfv4UIH4O2QkoMpvU^^nw%=U%;JX2t5E9kMCS467a2s3I6)>na;89I(>@))Ioj} zg(Ttz(IF>#O&Z5#%>9s^o@gK#&agRx3}}5(68ljJDKDeP+-Ss?G2;}n4gJ>yN63LV za%8>Q|`^Gd-IZe%PgBa?OW)3LRCQ1Zcp1? zR}L;5OxYX0us5VRH=H|5Qhc+@C!24zf7JJ3-xA+G+xd;%^N@30u`XEC&b9s2&MtLl zm(tw*phW3EmFgc+`$v?MXB3C;sg5qTVH<}}fl8IDgf^~}o?7w@D#K?}!{^oE^QqxU z75+Sv>B`0|Llsv&rF5WTqKZ9Pg9W}KTtX`8s#jh0=`9T()ZeI2SJuDpx#oGy=)ATk zl+JCR*Q0dBI}VkrooCTE%YxdIj}H^d;z_$JX1mkw+7*hhy<}dW<~&zp@3g<)eXaYQeb>is)g(V^{ID^#qf_0{ zsWctEJEfHOs9ev2Ijcj3M;897OSK5I-H$9>%2KXc${+Gh#dUCrf9~!cl|Pm)-wKhj z6%ZL)^wdPLj<=3oK6bVB-6Pl8rJ5F{dbhH>>w)8Q&u5+og^H*DOUpo(2fzOQw3u>~ zuTV^qwP9H70;>qEw;i8sy0a;@r$^n>qqO!u7+pTzuME7jbo^!I#ZyYf;4*hQ%{iV) z_j600LwEb{`|kNt9fNAe;PTKJ#dmgT$gi}Is-Dq&-v5SqUqSe1mSI@is#F4Z?Kk>E zbw_u#(Dz%+&r{6(gNItW_c5RCV_-41&gCG7EgJuy`~u(#_Bu*diySr61Uk&Kf^duj z+7;9}bZvTEFCTZ<<*oud4gI{&!Q(V}ely&2GS`%#$7n*Gr1aD@@zU|;PN-fX_G(b@ zPfm)_NihT;bQ|o6ytzT0AP|@ui*g+PfMxB~<-Sq|a0Ba9LjTu_F-eKz zu!>hdQ;q+NIKD>R;{cZ^xapiLWo`Jv+K{$m0l7+3&PLVQxI)q9irLPzl}}l>sn%^N zYolsyytO;kd_rwLk!l`Ln+KMx18G3@VMeMO+3&LImq!G>g4D0iah5eE=77T}dJ}(isz>$PsSI^}b$Bd!~NJi4z za=Ii|ZD;H&TdJ6%l`c}xI9E;(0p{&w7xNPNYlIXs;FLUC+P8fYb&+{9jK+VOog0+q jvSxkNw0{C#KseYYUX;HoZQ@6Dgr7HHb^e9IxhDE=$Ul{7 delta 1036 zcmYk4OK1~O6o%){D@~fDV`381nmSEF(jbW%AC#I#`|?q>(i9){6^CZ5Dbpn0nTUpp z8wEudmJ7NPT)0pK5dxw}vAR)kQ&Fi5g18bl8lMYM@Sa4$yZGkZbN+Mgx#!N^k?UQ~ zXAXyrp(|M{AHBC^#`zY_&3t@dJI95CqonMR{b zh%xG^kRE36;ZvMp2OHL8OXK@LbL=!bDUlJchvN)4dC!NChbT+k9^XGzj=zN(HjS?q z;%J&Ef*Y+RWg$jx2!VmnAjU;3PpAbID@MLBA?wOiNmF(C2z3{6PQ`MC?Uu{?X_$T)rkd{A} zmcNqXHG%qTt>lx%-`_ts_+;qO&|K;%ewo=dzkAP%%-;EtvHA9WFQxr8ky+$jkb=*p z;2VEyjX_Jdpsx&S-HP698-9B1*}BghYR!=0rZ&Hq_IOk3VP~y<5#f>8>+S0h9(M?R zk?NVI42LRYp}E~(;ZtQTO{wM66;6)J6(O|(Y~r-94nqkHtNyzmLIJnyZG9U=B0P6D)G28=p1E3p|KdxvxUJv^y zg$^pXg!SJ&lEm+iqO;Xre-5!RiQJBCAA?p*>kg%GGC4(o*Fbt3AOJ`Jk`$&Rn;q8_ zT_0C9Et`!Y+)2GU{Pi0fQ?h~-&;)_AU`V+uQpIaRBt-ehnO_+jinn^IHsO*A!)HB#za{test.name} - {test.product_id.name}" + test_list += "" + + self.message_post( + body=_("Pruebas generadas automáticamente: %s") % test_list, + message_type='notification' + ) + _logger.info(f"Created {len(created_tests)} tests for order {self.name}") + + def _find_sample_for_analysis(self, product): + """Find the appropriate sample for an analysis product""" + # Check if the analysis has a required sample type + if not product.required_sample_type_id: + return False + + # Find a generated sample with matching sample type + for sample in self.generated_sample_ids: + if sample.sample_type_product_id.id == product.required_sample_type_id.id: + return sample + + return False