From 87640b48e02161ef0ef0f80521ac573b03c57b26 Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Tue, 15 Jul 2025 22:49:43 -0600 Subject: [PATCH 1/2] feat(#58): Implementar flujo de rechazo de muestras MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Agregar estado 'rejected' al ciclo de vida de la muestra - Crear modelo lims.rejection.reason para gestionar motivos de rechazo - Agregar campos de rechazo en stock.lot (reason, notes, rejected_by, date) - Crear wizard para proceso de rechazo con validaciones - Implementar acción de rechazo con notificaciones - Crear vistas para muestras rechazadas con filtros y búsquedas - Agregar 10 motivos de rechazo predefinidos (hemolizada, coagulada, etc.) - Incluir permisos de seguridad para los nuevos modelos - Agregar menús para gestión de rechazos y muestras rechazadas - Corregir compatibilidad con Odoo 18 en vistas existentes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .claude/settings.local.json | 3 +- lims_management/__init__.py | 1 + lims_management/__manifest__.py | 3 + .../__pycache__/__init__.cpython-312.pyc | Bin 178 -> 209 bytes .../data/rejection_reason_data.xml | 95 ++++++++++++++++++ lims_management/models/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 524 -> 564 bytes .../__pycache__/sale_order.cpython-312.pyc | Bin 13714 -> 13714 bytes .../__pycache__/stock_lot.cpython-312.pyc | Bin 9881 -> 14341 bytes lims_management/models/rejection_reason.py | 61 +++++++++++ lims_management/models/stock_lot.py | 71 ++++++++++++- lims_management/security/ir.model.access.csv | 5 + lims_management/views/lims_test_views.xml | 2 +- lims_management/views/menus.xml | 15 +++ .../views/rejection_reason_views.xml | 93 +++++++++++++++++ lims_management/views/stock_lot_views.xml | 65 +++++++++++- lims_management/wizards/__init__.py | 2 + .../wizards/sample_rejection_wizard.py | 94 +++++++++++++++++ .../wizards/sample_rejection_wizard_views.xml | 45 +++++++++ pr_body_10.txt | 32 ++++++ 20 files changed, 583 insertions(+), 5 deletions(-) create mode 100644 lims_management/data/rejection_reason_data.xml create mode 100644 lims_management/models/rejection_reason.py create mode 100644 lims_management/views/rejection_reason_views.xml create mode 100644 lims_management/wizards/__init__.py create mode 100644 lims_management/wizards/sample_rejection_wizard.py create mode 100644 lims_management/wizards/sample_rejection_wizard_views.xml create mode 100644 pr_body_10.txt diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 645c6df..2f0bda8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -24,7 +24,8 @@ "Bash(true)", "Bash(bash:*)", "Bash(grep:*)", - "Bash(gh pr merge:*)" + "Bash(gh pr merge:*)", + "Bash(git cherry-pick:*)" ], "deny": [] } diff --git a/lims_management/__init__.py b/lims_management/__init__.py index cde864b..f553d8f 100644 --- a/lims_management/__init__.py +++ b/lims_management/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from . import models +from . import wizards diff --git a/lims_management/__manifest__.py b/lims_management/__manifest__.py index 3895a7a..ca286f3 100644 --- a/lims_management/__manifest__.py +++ b/lims_management/__manifest__.py @@ -29,9 +29,12 @@ 'data/product_category.xml', 'data/sample_types.xml', 'data/lims_sequence.xml', + 'data/rejection_reason_data.xml', 'views/partner_views.xml', 'views/analysis_views.xml', 'views/sale_order_views.xml', + 'views/rejection_reason_views.xml', + 'wizards/sample_rejection_wizard_views.xml', 'views/stock_lot_views.xml', 'views/lims_test_views.xml', 'views/lims_result_views.xml', diff --git a/lims_management/__pycache__/__init__.cpython-312.pyc b/lims_management/__pycache__/__init__.cpython-312.pyc index 98b96b49daac6600d49f031f5b440a5b864d10c3..9160d9a6f0c2b097c38378fa202761479edf44a4 100644 GIT binary patch delta 129 zcmdnQc#)C!G%qg~0}$-dEYEP6$Sdg~1LRC+NMT4}%wfo7jACR2v6+BurYI&xhDv5l zmY0k`C7O)4*mCnzQge!dEcWuus>Gs{Vn0phTMR|aK4xgJ=;uPy_%mXd3ST delta 98 zcmcb}xQUVXG%qg~0}zPWmt^Qo + + + + + Muestra Insuficiente + INSUF + El volumen de muestra recibido es insuficiente para realizar los análisis solicitados + high + + 10 + + + + Muestra Hemolizada + HEMO + La muestra presenta hemólisis que interfiere con los análisis + high + + 20 + + + + Muestra Coagulada + COAG + La muestra presenta coágulos que impiden su procesamiento + high + + 30 + + + + Muestra Lipémica + LIP + La muestra presenta lipemia excesiva que interfiere con los análisis + medium + + 40 + + + + Recipiente Inadecuado + RECIP + El tipo de recipiente utilizado no es apropiado para el análisis solicitado + high + + 50 + + + + Identificación Incorrecta + ID + La identificación de la muestra no coincide con la solicitud o es ilegible + critical + + 60 + + + + Muestra sin Rotular + NOLAB + La muestra no tiene etiqueta de identificación + critical + + 70 + + + + Condiciones de Transporte Inadecuadas + TRANS + La muestra no fue transportada en las condiciones requeridas (temperatura, tiempo, etc.) + high + + 80 + + + + Muestra Contaminada + CONT + La muestra presenta signos evidentes de contaminación + critical + + 90 + + + + Tiempo de Entrega Excedido + TIME + La muestra fue recibida fuera del tiempo límite establecido para su procesamiento + high + + 100 + + + \ No newline at end of file diff --git a/lims_management/models/__init__.py b/lims_management/models/__init__.py index 8e58433..7110907 100644 --- a/lims_management/models/__init__.py +++ b/lims_management/models/__init__.py @@ -6,6 +6,7 @@ from . import product from . import partner from . import sale_order from . import stock_lot +from . import rejection_reason from . import lims_test from . import lims_result from . import res_config_settings diff --git a/lims_management/models/__pycache__/__init__.cpython-312.pyc b/lims_management/models/__pycache__/__init__.cpython-312.pyc index 902966496aa38fd59151600e974c23a7c829ead8..fc0d06e3cf4f6bc158eee4af79a39a3880db5464 100644 GIT binary patch delta 112 zcmeBS*}}qmnwOW00SMYP%QFHd@=7vxOjNHGNMT6f&SA*qj^buysN~V)ow%YwP@pI^ zD>b>e!060;&&M& L?=dJA2>?X_Sl%1) delta 76 zcmdnO(!;`gnwOW00SL^#mSq@D>(G%qg~0}yOi-N@Bp3II7?1xNq@ delta 19 ZcmbP~Jt>>(G%qg~0}#v--pJKq3II4(1sMPU diff --git a/lims_management/models/__pycache__/stock_lot.cpython-312.pyc b/lims_management/models/__pycache__/stock_lot.cpython-312.pyc index e690aed8a0e6193bd385cf6d93964461750547af..8e1cd0131210198ae74f2b98601755983cd729c6 100644 GIT binary patch delta 6376 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 bKtEQ6wiTg6RyKIzxl=PIXTD_cSttEB0CHW3 delta 2144 zcmZ`)TWk|Y6x~_B)~`J5#Ia+?F>&Jfm6VW>G`uMdK|n$gZ}W&;&w`gYyJ2<%Bt?Zt zX;mbo0HZ1(A(i$Il0GDV^4}u$qv0b`yK1HK(MtIO(e{V0&Rqw`JYq{ncjnxC&YhW^ z-OtAF4psf^b~`xsoAqAGUyaavGGMRe5VF zyxhigh<=ai1GV2E?7`lJy0Tt%SuaqJeYgSp7XtI<6|lrrTE{0*I)O>yi^NwzKbriUJhMO<-#%0#w9+uW( zr0r&DA8y3GuR6zl6^X6*eU{jU`x(aYCWh^kn-g1f;gF`}M)xb)lwF0Wv!H17u6YjG zX_qB~?8O_FUSzd1KT<^>Tc@2ySW_Aww*7{>X{Qioc|$@HA%R}DFGqFjfU0G&LZU1! zVIoFJgvGj`k!)^MlF!j!+&&(2x7!kgyv61m3MV^a?zVu5|X5uk-CpgxA=AFQBlAI;=EG9k%>>(q&fiTP2F;TY^luIJft8$90WhGf8BiiJ+nBYl> zcC-Xg6CG^%2&wc=%Wc$LOhwyK@BkFsj>x03a!F3u3^6hU@(1WxYt;4Hvv#kw2hGqY zt!G&E`Zg;(}7*OLzLACB916*c~+o% z8R&wlk+YgKCK3^bR#nWBT)yOKl4Px7VZ-!hde9##mpXA)6Gz60cviYhYUs1{!_r#L z(62gT{Fhwur;b~u&;^#yUK1BV-K0*8=~gUV98)mcvk=v~c9v?U=%cO#|CB5K-8FiZLPwHBV0i5Z4_VeUB0*|0b+%2*}vK_+JarU2#`JYaVfV&mKehydV3 z)IZqUZif4nuWl^%ZrqFd?P2S44_DoA-}wXn*K2ogxi;{^$6Jn|7Xoid0;J1Pyd0&T z3pCkx6gAW7zP8jSOjx%JDOfBRM&uz_mOFSUY+;8nQ4qJ0Z|Rf1=K8z|)qQbs_R94u K|8Wo-ss9660P(B< diff --git a/lims_management/models/rejection_reason.py b/lims_management/models/rejection_reason.py new file mode 100644 index 0000000..bd65d1a --- /dev/null +++ b/lims_management/models/rejection_reason.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + +class LimsRejectionReason(models.Model): + _name = 'lims.rejection.reason' + _description = 'Motivo de Rechazo de Muestra' + _order = 'sequence, name' + + name = fields.Char( + string='Motivo', + required=True + ) + code = fields.Char( + string='Código', + required=True, + help="Código único para identificar el motivo" + ) + description = fields.Text( + string='Descripción', + help="Descripción detallada del motivo de rechazo" + ) + active = fields.Boolean( + string='Activo', + default=True + ) + sequence = fields.Integer( + string='Secuencia', + default=10, + help="Orden de aparición en las listas" + ) + requires_new_sample = fields.Boolean( + string='Requiere Nueva Muestra', + default=True, + help="Indica si este tipo de rechazo requiere solicitar una nueva muestra" + ) + severity = fields.Selection([ + ('low', 'Baja'), + ('medium', 'Media'), + ('high', 'Alta'), + ('critical', 'Crítica') + ], string='Severidad', default='medium', + help="Severidad del problema que causa el rechazo") + + # Statistics + rejection_count = fields.Integer( + string='Cantidad de Rechazos', + compute='_compute_rejection_count', + help="Número de muestras rechazadas con este motivo" + ) + + @api.depends('name') + def _compute_rejection_count(self): + for record in self: + record.rejection_count = self.env['stock.lot'].search_count([ + ('rejection_reason_id', '=', record.id), + ('state', '=', 'rejected') + ]) + + _sql_constraints = [ + ('code_uniq', 'unique (code)', 'El código del motivo de rechazo debe ser único!'), + ] \ No newline at end of file diff --git a/lims_management/models/stock_lot.py b/lims_management/models/stock_lot.py index 6d8e313..ee44c78 100644 --- a/lims_management/models/stock_lot.py +++ b/lims_management/models/stock_lot.py @@ -82,8 +82,29 @@ class StockLot(models.Model): ('analyzed', 'Analizada'), ('stored', 'Almacenada'), ('disposed', 'Desechada'), - ('cancelled', 'Cancelada') + ('cancelled', 'Cancelada'), + ('rejected', 'Rechazada') ], string='Estado', default='collected', tracking=True) + + # Rejection fields + rejection_reason_id = fields.Many2one( + 'lims.rejection.reason', + string='Motivo de Rechazo', + tracking=True + ) + rejection_notes = fields.Text( + string='Notas de Rechazo', + help="Información adicional sobre el rechazo" + ) + rejected_by = fields.Many2one( + 'res.users', + string='Rechazado por', + readonly=True + ) + rejection_date = fields.Datetime( + string='Fecha de Rechazo', + readonly=True + ) def action_collect(self): """Mark sample as collected""" @@ -155,6 +176,54 @@ class StockLot(models.Model): message_type='notification' ) + def action_open_rejection_wizard(self): + """Open the rejection wizard""" + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'Rechazar Muestra', + 'res_model': 'lims.sample.rejection.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_sample_id': self.id, + } + } + + def action_reject(self): + """Reject the sample - to be called from wizard""" + self.ensure_one() + if self.state == 'completed': + raise ValueError('No se puede rechazar una muestra ya completada') + + # This method is called from the wizard, so rejection fields should already be set + self.write({ + 'state': 'rejected', + 'rejected_by': self.env.user.id, + 'rejection_date': fields.Datetime.now() + }) + + reason_name = self.rejection_reason_id.name if self.rejection_reason_id else 'Sin especificar' + notes = self.rejection_notes or '' + + body = f'Muestra rechazada por {self.env.user.name}
Motivo: {reason_name}' + if notes: + body += f'
Notas: {notes}' + + self.message_post( + body=body, + subject='Estado actualizado: Rechazada', + message_type='notification' + ) + + # Notify related sale order if exists + if self.request_id: + self.request_id.message_post( + body=f'La muestra {self.name} ha sido rechazada. Motivo: {reason_name}', + subject='Muestra Rechazada', + message_type='notification' + ) + @api.onchange('sample_type_product_id') def _onchange_sample_type_product_id(self): """Synchronize container_type when sample_type_product_id changes""" diff --git a/lims_management/security/ir.model.access.csv b/lims_management/security/ir.model.access.csv index 1e120c3..ef28518 100644 --- a/lims_management/security/ir.model.access.csv +++ b/lims_management/security/ir.model.access.csv @@ -13,3 +13,8 @@ access_lims_test_admin,lims.test.admin,model_lims_test,group_lims_admin,1,1,1,1 access_lims_result_receptionist,lims.result.receptionist,model_lims_result,group_lims_receptionist,1,0,0,0 access_lims_result_technician,lims.result.technician,model_lims_result,group_lims_technician,1,1,1,0 access_lims_result_admin,lims.result.admin,model_lims_result,group_lims_admin,1,1,1,1 +access_lims_rejection_reason_user,lims.rejection.reason.user,model_lims_rejection_reason,base.group_user,1,0,0,0 +access_lims_rejection_reason_technician,lims.rejection.reason.technician,model_lims_rejection_reason,group_lims_technician,1,0,0,0 +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 diff --git a/lims_management/views/lims_test_views.xml b/lims_management/views/lims_test_views.xml index e4b721c..262938d 100644 --- a/lims_management/views/lims_test_views.xml +++ b/lims_management/views/lims_test_views.xml @@ -64,7 +64,7 @@ + + + + + + + + + + lims.rejection.reason.list + lims.rejection.reason + + + + + + + + + + + + + + + + lims.rejection.reason.form + lims.rejection.reason + +
+ + +
+
+ + + + + + + + + + + + + + + +
+
+
+
+ + + + lims.rejection.reason.search + lims.rejection.reason + + + + + + + + + + + + + + + + + + + + + Motivos de Rechazo + lims.rejection.reason + list,form + + {'search_default_active': 1} + +

+ Configure los motivos de rechazo de muestras +

+

+ Los motivos de rechazo permiten categorizar y documentar + las razones por las cuales una muestra no puede ser procesada. +

+
+
+
\ 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 ec3f47d..35dce0c 100644 --- a/lims_management/views/stock_lot_views.xml +++ b/lims_management/views/stock_lot_views.xml @@ -15,7 +15,7 @@ - +
@@ -33,7 +33,13 @@