@@ -69,10 +75,65 @@
invisible="sample_type_product_id != False"/>
+
+
+
+
+
+
+
+
+ lab.sample.search
+ stock.lot
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Muestras Rechazadas
+ stock.lot
+ list,form
+ [('is_lab_sample', '=', True), ('state', '=', 'rejected')]
+ {'search_default_rejected': 1, 'default_is_lab_sample': True}
+
+
+
+ No hay muestras rechazadas
+
+
+ Las muestras rechazadas aparecerán aquí con información
+ sobre el motivo del rechazo y las acciones tomadas.
+
+
+
+
diff --git a/lims_management/wizards/__init__.py b/lims_management/wizards/__init__.py
new file mode 100644
index 0000000..c203ef3
--- /dev/null
+++ b/lims_management/wizards/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import sample_rejection_wizard
\ No newline at end of file
diff --git a/lims_management/wizards/sample_rejection_wizard.py b/lims_management/wizards/sample_rejection_wizard.py
new file mode 100644
index 0000000..9bea34b
--- /dev/null
+++ b/lims_management/wizards/sample_rejection_wizard.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+from odoo import models, fields, api
+from odoo.exceptions import ValidationError
+
+class SampleRejectionWizard(models.TransientModel):
+ _name = 'lims.sample.rejection.wizard'
+ _description = 'Wizard para Rechazo de Muestras'
+
+ sample_id = fields.Many2one(
+ 'stock.lot',
+ string='Muestra',
+ required=True,
+ readonly=True,
+ domain=[('is_lab_sample', '=', True)]
+ )
+
+ rejection_reason_id = fields.Many2one(
+ 'lims.rejection.reason',
+ string='Motivo de Rechazo',
+ required=True,
+ domain=[('active', '=', True)]
+ )
+
+ rejection_notes = fields.Text(
+ string='Notas Adicionales',
+ help="Información adicional sobre el rechazo"
+ )
+
+ requires_new_sample = fields.Boolean(
+ string='Requiere Nueva Muestra',
+ related='rejection_reason_id.requires_new_sample',
+ readonly=True
+ )
+
+ create_new_sample = fields.Boolean(
+ string='Crear Nueva Solicitud',
+ help="Crear automáticamente una nueva solicitud de muestra"
+ )
+
+ @api.model
+ def default_get(self, fields):
+ res = super(SampleRejectionWizard, self).default_get(fields)
+ active_id = self.env.context.get('active_id')
+ if active_id:
+ sample = self.env['stock.lot'].browse(active_id)
+ res['sample_id'] = sample.id
+ return res
+
+ @api.onchange('rejection_reason_id')
+ def _onchange_rejection_reason_id(self):
+ if self.rejection_reason_id and self.rejection_reason_id.requires_new_sample:
+ self.create_new_sample = True
+
+ def action_reject_sample(self):
+ """Reject the sample with the provided reason"""
+ self.ensure_one()
+
+ if not self.sample_id:
+ raise ValidationError('No se ha seleccionado ninguna muestra')
+
+ if self.sample_id.state == 'completed':
+ raise ValidationError('No se puede rechazar una muestra ya completada')
+
+ # Update sample with rejection information
+ self.sample_id.write({
+ 'rejection_reason_id': self.rejection_reason_id.id,
+ 'rejection_notes': self.rejection_notes
+ })
+
+ # Call the rejection method on the sample
+ self.sample_id.action_reject()
+
+ # Create new sample request if needed
+ if self.create_new_sample and self.sample_id.request_id:
+ self._create_new_sample_request()
+
+ return {'type': 'ir.actions.act_window_close'}
+
+ def _create_new_sample_request(self):
+ """Create a new sample request based on the rejected one"""
+ original_order = self.sample_id.request_id
+
+ # Create a note in the original order
+ original_order.message_post(
+ body=f'Se solicitará una nueva muestra debido al rechazo. Motivo: {self.rejection_reason_id.name}',
+ subject='Nueva Muestra Solicitada',
+ message_type='notification'
+ )
+
+ # Here you could implement logic to create a new sale.order
+ # or a specific request for a new sample
+ # For now, we'll just add a note
+
+ return True
\ No newline at end of file
diff --git a/lims_management/wizards/sample_rejection_wizard_views.xml b/lims_management/wizards/sample_rejection_wizard_views.xml
new file mode 100644
index 0000000..eb968e0
--- /dev/null
+++ b/lims_management/wizards/sample_rejection_wizard_views.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ lims.sample.rejection.wizard.form
+ lims.sample.rejection.wizard
+
+
+
+
+
+
+
+ Rechazar Muestra
+ lims.sample.rejection.wizard
+ form
+ new
+ {'default_sample_id': active_id}
+
+
\ No newline at end of file
diff --git a/pr_body_10.txt b/pr_body_10.txt
new file mode 100644
index 0000000..d8673a2
--- /dev/null
+++ b/pr_body_10.txt
@@ -0,0 +1,32 @@
+## Descripción
+Implementación del sistema de etiquetas con código de barras para las muestras de laboratorio.
+
+## Cambios realizados
+
+### Funcionalidad principal
+- Creado reporte QWeb para imprimir etiquetas de muestras (100x50mm)
+- Implementado botón 'Imprimir Etiquetas' en órdenes de laboratorio
+- Las etiquetas incluyen:
+ - Información del paciente
+ - Código de muestra y orden
+ - Tipo de contenedor
+ - Fecha de recolección
+ - Código de barras Code128
+ - Lista de análisis a realizar
+
+### Correcciones técnicas
+- **Código de barras**: Corregido problema de visualización usando widget nativo de Odoo 18
+- **Caracteres especiales**: Solucionado problema de codificación UTF-8 con referencias numéricas
+- **Layout**: Ajustado diseño para mostrar múltiples etiquetas por página sin solapamiento
+- **Espaciado**: Optimizado el tamaño y posición del código de barras
+
+## Testing
+- Probado con órdenes que tienen múltiples muestras
+- Verificado que los códigos de barras se generen y visualicen correctamente
+- Confirmado que los caracteres en español (tildes, ñ) se muestren bien
+- Validado que no hay solapamiento entre etiquetas
+
+## Capturas
+- Los códigos de barras ahora se visualizan correctamente
+- Las etiquetas respetan el formato 100x50mm
+- Múltiples etiquetas por página sin problemas de diseño
diff --git a/test/check_demo_users.py b/test/check_demo_users.py
new file mode 100644
index 0000000..82704c3
--- /dev/null
+++ b/test/check_demo_users.py
@@ -0,0 +1,63 @@
+import odoo
+import json
+
+def check_demo_users(cr):
+ """Verificar si los usuarios demo fueron creados"""
+ cr.execute("""
+ SELECT
+ u.id,
+ u.login,
+ u.name,
+ u.active,
+ array_agg(g.name) as groups
+ FROM res_users u
+ LEFT JOIN res_groups_users_rel rel ON rel.uid = u.id
+ LEFT JOIN res_groups g ON g.id = rel.gid
+ WHERE u.login IN ('recepcionista', 'tecnico', 'administrador')
+ GROUP BY u.id, u.login, u.name, u.active
+ ORDER BY u.login
+ """)
+
+ users = cr.fetchall()
+
+ print("\n=== USUARIOS DEMO CREADOS ===")
+ print("-" * 60)
+
+ if not users:
+ print("❌ NO se encontraron usuarios demo")
+ return
+
+ for user in users:
+ user_id, login, name, active, groups = user
+ status = "✓ Activo" if active else "✗ Inactivo"
+ print(f"\nUsuario: {login}")
+ print(f" ID: {user_id}")
+ print(f" Nombre: {name}")
+ print(f" Estado: {status}")
+ print(f" Grupos: {', '.join(groups) if groups[0] else 'Sin grupos'}")
+
+ print("\n" + "-" * 60)
+ print(f"Total usuarios demo encontrados: {len(users)}")
+
+ # Verificar contraseñas (solo para confirmar que pueden loguearse)
+ expected_users = {
+ 'recepcionista': 'Recepcionista Demo',
+ 'tecnico': 'Técnico Demo',
+ 'administrador': 'Administrador Lab Demo'
+ }
+
+ missing = []
+ for login, expected_name in expected_users.items():
+ if not any(u[1] == login for u in users):
+ missing.append(login)
+
+ if missing:
+ print(f"\n⚠️ Usuarios faltantes: {', '.join(missing)}")
+ else:
+ print("\n✅ Todos los usuarios demo esperados fueron creados")
+
+if __name__ == '__main__':
+ db_name = 'lims_demo'
+ registry = odoo.registry(db_name)
+ with registry.cursor() as cr:
+ check_demo_users(cr)
\ No newline at end of file