diff --git a/lims_management/models/__pycache__/product.cpython-312.pyc b/lims_management/models/__pycache__/product.cpython-312.pyc
index 945c75b..7c7c822 100644
Binary files a/lims_management/models/__pycache__/product.cpython-312.pyc and b/lims_management/models/__pycache__/product.cpython-312.pyc differ
diff --git a/lims_management/models/__pycache__/sale_order.cpython-312.pyc b/lims_management/models/__pycache__/sale_order.cpython-312.pyc
index deaa781..7ad7754 100644
Binary files a/lims_management/models/__pycache__/sale_order.cpython-312.pyc and b/lims_management/models/__pycache__/sale_order.cpython-312.pyc differ
diff --git a/lims_management/models/__pycache__/stock_lot.cpython-312.pyc b/lims_management/models/__pycache__/stock_lot.cpython-312.pyc
index 35ea7ee..484c66e 100644
Binary files a/lims_management/models/__pycache__/stock_lot.cpython-312.pyc and b/lims_management/models/__pycache__/stock_lot.cpython-312.pyc differ
diff --git a/lims_management/models/sale_order.py b/lims_management/models/sale_order.py
index a9217b4..e3cc54b 100644
--- a/lims_management/models/sale_order.py
+++ b/lims_management/models/sale_order.py
@@ -1,5 +1,9 @@
# -*- coding: utf-8 -*-
-from odoo import models, fields
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
+import logging
+
+_logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = 'sale.order'
@@ -17,3 +21,145 @@ class SaleOrder(models.Model):
domain="[('is_doctor', '=', True)]",
help="The doctor who referred the patient for this laboratory request."
)
+
+ generated_sample_ids = fields.Many2many(
+ 'stock.lot',
+ 'sale_order_stock_lot_rel',
+ 'order_id',
+ 'lot_id',
+ string='Muestras Generadas',
+ domain="[('is_lab_sample', '=', True)]",
+ readonly=True,
+ help="Laboratory samples automatically generated when this order was confirmed"
+ )
+
+ def action_confirm(self):
+ """Override to generate laboratory samples automatically"""
+ res = super(SaleOrder, self).action_confirm()
+
+ # Generate samples only for laboratory requests
+ for order in self.filtered('is_lab_request'):
+ try:
+ order._generate_lab_samples()
+ except Exception as e:
+ _logger.error(f"Error generating samples for order {order.name}: {str(e)}")
+ # Continue with order confirmation even if sample generation fails
+ # But notify the user
+ order.message_post(
+ body=_("Error al generar muestras automáticamente: %s. "
+ "Por favor, genere las muestras manualmente.") % str(e),
+ message_type='notification'
+ )
+
+ return res
+
+ def _generate_lab_samples(self):
+ """Generate laboratory samples based on the analyses in the order"""
+ self.ensure_one()
+ _logger.info(f"Generating laboratory samples for order {self.name}")
+
+ # Group analyses by sample type
+ sample_groups = self._group_analyses_by_sample_type()
+
+ if not sample_groups:
+ _logger.warning(f"No analyses with sample types found in order {self.name}")
+ return
+
+ # Create samples for each group
+ created_samples = self.env['stock.lot']
+
+ for sample_type_id, group_data in sample_groups.items():
+ sample = self._create_sample_for_group(group_data)
+ if sample:
+ created_samples |= sample
+
+ # Link created samples to the order
+ if created_samples:
+ self.generated_sample_ids = [(6, 0, created_samples.ids)]
+ _logger.info(f"Created {len(created_samples)} samples for order {self.name}")
+
+ # Post message with created samples
+ sample_list = "
"
+ for sample in created_samples:
+ sample_list += f"- {sample.name} - {sample.sample_type_product_id.name}
"
+ sample_list += "
"
+
+ self.message_post(
+ body=_("Muestras generadas automáticamente: %s") % sample_list,
+ message_type='notification'
+ )
+
+ def _group_analyses_by_sample_type(self):
+ """Group order lines by required sample type"""
+ groups = {}
+
+ for line in self.order_line:
+ product = line.product_id
+
+ # Skip non-analysis products
+ if not product.is_analysis:
+ continue
+
+ # Check if analysis has a required sample type
+ if not product.required_sample_type_id:
+ _logger.warning(
+ f"Analysis {product.name} has no required sample type defined"
+ )
+ # Post warning message
+ self.message_post(
+ body=_("Advertencia: El análisis '%s' no tiene tipo de muestra definido") % product.name,
+ message_type='notification'
+ )
+ continue
+
+ sample_type = product.required_sample_type_id
+
+ # Initialize group if not exists
+ if sample_type.id not in groups:
+ groups[sample_type.id] = {
+ 'sample_type': sample_type,
+ 'lines': [],
+ 'total_volume': 0.0,
+ 'analyses': []
+ }
+
+ # Add line to group
+ groups[sample_type.id]['lines'].append(line)
+ groups[sample_type.id]['analyses'].append(product.name)
+ groups[sample_type.id]['total_volume'] += (product.sample_volume_ml or 0.0) * line.product_uom_qty
+
+ return groups
+
+ def _create_sample_for_group(self, group_data):
+ """Create a single sample for a group of analyses"""
+ try:
+ sample_type = group_data['sample_type']
+
+ # Prepare sample values
+ vals = {
+ 'product_id': sample_type.product_variant_id.id,
+ 'patient_id': self.partner_id.id,
+ 'doctor_id': self.doctor_id.id if self.doctor_id else False,
+ 'origin': self.name,
+ 'sample_type_product_id': sample_type.id,
+ 'volume_ml': group_data['total_volume'],
+ 'is_lab_sample': True,
+ 'state': 'pending_collection',
+ 'analysis_names': ', '.join(group_data['analyses'][:3]) +
+ ('...' if len(group_data['analyses']) > 3 else '')
+ }
+
+ # Create the sample
+ sample = self.env['stock.lot'].create(vals)
+
+ _logger.info(
+ f"Created sample {sample.name} for {len(group_data['analyses'])} analyses"
+ )
+
+ return sample
+
+ except Exception as e:
+ _logger.error(f"Error creating sample: {str(e)}")
+ raise UserError(
+ _("Error al crear muestra para %s: %s") % (sample_type.name, str(e))
+ )
diff --git a/lims_management/models/stock_lot.py b/lims_management/models/stock_lot.py
index e24cbf6..1cbb2a7 100644
--- a/lims_management/models/stock_lot.py
+++ b/lims_management/models/stock_lot.py
@@ -40,8 +40,31 @@ class StockLot(models.Model):
string='Collected by',
default=lambda self: self.env.user
)
+
+ doctor_id = fields.Many2one(
+ 'res.partner',
+ string='Médico Referente',
+ domain="[('is_doctor', '=', True)]",
+ help="Médico que ordenó los análisis"
+ )
+
+ origin = fields.Char(
+ string='Origen',
+ help="Referencia a la orden de laboratorio que generó esta muestra"
+ )
+
+ volume_ml = fields.Float(
+ string='Volumen (ml)',
+ help="Volumen total de muestra requerido"
+ )
+
+ analysis_names = fields.Char(
+ string='Análisis',
+ help="Lista de análisis que se realizarán con esta muestra"
+ )
state = fields.Selection([
+ ('pending_collection', 'Pendiente de Recolección'),
('collected', 'Recolectada'),
('received', 'Recibida en Laboratorio'),
('in_process', 'En Proceso'),
diff --git a/pr_description_issue44.txt b/pr_description_issue44.txt
new file mode 100644
index 0000000..a1d9983
--- /dev/null
+++ b/pr_description_issue44.txt
@@ -0,0 +1,37 @@
+## Resumen
+
+Este Pull Request implementa la relación entre análisis y tipos de muestra (Issue #44), estableciendo la base necesaria para la automatización de generación de muestras (Issue #32).
+
+## Cambios principales
+
+### 1. Modelos
+- **ProductTemplate**: Añadidos campos `required_sample_type_id` y `sample_volume_ml` para definir requisitos de muestra en análisis
+- **StockLot**: Añadido campo `sample_type_product_id` manteniendo compatibilidad con `container_type`
+
+### 2. Vistas
+- Actualización de vistas de análisis para mostrar campos de tipo de muestra
+- Actualización de vistas de stock.lot con nuevo campo de tipo de muestra
+- Visualización de relaciones test-muestra en listas y formularios
+
+### 3. Datos
+- Creación de 10 tipos de muestra comunes (Tubo Suero, EDTA, Orina, etc.)
+- Actualización de análisis demo con tipos de muestra requeridos
+- Actualización de muestras demo con referencias a productos tipo muestra
+
+### 4. Herramientas
+- Script de verificación `verify_sample_relationships.py` para validar la implementación
+- Documentación completa en `ISSUE44_IMPLEMENTATION.md`
+
+## Compatibilidad
+
+- Mantiene compatibilidad total con el campo legacy `container_type`
+- Sincronización automática entre campos viejos y nuevos
+- Sin ruptura de funcionalidad existente
+
+## Pruebas
+
+Todas las tareas fueron probadas individualmente con reinicio de instancia efímera y verificación de logs sin errores.
+
+## Próximos pasos
+
+Con esta base implementada, el Issue #32 puede proceder con la automatización de generación de muestras al confirmar órdenes de laboratorio.
\ No newline at end of file