Merge pull request 'feat(#60): Automatización configurable de re-muestreo y reorganización de estados' (#62) from feature/60-user-assignment-improvements into dev
This commit is contained in:
commit
fc2275f809
BIN
documents/logs/Screenshot_4.png
Normal file
BIN
documents/logs/Screenshot_4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
documents/logs/Screenshot_5.png
Normal file
BIN
documents/logs/Screenshot_5.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
33
init_odoo.py
33
init_odoo.py
|
@ -189,6 +189,39 @@ EOF
|
||||||
else:
|
else:
|
||||||
print(f"Advertencia: Fallo al actualizar logo de empresa (código {result.returncode})")
|
print(f"Advertencia: Fallo al actualizar logo de empresa (código {result.returncode})")
|
||||||
|
|
||||||
|
# --- Asignar admin al grupo de Administrador de Laboratorio ---
|
||||||
|
print("\nAsignando usuario admin al grupo de Administrador de Laboratorio...")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
if os.path.exists("/app/scripts/assign_admin_to_lab_group.py"):
|
||||||
|
with open("/app/scripts/assign_admin_to_lab_group.py", "r") as f:
|
||||||
|
admin_group_script = f.read()
|
||||||
|
|
||||||
|
assign_admin_command = f"""
|
||||||
|
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
|
||||||
|
{admin_group_script}
|
||||||
|
EOF
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
assign_admin_command,
|
||||||
|
shell=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False
|
||||||
|
)
|
||||||
|
|
||||||
|
print("--- Assign Admin to Lab Group stdout ---")
|
||||||
|
print(result.stdout)
|
||||||
|
print("--- Assign Admin to Lab Group stderr ---")
|
||||||
|
print(result.stderr)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("Usuario admin asignado exitosamente al grupo de Administrador de Laboratorio.")
|
||||||
|
else:
|
||||||
|
print(f"Advertencia: Fallo al asignar admin al grupo (código {result.returncode})")
|
||||||
|
|
||||||
# --- Validación final del logo ---
|
# --- Validación final del logo ---
|
||||||
print("\nValidando estado final del logo y nombre...")
|
print("\nValidando estado final del logo y nombre...")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
'views/product_template_parameter_config_views.xml',
|
'views/product_template_parameter_config_views.xml',
|
||||||
'views/parameter_dashboard_views.xml',
|
'views/parameter_dashboard_views.xml',
|
||||||
'views/menus.xml',
|
'views/menus.xml',
|
||||||
|
'views/lims_config_views.xml',
|
||||||
'report/sample_label_report.xml',
|
'report/sample_label_report.xml',
|
||||||
],
|
],
|
||||||
'demo': [
|
'demo': [
|
||||||
|
|
Binary file not shown.
|
@ -10,3 +10,4 @@ from . import rejection_reason
|
||||||
from . import lims_test
|
from . import lims_test
|
||||||
from . import lims_result
|
from . import lims_result
|
||||||
from . import res_config_settings
|
from . import res_config_settings
|
||||||
|
from . import lims_config
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
44
lims_management/models/lims_config.py
Normal file
44
lims_management/models/lims_config.py
Normal file
|
@ -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
|
||||||
|
)
|
|
@ -33,6 +33,31 @@ class SaleOrder(models.Model):
|
||||||
help="Muestras de laboratorio generadas automáticamente cuando se confirmó esta orden"
|
help="Muestras de laboratorio generadas automáticamente cuando se confirmó esta orden"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
all_sample_ids = fields.Many2many(
|
||||||
|
'stock.lot',
|
||||||
|
string='Todas las Muestras (inc. Re-muestras)',
|
||||||
|
compute='_compute_all_samples',
|
||||||
|
help="Todas las muestras relacionadas con esta orden, incluyendo re-muestras"
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('generated_sample_ids', 'generated_sample_ids.child_sample_ids')
|
||||||
|
def _compute_all_samples(self):
|
||||||
|
"""Compute all samples including resamples"""
|
||||||
|
for order in self:
|
||||||
|
all_samples = order.generated_sample_ids
|
||||||
|
# Add all resamples recursively
|
||||||
|
resamples = self.env['stock.lot']
|
||||||
|
for sample in order.generated_sample_ids:
|
||||||
|
resamples |= self._get_all_resamples(sample)
|
||||||
|
order.all_sample_ids = all_samples | resamples
|
||||||
|
|
||||||
|
def _get_all_resamples(self, sample):
|
||||||
|
"""Recursively get all resamples of a sample"""
|
||||||
|
resamples = sample.child_sample_ids
|
||||||
|
for resample in sample.child_sample_ids:
|
||||||
|
resamples |= self._get_all_resamples(resample)
|
||||||
|
return resamples
|
||||||
|
|
||||||
def action_confirm(self):
|
def action_confirm(self):
|
||||||
"""Override to generate laboratory samples and tests automatically"""
|
"""Override to generate laboratory samples and tests automatically"""
|
||||||
res = super(SaleOrder, self).action_confirm()
|
res = super(SaleOrder, self).action_confirm()
|
||||||
|
@ -295,17 +320,22 @@ class SaleOrder(models.Model):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def action_print_sample_labels(self):
|
def action_print_sample_labels(self):
|
||||||
"""Imprimir etiquetas de todas las muestras generadas para esta orden"""
|
"""Imprimir etiquetas de todas las muestras activas (incluyendo re-muestras)"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
if not self.generated_sample_ids:
|
# Obtener todas las muestras activas (no rechazadas ni canceladas)
|
||||||
raise UserError(_('No hay muestras generadas para esta orden. Por favor, confirme la orden primero.'))
|
active_samples = self.all_sample_ids.filtered(
|
||||||
|
lambda s: s.state not in ['rejected', 'cancelled', 'disposed']
|
||||||
|
)
|
||||||
|
|
||||||
|
if not active_samples:
|
||||||
|
raise UserError(_('No hay muestras activas para imprimir. Todas las muestras están rechazadas, canceladas o desechadas.'))
|
||||||
|
|
||||||
# Asegurar que todas las muestras tengan código de barras
|
# Asegurar que todas las muestras tengan código de barras
|
||||||
self.generated_sample_ids._ensure_barcode()
|
active_samples._ensure_barcode()
|
||||||
|
|
||||||
# Obtener el reporte
|
# Obtener el reporte
|
||||||
report = self.env.ref('lims_management.action_report_sample_label')
|
report = self.env.ref('lims_management.action_report_sample_label')
|
||||||
|
|
||||||
# Retornar la acción de imprimir el reporte para todas las muestras
|
# Retornar la acción de imprimir el reporte para las muestras activas
|
||||||
return report.report_action(self.generated_sample_ids)
|
return report.report_action(active_samples)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from odoo import models, fields, api
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
@ -105,6 +106,43 @@ class StockLot(models.Model):
|
||||||
string='Fecha de Rechazo',
|
string='Fecha de Rechazo',
|
||||||
readonly=True
|
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
|
||||||
|
)
|
||||||
|
root_sample_id = fields.Many2one(
|
||||||
|
'stock.lot',
|
||||||
|
string='Muestra Original (Raíz)',
|
||||||
|
compute='_compute_root_sample',
|
||||||
|
store=True,
|
||||||
|
help='Muestra original de la cadena de re-muestreos'
|
||||||
|
)
|
||||||
|
resample_chain_count = fields.Integer(
|
||||||
|
string='Re-muestreos en Cadena',
|
||||||
|
compute='_compute_resample_chain_count',
|
||||||
|
help='Número total de re-muestreos en toda la cadena'
|
||||||
|
)
|
||||||
|
|
||||||
def action_collect(self):
|
def action_collect(self):
|
||||||
"""Mark sample as collected"""
|
"""Mark sample as collected"""
|
||||||
|
@ -190,8 +228,12 @@ class StockLot(models.Model):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def action_reject(self):
|
def action_reject(self, create_resample=None):
|
||||||
"""Reject the sample - to be called from wizard"""
|
"""Reject the sample - to be called from wizard
|
||||||
|
|
||||||
|
Args:
|
||||||
|
create_resample: Boolean to force resample creation. If None, uses system config
|
||||||
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
if self.state == 'completed':
|
if self.state == 'completed':
|
||||||
raise ValueError('No se puede rechazar una muestra ya completada')
|
raise ValueError('No se puede rechazar una muestra ya completada')
|
||||||
|
@ -223,6 +265,35 @@ class StockLot(models.Model):
|
||||||
subject='Muestra Rechazada',
|
subject='Muestra Rechazada',
|
||||||
message_type='notification'
|
message_type='notification'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Determine if we should create a resample
|
||||||
|
should_create_resample = False
|
||||||
|
|
||||||
|
if create_resample is not None:
|
||||||
|
# Explicit value from wizard
|
||||||
|
should_create_resample = create_resample
|
||||||
|
else:
|
||||||
|
# Check system configuration
|
||||||
|
IrConfig = self.env['ir.config_parameter'].sudo()
|
||||||
|
auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True'
|
||||||
|
should_create_resample = auto_resample
|
||||||
|
|
||||||
|
if should_create_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')
|
@api.onchange('sample_type_product_id')
|
||||||
def _onchange_sample_type_product_id(self):
|
def _onchange_sample_type_product_id(self):
|
||||||
|
@ -348,3 +419,177 @@ class StockLot(models.Model):
|
||||||
if record.is_lab_sample and not record.barcode:
|
if record.is_lab_sample and not record.barcode:
|
||||||
record.barcode = record._generate_unique_barcode()
|
record.barcode = record._generate_unique_barcode()
|
||||||
return True
|
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)
|
||||||
|
|
||||||
|
@api.depends('parent_sample_id')
|
||||||
|
def _compute_root_sample(self):
|
||||||
|
"""Compute the root sample of the resample chain"""
|
||||||
|
for record in self:
|
||||||
|
root = record
|
||||||
|
while root.parent_sample_id:
|
||||||
|
root = root.parent_sample_id
|
||||||
|
record.root_sample_id = root if root != record else False
|
||||||
|
|
||||||
|
@api.depends('parent_sample_id', 'child_sample_ids')
|
||||||
|
def _compute_resample_chain_count(self):
|
||||||
|
"""Compute total resamples in the entire chain"""
|
||||||
|
for record in self:
|
||||||
|
# Find root sample
|
||||||
|
root = record
|
||||||
|
while root.parent_sample_id:
|
||||||
|
root = root.parent_sample_id
|
||||||
|
# Count all resamples from root
|
||||||
|
record.resample_chain_count = self._count_all_resamples_in_chain(root)
|
||||||
|
|
||||||
|
def action_create_resample(self):
|
||||||
|
"""Create a new sample as a resample of the current one"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
# Determine the parent sample for the new resample
|
||||||
|
# If current sample is already a resample, use its parent
|
||||||
|
# Otherwise, use the current sample as parent
|
||||||
|
parent_for_resample = self.parent_sample_id if self.parent_sample_id else self
|
||||||
|
|
||||||
|
# Check if there's already an active resample for the parent
|
||||||
|
active_resamples = parent_for_resample.child_sample_ids.filtered(
|
||||||
|
lambda s: s.state not in ['rejected', 'cancelled', 'disposed']
|
||||||
|
)
|
||||||
|
if active_resamples:
|
||||||
|
raise UserError(_('La muestra %s ya tiene una re-muestra activa (%s). No se puede crear otra hasta que se procese o rechace la existente.') %
|
||||||
|
(parent_for_resample.name, ', '.join(active_resamples.mapped('name'))))
|
||||||
|
|
||||||
|
# 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'))
|
||||||
|
|
||||||
|
# Find the original sample (root of the resample chain)
|
||||||
|
original_sample = parent_for_resample
|
||||||
|
while original_sample.parent_sample_id:
|
||||||
|
original_sample = original_sample.parent_sample_id
|
||||||
|
|
||||||
|
# Count all resamples in the chain
|
||||||
|
total_resamples = self._count_all_resamples_in_chain(original_sample)
|
||||||
|
|
||||||
|
# Check maximum resample attempts based on the entire chain
|
||||||
|
if max_attempts > 0 and total_resamples >= max_attempts:
|
||||||
|
raise UserError(_('Se ha alcanzado el número máximo de re-muestreos (%d) para esta cadena de muestras.') % max_attempts)
|
||||||
|
|
||||||
|
# Calculate resample number for naming (based on parent's resample count)
|
||||||
|
resample_number = len(parent_for_resample.child_sample_ids) + 1
|
||||||
|
|
||||||
|
# Prepare values for new sample
|
||||||
|
vals = {
|
||||||
|
'name': f"{prefix}{parent_for_resample.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': parent_for_resample.id, # Always use the determined parent
|
||||||
|
'request_id': self.request_id.id if self.request_id else False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create the resample
|
||||||
|
resample = self.create(vals)
|
||||||
|
|
||||||
|
# Post message in all relevant samples
|
||||||
|
self.message_post(
|
||||||
|
body=_('Re-muestra creada: %s') % resample.name,
|
||||||
|
subject='Re-muestreo',
|
||||||
|
message_type='notification'
|
||||||
|
)
|
||||||
|
|
||||||
|
if self != parent_for_resample:
|
||||||
|
# If we're creating from a resample, also notify the parent
|
||||||
|
parent_for_resample.message_post(
|
||||||
|
body=_('Nueva re-muestra creada: %s (debido al rechazo de %s)') % (resample.name, self.name),
|
||||||
|
subject='Re-muestreo',
|
||||||
|
message_type='notification'
|
||||||
|
)
|
||||||
|
|
||||||
|
resample.message_post(
|
||||||
|
body=_('Esta es una re-muestra de: %s<br/>Creada debido al rechazo de: %s<br/>Motivo: %s') %
|
||||||
|
(parent_for_resample.name, 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 _count_all_resamples_in_chain(self, root_sample):
|
||||||
|
"""Count all resamples in the entire chain starting from root"""
|
||||||
|
count = 0
|
||||||
|
samples_to_check = [root_sample]
|
||||||
|
|
||||||
|
while samples_to_check:
|
||||||
|
sample = samples_to_check.pop(0)
|
||||||
|
# Add all child samples to the check list
|
||||||
|
for child in sample.child_sample_ids:
|
||||||
|
count += 1
|
||||||
|
samples_to_check.append(child)
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Get the model id for stock.lot
|
||||||
|
model_id = self.env['ir.model'].search([('model', '=', 'stock.lot')], limit=1).id
|
||||||
|
|
||||||
|
# Create activities for receptionists
|
||||||
|
for user in receptionist_users:
|
||||||
|
self.env['mail.activity'].create({
|
||||||
|
'res_model': 'stock.lot',
|
||||||
|
'res_model_id': model_id, # Campo obligatorio
|
||||||
|
'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(),
|
||||||
|
})
|
||||||
|
|
|
@ -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_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_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_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
|
||||||
|
|
|
55
lims_management/views/lims_config_views.xml
Normal file
55
lims_management/views/lims_config_views.xml
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Laboratory Configuration Form View -->
|
||||||
|
<record id="view_lims_config_settings_form" model="ir.ui.view">
|
||||||
|
<field name="name">lims.config.settings.form</field>
|
||||||
|
<field name="model">lims.config.settings</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Configuración del Laboratorio">
|
||||||
|
<header>
|
||||||
|
<button string="Guardar" type="object" name="execute" class="oe_highlight"/>
|
||||||
|
<button string="Cancelar" special="cancel"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="o_form_label">Configuración de Re-muestreo</div>
|
||||||
|
<group>
|
||||||
|
<group name="resample_settings" string="Re-muestreo Automático">
|
||||||
|
<field name="auto_resample_on_rejection"/>
|
||||||
|
<field name="resample_state" invisible="not auto_resample_on_rejection"/>
|
||||||
|
<field name="resample_prefix" invisible="not auto_resample_on_rejection"/>
|
||||||
|
<field name="max_resample_attempts" invisible="not auto_resample_on_rejection"/>
|
||||||
|
</group>
|
||||||
|
<group name="notification_settings" string="Notificaciones">
|
||||||
|
<field name="auto_notify_resample" invisible="not auto_resample_on_rejection"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Información">
|
||||||
|
<div class="text-muted">
|
||||||
|
<p>El re-muestreo automático permite generar una nueva muestra cuando se rechaza una existente.</p>
|
||||||
|
<p>Las notificaciones se enviarán a todos los usuarios con rol de Recepcionista.</p>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action to open laboratory configuration -->
|
||||||
|
<record id="action_lims_config_settings" model="ir.actions.act_window">
|
||||||
|
<field name="name">Configuración del Laboratorio</field>
|
||||||
|
<field name="res_model">lims.config.settings</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">inline</field>
|
||||||
|
<field name="context">{'dialog_size': 'medium'}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu for Laboratory Configuration -->
|
||||||
|
<menuitem id="menu_lims_lab_config"
|
||||||
|
name="Configuración del Laboratorio"
|
||||||
|
parent="lims_management.lims_menu_config"
|
||||||
|
action="action_lims_config_settings"
|
||||||
|
sequence="60"
|
||||||
|
groups="lims_management.group_lims_admin"/>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
|
@ -14,7 +14,7 @@
|
||||||
string="Imprimir Etiquetas"
|
string="Imprimir Etiquetas"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
invisible="not is_lab_request or state != 'sale' or not generated_sample_ids"
|
invisible="not is_lab_request or state != 'sale' or not all_sample_ids"
|
||||||
icon="fa-print"/>
|
icon="fa-print"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='partner_id']" position="after">
|
<xpath expr="//field[@name='partner_id']" position="after">
|
||||||
|
@ -29,26 +29,42 @@
|
||||||
</xpath>
|
</xpath>
|
||||||
<!-- Add Generated Samples tab -->
|
<!-- Add Generated Samples tab -->
|
||||||
<xpath expr="//notebook" position="inside">
|
<xpath expr="//notebook" position="inside">
|
||||||
<page string="Muestras Generadas" name="generated_samples" invisible="not is_lab_request">
|
<page string="Muestras" name="all_samples" invisible="not is_lab_request">
|
||||||
<group>
|
<group string="Todas las Muestras (incluyendo Re-muestras)">
|
||||||
<field name="generated_sample_ids" nolabel="1" readonly="1">
|
<field name="all_sample_ids" nolabel="1" readonly="1">
|
||||||
<list string="Muestras Generadas" create="false" edit="false" delete="false">
|
<list string="Todas las Muestras" create="false" edit="false" delete="false">
|
||||||
<field name="name" string="Código de Muestra"/>
|
<field name="name" string="Código de Muestra"/>
|
||||||
<field name="barcode" string="Código de Barras"/>
|
<field name="barcode" string="Código de Barras" optional="show"/>
|
||||||
<field name="sample_type_product_id" string="Tipo de Muestra"/>
|
<field name="sample_type_product_id" string="Tipo de Muestra"/>
|
||||||
<field name="volume_ml" string="Volumen (ml)"/>
|
<field name="volume_ml" string="Volumen (ml)" optional="show"/>
|
||||||
<field name="analysis_names" string="Análisis"/>
|
<field name="analysis_names" string="Análisis" optional="show"/>
|
||||||
<field name="state" string="Estado"/>
|
<field name="is_resample" string="Es Re-muestra" widget="boolean_toggle"/>
|
||||||
|
<field name="parent_sample_id" string="Muestra Original" optional="show"/>
|
||||||
|
<field name="state" string="Estado" widget="badge"
|
||||||
|
decoration-success="state == 'analyzed'"
|
||||||
|
decoration-info="state == 'in_process'"
|
||||||
|
decoration-danger="state == 'rejected'"
|
||||||
|
decoration-warning="state == 'pending_collection'"/>
|
||||||
|
<field name="rejection_reason_id" string="Motivo Rechazo" optional="show"/>
|
||||||
<button name="action_collect" string="Recolectar" type="object"
|
<button name="action_collect" string="Recolectar" type="object"
|
||||||
class="btn-primary" invisible="state != 'pending_collection'"/>
|
class="btn-sm btn-primary" invisible="state != 'pending_collection'"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</group>
|
</group>
|
||||||
<group invisible="not generated_sample_ids">
|
<group string="Resumen" col="4">
|
||||||
<div class="alert alert-info" role="alert">
|
<field name="generated_sample_ids" invisible="1"/>
|
||||||
<p>Las muestras han sido generadas automáticamente basándose en los análisis solicitados.
|
<group>
|
||||||
Cada muestra agrupa los análisis que requieren el mismo tipo de contenedor.</p>
|
<label for="generated_sample_ids" string="Muestras Originales:"/>
|
||||||
</div>
|
<div>
|
||||||
|
<span class="badge badge-primary"><field name="generated_sample_ids" readonly="1" widget="many2many_tags"/></span>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<p><i class="fa fa-info-circle"/> Las muestras han sido generadas automáticamente basándose en los análisis solicitados.</p>
|
||||||
|
<p>Las re-muestras se generan cuando una muestra es rechazada.</p>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
<field name="collector_id" string="Recolectado por"/>
|
<field name="collector_id" string="Recolectado por"/>
|
||||||
<field name="container_type" optional="hide" string="Tipo Contenedor (Obsoleto)"/>
|
<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-danger="state == 'rejected'" decoration-muted="state == 'stored' or state == 'disposed' or state == 'cancelled'" 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"/>
|
||||||
|
<field name="is_resample" string="Re-muestra" widget="boolean_toggle" optional="show"/>
|
||||||
|
<field name="resample_count" string="Re-muestreos" optional="show"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
@ -39,6 +41,12 @@
|
||||||
class="btn-danger"
|
class="btn-danger"
|
||||||
invisible="state in ['completed', 'rejected', 'disposed', 'cancelled']"/>
|
invisible="state in ['completed', 'rejected', 'disposed', 'cancelled']"/>
|
||||||
<button name="action_cancel" string="Cancelar" type="object" invisible="state in ['cancelled', 'rejected', 'disposed']"/>
|
<button name="action_cancel" string="Cancelar" type="object" invisible="state in ['cancelled', 'rejected', 'disposed']"/>
|
||||||
|
<button name="action_create_resample"
|
||||||
|
string="Crear Re-muestra"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="state != 'rejected' or resample_count >= 3"
|
||||||
|
confirm="¿Está seguro de que desea crear una re-muestra para esta muestra rechazada?"/>
|
||||||
<field name="state" widget="statusbar" statusbar_visible="pending_collection,collected,received,in_process,analyzed,stored,rejected"/>
|
<field name="state" widget="statusbar" statusbar_visible="pending_collection,collected,received,in_process,analyzed,stored,rejected"/>
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
|
@ -81,6 +89,34 @@
|
||||||
<field name="rejection_date" readonly="1"/>
|
<field name="rejection_date" readonly="1"/>
|
||||||
<field name="rejection_notes" readonly="1" colspan="4"/>
|
<field name="rejection_notes" readonly="1" colspan="4"/>
|
||||||
</group>
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Re-muestreo" invisible="not is_resample and resample_count == 0">
|
||||||
|
<group col="4">
|
||||||
|
<field name="is_resample" invisible="1"/>
|
||||||
|
<field name="resample_count" invisible="1"/>
|
||||||
|
<field name="parent_sample_id" readonly="1" invisible="not is_resample"/>
|
||||||
|
<field name="root_sample_id" readonly="1" invisible="not is_resample"/>
|
||||||
|
<field name="resample_chain_count" readonly="1" invisible="resample_chain_count == 0"/>
|
||||||
|
</group>
|
||||||
|
<group string="Re-muestras Generadas" invisible="resample_count == 0">
|
||||||
|
<field name="child_sample_ids" nolabel="1">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="state" widget="badge"/>
|
||||||
|
<field name="collection_date"/>
|
||||||
|
<field name="rejection_reason_id"/>
|
||||||
|
<field name="resample_count" string="Re-muestras propias"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</group>
|
||||||
|
<group string="Información de Trazabilidad" invisible="not is_resample">
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<p><i class="fa fa-info-circle"/> Esta muestra es parte de una cadena de re-muestreo.</p>
|
||||||
|
<p>Total de re-muestreos en la cadena: <field name="resample_chain_count" readonly="1" nolabel="1" class="oe_inline"/></p>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
|
@ -100,6 +136,8 @@
|
||||||
<filter string="En Proceso" name="in_process" domain="[('state', '=', 'in_process')]"/>
|
<filter string="En Proceso" name="in_process" domain="[('state', '=', 'in_process')]"/>
|
||||||
<filter string="Analizadas" name="analyzed" domain="[('state', '=', 'analyzed')]"/>
|
<filter string="Analizadas" name="analyzed" domain="[('state', '=', 'analyzed')]"/>
|
||||||
<filter string="Rechazadas" name="rejected" domain="[('state', '=', 'rejected')]"/>
|
<filter string="Rechazadas" name="rejected" domain="[('state', '=', 'rejected')]"/>
|
||||||
|
<filter string="Re-muestras" name="resamples" domain="[('is_resample', '=', True)]"/>
|
||||||
|
<filter string="Con Re-muestras" name="has_resamples" domain="[('resample_count', '>', 0)]"/>
|
||||||
<separator/>
|
<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="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'))]"/>
|
<filter string="Esta Semana" name="this_week" domain="[('collection_date', '>=', (datetime.datetime.now() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"/>
|
||||||
|
@ -111,6 +149,7 @@
|
||||||
<filter string="Paciente" name="group_patient" context="{'group_by': 'patient_id'}"/>
|
<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="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'}"/>
|
<filter string="Motivo de Rechazo" name="group_rejection" context="{'group_by': 'rejection_reason_id'}"/>
|
||||||
|
<filter string="Es Re-muestra" name="group_resample" context="{'group_by': 'is_resample'}"/>
|
||||||
</group>
|
</group>
|
||||||
</search>
|
</search>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
@ -67,12 +67,8 @@ class SampleRejectionWizard(models.TransientModel):
|
||||||
'rejection_notes': self.rejection_notes
|
'rejection_notes': self.rejection_notes
|
||||||
})
|
})
|
||||||
|
|
||||||
# Call the rejection method on the sample
|
# Call the rejection method on the sample with explicit resample creation preference
|
||||||
self.sample_id.action_reject()
|
self.sample_id.action_reject(create_resample=self.create_new_sample)
|
||||||
|
|
||||||
# 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'}
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|
||||||
|
|
46
scripts/assign_admin_to_lab_group.py
Normal file
46
scripts/assign_admin_to_lab_group.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Script para asignar el usuario admin al grupo de Administrador de Laboratorio
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configurar logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Buscar el usuario admin
|
||||||
|
admin_user = env['res.users'].search([('login', '=', 'admin')], limit=1)
|
||||||
|
if not admin_user:
|
||||||
|
_logger.error("No se encontró el usuario admin")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Buscar el grupo de Administrador de Laboratorio
|
||||||
|
try:
|
||||||
|
lab_admin_group = env.ref('lims_management.group_lims_admin')
|
||||||
|
except ValueError:
|
||||||
|
_logger.error("No se encontró el grupo de Administrador de Laboratorio")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Verificar si el usuario ya está en el grupo
|
||||||
|
if lab_admin_group in admin_user.groups_id:
|
||||||
|
_logger.info("El usuario admin ya está en el grupo de Administrador de Laboratorio")
|
||||||
|
else:
|
||||||
|
# Agregar el usuario al grupo
|
||||||
|
admin_user.write({
|
||||||
|
'groups_id': [(4, lab_admin_group.id)]
|
||||||
|
})
|
||||||
|
_logger.info("Usuario admin agregado exitosamente al grupo de Administrador de Laboratorio")
|
||||||
|
|
||||||
|
# Confirmar los grupos del usuario
|
||||||
|
group_names = ', '.join(admin_user.groups_id.mapped('name'))
|
||||||
|
_logger.info(f"Grupos del usuario admin: {group_names}")
|
||||||
|
|
||||||
|
env.cr.commit()
|
||||||
|
_logger.info("Cambios guardados exitosamente")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Error al asignar usuario admin al grupo: {str(e)}")
|
||||||
|
exit(1)
|
Loading…
Reference in New Issue
Block a user