Merge pull request 'feat(#10): Implementar etiquetas de muestras con código de barras' (#57) from feature/10-sample-barcode-labels into dev

This commit is contained in:
luis_portillo 2025-07-16 04:05:26 +00:00
commit 36a9772a07
14 changed files with 301 additions and 1 deletions

View File

@ -23,7 +23,8 @@
"Bash(find:*)",
"Bash(true)",
"Bash(bash:*)",
"Bash(grep:*)"
"Bash(grep:*)",
"Bash(gh pr merge:*)"
],
"deny": []
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -43,6 +43,7 @@
'views/product_template_parameter_config_views.xml',
'views/parameter_dashboard_views.xml',
'views/menus.xml',
'report/sample_label_report.xml',
],
'demo': [
'demo/demo_users.xml',

View File

@ -293,3 +293,19 @@ class SaleOrder(models.Model):
_logger.info(f"Cancelled {len(samples_to_cancel)} samples and {len(tests_to_cancel)} tests for order {self.name}")
return res
def action_print_sample_labels(self):
"""Imprimir etiquetas de todas las muestras generadas para esta orden"""
self.ensure_one()
if not self.generated_sample_ids:
raise UserError(_('No hay muestras generadas para esta orden. Por favor, confirme la orden primero.'))
# Asegurar que todas las muestras tengan código de barras
self.generated_sample_ids._ensure_barcode()
# Obtener el reporte
report = self.env.ref('lims_management.action_report_sample_label')
# Retornar la acción de imprimir el reporte para todas las muestras
return report.report_action(self.generated_sample_ids)

View File

@ -272,3 +272,10 @@ class StockLot(models.Model):
even_sum = sum([sum(divmod(2 * d, 10)) for d in digits[-2::-2]])
total = odd_sum + even_sum
return (10 - (total % 10)) % 10
def _ensure_barcode(self):
"""Ensure all lab samples have a barcode"""
for record in self:
if record.is_lab_sample and not record.barcode:
record.barcode = record._generate_unique_barcode()
return True

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Formato de papel para etiquetas - DEBE IR PRIMERO -->
<record id="paperformat_sample_label" model="report.paperformat">
<field name="name">Formato Etiqueta Muestra</field>
<field name="default" eval="False"/>
<field name="format">custom</field>
<field name="page_height">50</field>
<field name="page_width">100</field>
<field name="orientation">Landscape</field>
<field name="margin_top">2</field>
<field name="margin_bottom">2</field>
<field name="margin_left">2</field>
<field name="margin_right">2</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">0</field>
<field name="dpi">200</field>
</record>
<!-- Definir el reporte - DESPUÉS del paperformat -->
<record id="action_report_sample_label" model="ir.actions.report">
<field name="name">Etiquetas de Muestras</field>
<field name="model">stock.lot</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">lims_management.report_sample_label</field>
<field name="report_file">lims_management.report_sample_label</field>
<field name="print_report_name">'Etiquetas - ' + object.name</field>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="lims_management.paperformat_sample_label"/>
<field name="attachment_use" eval="False"/>
</record>
<!-- Template del reporte -->
<template id="report_sample_label">
<t t-call="web.basic_layout">
<t t-set="body_classname">o_report_qweb_pdf</t>
<div class="page">
<t t-foreach="docs" t-as="o">
<div style="width: 96mm; height: 46mm; border: 1px solid #ccc; padding: 2mm; margin: 2mm; font-family: 'DejaVu Sans', Arial, sans-serif; display: inline-block; vertical-align: top; page-break-inside: avoid; overflow: hidden;">
<!-- Encabezado -->
<div style="text-align: center; margin-bottom: 2mm;">
<h4 style="margin: 0; font-size: 14px; font-family: 'DejaVu Sans', Arial, sans-serif;">LABORATORIO CL&#205;NICO</h4>
</div>
<!-- Información del paciente -->
<div style="font-size: 11px; margin-bottom: 2mm; font-family: 'DejaVu Sans', Arial, sans-serif;">
<div><strong>Paciente:</strong> <span t-field="o.patient_id.name"/></div>
<div><strong>ID:</strong> <span t-field="o.patient_id.vat" t-if="o.patient_id.vat"/>
<span t-else="">Sin ID</span>
</div>
</div>
<!-- Información de la muestra -->
<div style="font-size: 10px; margin-bottom: 3mm; font-family: 'DejaVu Sans', Arial, sans-serif;">
<div><strong>Orden:</strong> <span t-field="o.origin"/></div>
<div><strong>Tipo:</strong> <span t-esc="o.get_container_name()"/></div>
<div><strong>Fecha:</strong> <span t-field="o.collection_date" t-options='{"widget": "date"}'/></div>
</div>
<!-- Código de barras -->
<div style="text-align: center; margin-top: 2mm;">
<t t-set="barcode_value" t-value="o.barcode if o.barcode else o.name"/>
<t t-if="barcode_value">
<!-- Usar sintaxis específica de Odoo para código de barras -->
<div style="overflow: hidden; height: 55px;">
<span t-field="o.barcode"
t-options="{'widget': 'barcode', 'type': 'Code128', 'width': 220, 'height': 45, 'humanreadable': 1}"
style="display: block;"/>
</div>
</t>
<t t-else="">
<div style="border: 1px solid #ccc; width: 220px; height: 45px; margin: 0 auto; display: flex; align-items: center; justify-content: center;">
<span style="color: #666;">Sin código de barras</span>
</div>
</t>
</div>
<!-- Análisis a realizar (si caben) -->
<div style="font-size: 9px; margin-top: 1mm; font-family: 'DejaVu Sans', Arial, sans-serif;" t-if="o.analysis_names">
<div><strong>An&#225;lisis:</strong> <span t-field="o.analysis_names"/></div>
</div>
</div>
</t>
</div>
</t>
</template>
</data>
</odoo>

View File

@ -8,6 +8,15 @@
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<!-- Agregar botón de imprimir etiquetas en el header -->
<xpath expr="//header" position="inside">
<button name="action_print_sample_labels"
string="Imprimir Etiquetas"
type="object"
class="btn-primary"
invisible="not is_lab_request or state != 'sale' or not generated_sample_ids"
icon="fa-print"/>
</xpath>
<xpath expr="//field[@name='partner_id']" position="after">
<field name="doctor_id" invisible="not is_lab_request"/>
</xpath>

48
pr_body_9.txt Normal file
View File

@ -0,0 +1,48 @@
## Implementación del flujo de validación y seguridad
### Cambios realizados
#### 1. Ajuste de permisos base (ir.model.access.csv)
- Recepcionista: Solo lectura en lims.test y lims.result
- Técnico: Lectura/escritura pero sin crear/eliminar
- Administrador: Permisos completos
#### 2. Reglas de registro implementadas (lims_security.xml)
- Recepcionistas no pueden editar pruebas
- Técnicos solo pueden editar pruebas no validadas
- Administradores tienen acceso completo
#### 3. Validación de permisos en transiciones (lims_test.py)
- `action_start_process()`: Solo técnicos y administradores
- `action_enter_results()`: Solo técnicos y administradores
- `action_validate()`: Solo administradores
- `action_cancel()`: Técnicos (excepto validadas) y administradores
- `action_draft()`: Solo administradores
#### 4. Trazabilidad mejorada
- stock.lot ahora hereda de mail.thread
- Todos los cambios de estado se registran en el chatter
- Mensajes más descriptivos con contexto
#### 5. Validaciones adicionales
- Control de transiciones de estado válidas
- Verificación del estado de la muestra
- Validación de resultados críticos fuera de rango
- No se puede crear pruebas en estado != draft sin ser admin
#### 6. Vistas actualizadas
- Botones visibles solo para roles apropiados
- Campos de resultados editables solo por técnicos/admin
#### 7. Usuarios demo para pruebas
- Usuario: `recepcionista` / Contraseña: `demo`
- Usuario: `tecnico` / Contraseña: `demo`
- Usuario: `administrador` / Contraseña: `demo`
### Pruebas realizadas
- Verificación de permisos por rol
- Validación de transiciones de estado
- Trazabilidad en chatter
- Restricciones visuales en formularios
Closes #9

View File

@ -0,0 +1,128 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para verificar los códigos de barras de las muestras en la orden S00025
"""
import odoo
import json
from datetime import datetime
def check_order_samples(cr, order_name='S00025'):
"""Verificar las muestras y sus códigos de barras para una orden específica"""
# Buscar la orden
cr.execute("""
SELECT id, name, state, is_lab_request
FROM sale_order
WHERE name = %s
""", (order_name,))
order = cr.fetchone()
if not order:
print(f"❌ No se encontró la orden {order_name}")
return
print(f"✅ Orden encontrada: {order[1]}")
print(f" - ID: {order[0]}")
print(f" - Estado: {order[2]}")
print(f" - Es orden de lab: {order[3]}")
print("")
# Buscar las muestras asociadas a la orden
cr.execute("""
SELECT
sl.id,
sl.name,
sl.barcode,
sl.is_lab_sample,
sl.patient_id,
sl.collection_date,
sl.state,
sl.sample_type_product_id,
rp.name as patient_name
FROM stock_lot sl
LEFT JOIN res_partner rp ON sl.patient_id = rp.id
WHERE sl.id IN (
SELECT lot_id
FROM sale_order_stock_lot_rel
WHERE order_id = %s
)
""", (order[0],))
samples = cr.fetchall()
if not samples:
print(f"❌ No se encontraron muestras para la orden {order_name}")
# Verificar si hay relación en la tabla intermedia
cr.execute("""
SELECT COUNT(*)
FROM sale_order_stock_lot_rel
WHERE order_id = %s
""", (order[0],))
count = cr.fetchone()[0]
print(f" Registros en sale_order_stock_lot_rel: {count}")
return
print(f"📋 Muestras encontradas: {len(samples)}")
print("-" * 80)
for sample in samples:
print(f"Muestra ID: {sample[0]}")
print(f" - Nombre: {sample[1]}")
print(f" - Código de barras: {sample[2] or '❌ VACÍO'}")
print(f" - Es muestra de lab: {sample[3]}")
print(f" - Paciente: {sample[8]} (ID: {sample[4]})")
print(f" - Fecha recolección: {sample[5]}")
print(f" - Estado: {sample[6]}")
print(f" - Tipo muestra ID: {sample[7]}")
# Si no tiene código de barras, generar uno de ejemplo
if not sample[2]:
print(f" ⚠️ FALTA CÓDIGO DE BARRAS - Ejemplo generado: {datetime.now().strftime('%y%m%d')}000001")
print("-" * 40)
# Verificar el campo generated_sample_ids
cr.execute("""
SELECT COUNT(*)
FROM sale_order_stock_lot_rel
WHERE order_id = %s
""", (order[0],))
rel_count = cr.fetchone()[0]
print(f"\n📊 Resumen:")
print(f" - Total muestras en relación: {rel_count}")
print(f" - Muestras sin código de barras: {sum(1 for s in samples if not s[2])}")
# Verificar si el campo barcode es calculado o almacenado
cr.execute("""
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = 'stock_lot'
AND column_name = 'barcode'
""")
col_info = cr.fetchone()
if col_info:
print(f"\n🔍 Información del campo 'barcode':")
print(f" - Tipo de dato: {col_info[1]}")
print(f" - Permite NULL: {col_info[2]}")
print(f" - Valor por defecto: {col_info[3]}")
if __name__ == '__main__':
print("🔍 Verificando códigos de barras para orden S00025...")
print("=" * 80)
db_name = 'lims_demo'
try:
registry = odoo.registry(db_name)
with registry.cursor() as cr:
check_order_samples(cr, 'S00025')
except Exception as e:
print(f"❌ Error: {str(e)}")
print(" Asegúrate de ejecutar este script dentro del contenedor de Odoo")