Merge pull request #61 from feature/58-sample-rejection-flow
feat(#58): Implementar flujo de rechazo de muestras
This commit is contained in:
commit
0751f272ae
|
@ -24,7 +24,8 @@
|
|||
"Bash(true)",
|
||||
"Bash(bash:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(gh pr merge:*)"
|
||||
"Bash(gh pr merge:*)",
|
||||
"Bash(git cherry-pick:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
|
BIN
documents/logs/Screenshot_3.png
Normal file
BIN
documents/logs/Screenshot_3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
|
@ -36,7 +36,6 @@ odoo_command = [
|
|||
"-d", DB_NAME,
|
||||
"-i", MODULES_TO_INSTALL,
|
||||
"--load-language", "es_ES",
|
||||
"--without-demo=", # Forzar carga de datos demo
|
||||
"--stop-after-init"
|
||||
]
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import wizards
|
||||
|
|
|
@ -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',
|
||||
|
|
Binary file not shown.
95
lims_management/data/rejection_reason_data.xml
Normal file
95
lims_management/data/rejection_reason_data.xml
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Rejection Reasons -->
|
||||
<record id="rejection_reason_insufficient" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Insuficiente</field>
|
||||
<field name="code">INSUF</field>
|
||||
<field name="description">El volumen de muestra recibido es insuficiente para realizar los análisis solicitados</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_hemolyzed" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Hemolizada</field>
|
||||
<field name="code">HEMO</field>
|
||||
<field name="description">La muestra presenta hemólisis que interfiere con los análisis</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_coagulated" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Coagulada</field>
|
||||
<field name="code">COAG</field>
|
||||
<field name="description">La muestra presenta coágulos que impiden su procesamiento</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">30</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_lipemic" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Lipémica</field>
|
||||
<field name="code">LIP</field>
|
||||
<field name="description">La muestra presenta lipemia excesiva que interfiere con los análisis</field>
|
||||
<field name="severity">medium</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">40</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_wrong_container" model="lims.rejection.reason">
|
||||
<field name="name">Recipiente Inadecuado</field>
|
||||
<field name="code">RECIP</field>
|
||||
<field name="description">El tipo de recipiente utilizado no es apropiado para el análisis solicitado</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">50</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_wrong_id" model="lims.rejection.reason">
|
||||
<field name="name">Identificación Incorrecta</field>
|
||||
<field name="code">ID</field>
|
||||
<field name="description">La identificación de la muestra no coincide con la solicitud o es ilegible</field>
|
||||
<field name="severity">critical</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">60</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_no_label" model="lims.rejection.reason">
|
||||
<field name="name">Muestra sin Rotular</field>
|
||||
<field name="code">NOLAB</field>
|
||||
<field name="description">La muestra no tiene etiqueta de identificación</field>
|
||||
<field name="severity">critical</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">70</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_transport" model="lims.rejection.reason">
|
||||
<field name="name">Condiciones de Transporte Inadecuadas</field>
|
||||
<field name="code">TRANS</field>
|
||||
<field name="description">La muestra no fue transportada en las condiciones requeridas (temperatura, tiempo, etc.)</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">80</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_contaminated" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Contaminada</field>
|
||||
<field name="code">CONT</field>
|
||||
<field name="description">La muestra presenta signos evidentes de contaminación</field>
|
||||
<field name="severity">critical</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">90</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_expired" model="lims.rejection.reason">
|
||||
<field name="name">Tiempo de Entrega Excedido</field>
|
||||
<field name="code">TIME</field>
|
||||
<field name="description">La muestra fue recibida fuera del tiempo límite establecido para su procesamiento</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">100</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
|
@ -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
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
61
lims_management/models/rejection_reason.py
Normal file
61
lims_management/models/rejection_reason.py
Normal file
|
@ -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!'),
|
||||
]
|
|
@ -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}<br/>Motivo: {reason_name}'
|
||||
if notes:
|
||||
body += f'<br/>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"""
|
||||
|
|
|
@ -6,6 +6,11 @@ access_product_template_parameter_manager,product.template.parameter.manager,mod
|
|||
access_lims_parameter_range_user,lims.parameter.range.user,model_lims_parameter_range,base.group_user,1,0,0,0
|
||||
access_lims_parameter_range_manager,lims.parameter.range.manager,model_lims_parameter_range,group_lims_admin,1,1,1,1
|
||||
access_sale_order_receptionist,sale.order.receptionist,sale.model_sale_order,group_lims_receptionist,1,1,1,0
|
||||
access_sale_order_line_receptionist,sale.order.line.receptionist,sale.model_sale_order_line,group_lims_receptionist,1,1,1,0
|
||||
access_sale_order_technician,sale.order.technician,sale.model_sale_order,group_lims_technician,1,0,0,0
|
||||
access_sale_order_line_technician,sale.order.line.technician,sale.model_sale_order_line,group_lims_technician,1,0,0,0
|
||||
access_sale_order_admin,sale.order.admin,sale.model_sale_order,group_lims_admin,1,1,1,1
|
||||
access_sale_order_line_admin,sale.order.line.admin,sale.model_sale_order_line,group_lims_admin,1,1,1,1
|
||||
access_stock_lot_user,stock.lot.user,stock.model_stock_lot,base.group_user,1,1,1,1
|
||||
access_lims_test_receptionist,lims.test.receptionist,model_lims_test,group_lims_receptionist,1,0,0,0
|
||||
access_lims_test_technician,lims.test.technician,model_lims_test,group_lims_technician,1,1,1,0
|
||||
|
@ -13,3 +18,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
|
||||
|
|
|
|
@ -64,7 +64,7 @@
|
|||
<notebook>
|
||||
<page string="Resultados">
|
||||
<field name="result_ids"
|
||||
readonly="state in ['validated', 'cancelled'] or not env.user.has_group('lims_management.group_lims_technician')"
|
||||
readonly="state in ['validated', 'cancelled']"
|
||||
context="{'default_test_id': id, 'default_patient_id': patient_id, 'default_test_date': create_date}"
|
||||
mode="list">
|
||||
<list string="Resultados" editable="bottom"
|
||||
|
|
|
@ -102,6 +102,14 @@
|
|||
action="action_lims_lab_sample"
|
||||
sequence="16"/>
|
||||
|
||||
<!-- Menú para Muestras Rechazadas -->
|
||||
<menuitem
|
||||
id="lims_menu_lab_samples_rejected"
|
||||
name="Muestras Rechazadas"
|
||||
parent="lims_menu_root"
|
||||
action="action_lab_sample_rejected"
|
||||
sequence="17"/>
|
||||
|
||||
<!-- Submenú de Laboratorio -->
|
||||
<menuitem
|
||||
id="lims_menu_laboratory"
|
||||
|
@ -265,6 +273,13 @@
|
|||
action="action_lims_parameter_statistics"
|
||||
sequence="40"/>
|
||||
|
||||
<!-- Menú de Motivos de Rechazo -->
|
||||
<menuitem id="menu_lims_rejection_reason"
|
||||
name="Motivos de Rechazo"
|
||||
parent="lims_menu_config"
|
||||
action="action_lims_rejection_reason"
|
||||
sequence="50"/>
|
||||
|
||||
<!-- Menú de configuración de ajustes -->
|
||||
<menuitem id="menu_lims_config_settings"
|
||||
name="Ajustes"
|
||||
|
|
93
lims_management/views/rejection_reason_views.xml
Normal file
93
lims_management/views/rejection_reason_views.xml
Normal file
|
@ -0,0 +1,93 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- List View for Rejection Reasons -->
|
||||
<record id="view_lims_rejection_reason_list" model="ir.ui.view">
|
||||
<field name="name">lims.rejection.reason.list</field>
|
||||
<field name="model">lims.rejection.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Motivos de Rechazo" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="severity" widget="badge"/>
|
||||
<field name="requires_new_sample"/>
|
||||
<field name="rejection_count"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View for Rejection Reasons -->
|
||||
<record id="view_lims_rejection_reason_form" model="ir.ui.view">
|
||||
<field name="name">lims.rejection.reason.form</field>
|
||||
<field name="model">lims.rejection.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Motivo de Rechazo">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archivado" invisible="active"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Motivo de rechazo..."/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
<field name="severity"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="requires_new_sample"/>
|
||||
<field name="active"/>
|
||||
<field name="rejection_count"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Descripción">
|
||||
<field name="description" nolabel="1" placeholder="Descripción detallada del motivo..."/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View for Rejection Reasons -->
|
||||
<record id="view_lims_rejection_reason_search" model="ir.ui.view">
|
||||
<field name="name">lims.rejection.reason.search</field>
|
||||
<field name="model">lims.rejection.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Motivos de Rechazo">
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<filter string="Activos" name="active" domain="[('active', '=', True)]"/>
|
||||
<filter string="Archivados" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="Requiere Nueva Muestra" name="requires_new" domain="[('requires_new_sample', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="Severidad Alta/Crítica" name="high_severity" domain="[('severity', 'in', ['high', 'critical'])]"/>
|
||||
<group expand="0" string="Agrupar por">
|
||||
<filter string="Severidad" name="group_severity" context="{'group_by': 'severity'}"/>
|
||||
<filter string="Requiere Nueva Muestra" name="group_requires_new" context="{'group_by': 'requires_new_sample'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Rejection Reasons -->
|
||||
<record id="action_lims_rejection_reason" model="ir.actions.act_window">
|
||||
<field name="name">Motivos de Rechazo</field>
|
||||
<field name="res_model">lims.rejection.reason</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_lims_rejection_reason_search"/>
|
||||
<field name="context">{'search_default_active': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Configure los motivos de rechazo de muestras
|
||||
</p>
|
||||
<p>
|
||||
Los motivos de rechazo permiten categorizar y documentar
|
||||
las razones por las cuales una muestra no puede ser procesada.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -15,7 +15,7 @@
|
|||
<field name="collection_date" string="Fecha de Recolección"/>
|
||||
<field name="collector_id" string="Recolectado por"/>
|
||||
<field name="container_type" optional="hide" string="Tipo Contenedor (Obsoleto)"/>
|
||||
<field name="state" string="Estado" decoration-success="state == 'analyzed'" decoration-info="state == 'in_process'" decoration-muted="state == 'stored' or state == 'disposed'" widget="badge"/>
|
||||
<field name="state" string="Estado" decoration-success="state == 'analyzed'" decoration-info="state == 'in_process'" decoration-danger="state == 'rejected'" decoration-muted="state == 'stored' or state == 'disposed' or state == 'cancelled'" widget="badge"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -33,7 +33,13 @@
|
|||
<button name="action_complete_analysis" string="Completar Análisis" type="object" class="oe_highlight" invisible="state != 'in_process'"/>
|
||||
<button name="action_store" string="Almacenar" type="object" invisible="state not in ['analyzed', 'in_process', 'received']"/>
|
||||
<button name="action_dispose" string="Desechar" type="object" invisible="state == 'disposed'"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="pending_collection,collected,received,in_process,analyzed,stored"/>
|
||||
<button name="action_open_rejection_wizard"
|
||||
string="Rechazar Muestra"
|
||||
type="object"
|
||||
class="btn-danger"
|
||||
invisible="state in ['completed', 'rejected', 'disposed', 'cancelled']"/>
|
||||
<button name="action_cancel" string="Cancelar" type="object" invisible="state in ['cancelled', 'rejected', 'disposed']"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="pending_collection,collected,received,in_process,analyzed,stored,rejected"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
|
@ -69,10 +75,65 @@
|
|||
invisible="sample_type_product_id != False"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Información de Rechazo" invisible="state != 'rejected'" col="4">
|
||||
<field name="rejection_reason_id" readonly="1"/>
|
||||
<field name="rejected_by" readonly="1"/>
|
||||
<field name="rejection_date" readonly="1"/>
|
||||
<field name="rejection_notes" readonly="1" colspan="4"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View for Lab Samples -->
|
||||
<record id="view_lab_sample_search" model="ir.ui.view">
|
||||
<field name="name">lab.sample.search</field>
|
||||
<field name="model">stock.lot</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Muestras">
|
||||
<field name="name" string="Código"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="barcode"/>
|
||||
<field name="analysis_names"/>
|
||||
<filter string="Pendientes" name="pending" domain="[('state', 'in', ['pending_collection', 'collected', 'received'])]"/>
|
||||
<filter string="En Proceso" name="in_process" domain="[('state', '=', 'in_process')]"/>
|
||||
<filter string="Analizadas" name="analyzed" domain="[('state', '=', 'analyzed')]"/>
|
||||
<filter string="Rechazadas" name="rejected" domain="[('state', '=', 'rejected')]"/>
|
||||
<separator/>
|
||||
<filter string="Hoy" name="today" domain="[('collection_date', '>=', datetime.datetime.now().strftime('%Y-%m-%d 00:00:00')), ('collection_date', '<=', datetime.datetime.now().strftime('%Y-%m-%d 23:59:59'))]"/>
|
||||
<filter string="Esta Semana" name="this_week" domain="[('collection_date', '>=', (datetime.datetime.now() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="Rechazadas - Alta Severidad" name="rejected_high"
|
||||
domain="[('state', '=', 'rejected'), ('rejection_reason_id.severity', 'in', ['high', 'critical'])]"/>
|
||||
<group expand="0" string="Agrupar por">
|
||||
<filter string="Estado" name="group_state" context="{'group_by': 'state'}"/>
|
||||
<filter string="Paciente" name="group_patient" context="{'group_by': 'patient_id'}"/>
|
||||
<filter string="Fecha de Recolección" name="group_collection" context="{'group_by': 'collection_date:day'}"/>
|
||||
<filter string="Motivo de Rechazo" name="group_rejection" context="{'group_by': 'rejection_reason_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Rejected Samples -->
|
||||
<record id="action_lab_sample_rejected" model="ir.actions.act_window">
|
||||
<field name="name">Muestras Rechazadas</field>
|
||||
<field name="res_model">stock.lot</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('is_lab_sample', '=', True), ('state', '=', 'rejected')]</field>
|
||||
<field name="context">{'search_default_rejected': 1, 'default_is_lab_sample': True}</field>
|
||||
<field name="search_view_id" ref="view_lab_sample_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No hay muestras rechazadas
|
||||
</p>
|
||||
<p>
|
||||
Las muestras rechazadas aparecerán aquí con información
|
||||
sobre el motivo del rechazo y las acciones tomadas.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
2
lims_management/wizards/__init__.py
Normal file
2
lims_management/wizards/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import sample_rejection_wizard
|
94
lims_management/wizards/sample_rejection_wizard.py
Normal file
94
lims_management/wizards/sample_rejection_wizard.py
Normal file
|
@ -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
|
45
lims_management/wizards/sample_rejection_wizard_views.xml
Normal file
45
lims_management/wizards/sample_rejection_wizard_views.xml
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form View for Sample Rejection Wizard -->
|
||||
<record id="view_lims_sample_rejection_wizard_form" model="ir.ui.view">
|
||||
<field name="name">lims.sample.rejection.wizard.form</field>
|
||||
<field name="model">lims.sample.rejection.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Rechazar Muestra">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="sample_id" options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="rejection_reason_id" options="{'no_create': True}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="requires_new_sample" invisible="1"/>
|
||||
<field name="create_new_sample"
|
||||
invisible="not requires_new_sample"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Información Adicional">
|
||||
<field name="rejection_notes" placeholder="Agregue cualquier información relevante sobre el rechazo..."/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_reject_sample"
|
||||
string="Rechazar Muestra"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
confirm="¿Está seguro de rechazar esta muestra? Esta acción no se puede deshacer."/>
|
||||
<button string="Cancelar" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Sample Rejection Wizard -->
|
||||
<record id="action_lims_sample_rejection_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Rechazar Muestra</field>
|
||||
<field name="res_model">lims.sample.rejection.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="context">{'default_sample_id': active_id}</field>
|
||||
</record>
|
||||
</odoo>
|
32
pr_body_10.txt
Normal file
32
pr_body_10.txt
Normal file
|
@ -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
|
63
test/check_demo_users.py
Normal file
63
test/check_demo_users.py
Normal file
|
@ -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)
|
Loading…
Reference in New Issue
Block a user