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:
commit
36a9772a07
|
@ -23,7 +23,8 @@
|
|||
"Bash(find:*)",
|
||||
"Bash(true)",
|
||||
"Bash(bash:*)",
|
||||
"Bash(grep:*)"
|
||||
"Bash(grep:*)",
|
||||
"Bash(gh pr merge:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
|
BIN
documents/logs/Etiquetas de Muestras (4).pdf
Normal file
BIN
documents/logs/Etiquetas de Muestras (4).pdf
Normal file
Binary file not shown.
BIN
documents/logs/Screenshot_1.png
Normal file
BIN
documents/logs/Screenshot_1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
BIN
documents/logs/Screenshot_2.png
Normal file
BIN
documents/logs/Screenshot_2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
|
@ -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',
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
1
lims_management/report/__init__.py
Normal file
1
lims_management/report/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
89
lims_management/report/sample_label_report.xml
Normal file
89
lims_management/report/sample_label_report.xml
Normal 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Í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álisis:</strong> <span t-field="o.analysis_names"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
|
@ -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
48
pr_body_9.txt
Normal 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
|
128
test/check_sample_barcodes.py
Normal file
128
test/check_sample_barcodes.py
Normal 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")
|
Loading…
Reference in New Issue
Block a user