diff --git a/lims_management/__manifest__.py b/lims_management/__manifest__.py index ca286f3..86435d5 100644 --- a/lims_management/__manifest__.py +++ b/lims_management/__manifest__.py @@ -46,6 +46,7 @@ 'views/product_template_parameter_config_views.xml', 'views/parameter_dashboard_views.xml', 'views/menus.xml', + 'views/lims_config_views.xml', 'report/sample_label_report.xml', ], 'demo': [ diff --git a/lims_management/__pycache__/__init__.cpython-312.pyc b/lims_management/__pycache__/__init__.cpython-312.pyc index 9160d9a..9e096b9 100644 Binary files a/lims_management/__pycache__/__init__.cpython-312.pyc and b/lims_management/__pycache__/__init__.cpython-312.pyc differ diff --git a/lims_management/models/__init__.py b/lims_management/models/__init__.py index 7110907..73d5fd3 100644 --- a/lims_management/models/__init__.py +++ b/lims_management/models/__init__.py @@ -10,3 +10,4 @@ from . import rejection_reason from . import lims_test from . import lims_result from . import res_config_settings +from . import lims_config diff --git a/lims_management/models/__pycache__/__init__.cpython-312.pyc b/lims_management/models/__pycache__/__init__.cpython-312.pyc index fc0d06e..caac40f 100644 Binary files a/lims_management/models/__pycache__/__init__.cpython-312.pyc and b/lims_management/models/__pycache__/__init__.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 8e1cd01..5e39c5f 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/lims_config.py b/lims_management/models/lims_config.py new file mode 100644 index 0000000..e9295dc --- /dev/null +++ b/lims_management/models/lims_config.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + +class LimsConfig(models.TransientModel): + _name = 'lims.config.settings' + _inherit = 'res.config.settings' + _description = 'Configuración del Laboratorio' + + auto_resample_on_rejection = fields.Boolean( + string='Re-muestreo Automático al Rechazar', + help='Si está activo, se generará automáticamente una nueva muestra cuando se rechace una existente', + config_parameter='lims_management.auto_resample_on_rejection', + default=True + ) + + resample_state = fields.Selection([ + ('pending_collection', 'Pendiente de Recolección'), + ('collected', 'Recolectada'), + ], string='Estado Inicial para Re-muestras', + help='Estado en el que se crearán las nuevas muestras generadas por re-muestreo', + config_parameter='lims_management.resample_state', + default='pending_collection' + ) + + auto_notify_resample = fields.Boolean( + string='Notificar Re-muestreo Automático', + help='Enviar notificación al recepcionista cuando se genera una nueva muestra por re-muestreo', + config_parameter='lims_management.auto_notify_resample', + default=True + ) + + resample_prefix = fields.Char( + string='Prefijo para Re-muestras', + help='Prefijo que se añadirá al código de las muestras generadas por re-muestreo (ej: RE-)', + config_parameter='lims_management.resample_prefix', + default='RE-' + ) + + max_resample_attempts = fields.Integer( + string='Máximo de Re-muestreos', + help='Número máximo de veces que se puede re-muestrear una muestra (0 = sin límite)', + config_parameter='lims_management.max_resample_attempts', + default=3 + ) \ No newline at end of file diff --git a/lims_management/models/stock_lot.py b/lims_management/models/stock_lot.py index ee44c78..5cd2659 100644 --- a/lims_management/models/stock_lot.py +++ b/lims_management/models/stock_lot.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -from odoo import models, fields, api +from odoo import models, fields, api, _ +from odoo.exceptions import UserError from datetime import datetime import random @@ -105,6 +106,31 @@ class StockLot(models.Model): string='Fecha de Rechazo', readonly=True ) + + # Re-sampling fields + parent_sample_id = fields.Many2one( + 'stock.lot', + string='Muestra Original', + help='Muestra original de la cual esta es un re-muestreo', + domain="[('is_lab_sample', '=', True)]" + ) + child_sample_ids = fields.One2many( + 'stock.lot', + 'parent_sample_id', + string='Re-muestras', + help='Muestras generadas como re-muestreo de esta' + ) + resample_count = fields.Integer( + string='Número de Re-muestreo', + help='Indica cuántas veces se ha re-muestreado esta muestra', + compute='_compute_resample_count', + store=True + ) + is_resample = fields.Boolean( + string='Es Re-muestra', + compute='_compute_is_resample', + store=True + ) def action_collect(self): """Mark sample as collected""" @@ -223,6 +249,27 @@ class StockLot(models.Model): subject='Muestra Rechazada', message_type='notification' ) + + # Check if automatic resample is enabled + IrConfig = self.env['ir.config_parameter'].sudo() + auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True' + + if auto_resample: + try: + # Create resample automatically + resample_action = self.action_create_resample() + self.message_post( + body=_('Re-muestra generada automáticamente debido al rechazo'), + subject='Re-muestreo Automático', + message_type='notification' + ) + except UserError as e: + # If resample creation fails (e.g., max attempts reached), log it + self.message_post( + body=_('No se pudo generar re-muestra automática: %s') % str(e), + subject='Error en Re-muestreo', + message_type='notification' + ) @api.onchange('sample_type_product_id') def _onchange_sample_type_product_id(self): @@ -348,3 +395,110 @@ class StockLot(models.Model): if record.is_lab_sample and not record.barcode: record.barcode = record._generate_unique_barcode() return True + + @api.depends('parent_sample_id') + def _compute_is_resample(self): + """Compute if this sample is a resample""" + for record in self: + record.is_resample = bool(record.parent_sample_id) + + @api.depends('child_sample_ids') + def _compute_resample_count(self): + """Compute the number of times this sample has been resampled""" + for record in self: + record.resample_count = len(record.child_sample_ids) + + def action_create_resample(self): + """Create a new sample as a resample of the current one""" + self.ensure_one() + + # Get configuration + IrConfig = self.env['ir.config_parameter'].sudo() + auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True' + initial_state = IrConfig.get_param('lims_management.resample_state', 'pending_collection') + prefix = IrConfig.get_param('lims_management.resample_prefix', 'RE-') + max_attempts = int(IrConfig.get_param('lims_management.max_resample_attempts', '3')) + + # Check maximum resample attempts + if max_attempts > 0 and self.resample_count >= max_attempts: + raise UserError(_('Se ha alcanzado el número máximo de re-muestreos (%d) para esta muestra.') % max_attempts) + + # Calculate resample number for naming + resample_number = self.resample_count + 1 + + # Prepare values for new sample + vals = { + 'name': f"{prefix}{self.name}-{resample_number}", + 'product_id': self.product_id.id, + 'patient_id': self.patient_id.id, + 'doctor_id': self.doctor_id.id, + 'origin': self.origin, + 'sample_type_product_id': self.sample_type_product_id.id, + 'volume_ml': self.volume_ml, + 'is_lab_sample': True, + 'state': initial_state, + 'analysis_names': self.analysis_names, + 'parent_sample_id': self.id, + 'request_id': self.request_id.id if self.request_id else False, + } + + # Create the resample + resample = self.create(vals) + + # Post message in both samples + self.message_post( + body=_('Re-muestra creada: %s') % resample.name, + subject='Re-muestreo', + message_type='notification' + ) + + resample.message_post( + body=_('Esta es una re-muestra de: %sMotivo: %s') % + (self.name, self.rejection_reason_id.name if self.rejection_reason_id else 'No especificado'), + subject='Re-muestra creada', + message_type='notification' + ) + + # Notify receptionist if configured + auto_notify = IrConfig.get_param('lims_management.auto_notify_resample', 'True') == 'True' + if auto_notify: + self._notify_resample_created(resample) + + # If there's a related order, update it + if self.request_id: + self.request_id.message_post( + body=_('Se ha creado una re-muestra (%s) para la muestra rechazada %s') % (resample.name, self.name), + subject='Re-muestra creada', + message_type='notification' + ) + # Add the new sample to the order's generated samples + self.request_id.generated_sample_ids = [(4, resample.id)] + + return { + 'type': 'ir.actions.act_window', + 'name': 'Re-muestra Creada', + 'res_model': 'stock.lot', + 'res_id': resample.id, + 'view_mode': 'form', + 'target': 'current', + } + + def _notify_resample_created(self, resample): + """Notify receptionist users about the created resample""" + # Find receptionist users + receptionist_group = self.env.ref('lims_management.group_lims_receptionist', raise_if_not_found=False) + if receptionist_group: + receptionist_users = receptionist_group.users + + # Create activities for receptionists + for user in receptionist_users: + self.env['mail.activity'].create({ + 'res_model': 'stock.lot', + 'res_id': resample.id, + 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, + 'summary': _('Nueva re-muestra pendiente de recolección'), + 'note': _('Se ha generado una re-muestra (%s) que requiere recolección. Muestra original: %s') % + (resample.name, self.name), + 'user_id': user.id, + 'date_deadline': fields.Date.today(), + }) diff --git a/lims_management/security/ir.model.access.csv b/lims_management/security/ir.model.access.csv index fbe8000..4a122e9 100644 --- a/lims_management/security/ir.model.access.csv +++ b/lims_management/security/ir.model.access.csv @@ -23,3 +23,4 @@ access_lims_rejection_reason_technician,lims.rejection.reason.technician,model_l access_lims_rejection_reason_admin,lims.rejection.reason.admin,model_lims_rejection_reason,group_lims_admin,1,1,1,1 access_lims_sample_rejection_wizard_user,lims.sample.rejection.wizard.user,model_lims_sample_rejection_wizard,base.group_user,1,1,1,1 access_lims_sample_rejection_wizard_technician,lims.sample.rejection.wizard.technician,model_lims_sample_rejection_wizard,group_lims_technician,1,1,1,1 +access_lims_config_settings_admin,lims.config.settings.admin,model_lims_config_settings,group_lims_admin,1,1,1,1 diff --git a/lims_management/views/lims_config_views.xml b/lims_management/views/lims_config_views.xml new file mode 100644 index 0000000..723bd71 --- /dev/null +++ b/lims_management/views/lims_config_views.xml @@ -0,0 +1,55 @@ + + + + + + lims.config.settings.form + lims.config.settings + + + + + + + + Configuración de Re-muestreo + + + + + + + + + + + + + + El re-muestreo automático permite generar una nueva muestra cuando se rechaza una existente. + Las notificaciones se enviarán a todos los usuarios con rol de Recepcionista. + + + + + + + + + + Configuración del Laboratorio + lims.config.settings + form + inline + {'dialog_size': 'medium'} + + + + + + \ No newline at end of file diff --git a/lims_management/views/stock_lot_views.xml b/lims_management/views/stock_lot_views.xml index 35dce0c..ec33607 100644 --- a/lims_management/views/stock_lot_views.xml +++ b/lims_management/views/stock_lot_views.xml @@ -16,6 +16,8 @@ + + @@ -39,6 +41,12 @@ class="btn-danger" invisible="state in ['completed', 'rejected', 'disposed', 'cancelled']"/> + @@ -81,6 +89,25 @@ + + + + + + + + + + + + + + + + + + + @@ -100,6 +127,8 @@ + + @@ -111,6 +140,7 @@ +
El re-muestreo automático permite generar una nueva muestra cuando se rechaza una existente.
Las notificaciones se enviarán a todos los usuarios con rol de Recepcionista.