kalenislims/lims_quality_control/lims.py

650 lines
26 KiB
Python
Raw Normal View History

2020-04-11 23:19:16 +02:00
# This file is part of lims_quality_control module for Tryton.
# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.
from datetime import datetime
from trytond.model import fields
from trytond.pyson import Eval, Equal
from trytond.transaction import Transaction
from trytond.pool import Pool, PoolMeta
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.modules.lims_interface.interface import FUNCTIONS
from .function import custom_functions
2020-04-11 23:19:16 +02:00
__all__ = ['Configuration', 'Method', 'Analysis', 'Typification',
'NotebookLine', 'EntryDetailAnalysis', 'ResultReport',
'NotebookLoadResultsManualLine', 'NotebookLoadResultsManual']
_PROOF_TYPES = [
('qualitative', 'Qualitative'),
('quantitative', 'Quantitative')
]
FUNCTIONS.update(custom_functions)
2020-04-11 23:19:16 +02:00
class Configuration(metaclass=PoolMeta):
__name__ = 'lims.configuration'
qc_fraction_type = fields.Many2One('lims.fraction.type',
'QC fraction type')
class Method(metaclass=PoolMeta):
__name__ = 'lims.lab.method'
specification = fields.Text('Specification')
class Analysis(metaclass=PoolMeta):
__name__ = 'lims.analysis'
quality_type = fields.Selection(_PROOF_TYPES, 'Quality Type',
required=True)
quality_uom = fields.Many2One('product.uom', 'Quality UoM',
domain=[('category.lims_only_available', '=', True)],
states={
'invisible': ~Equal(Eval('quality_type'), 'quantitative'),
'required': Equal(Eval('quality_type'), 'quantitative'),
}, depends=['quality_type'])
quality_possible_values = fields.One2Many('lims.quality.qualitative.value',
'analysis', 'Possible Values',
states={
'invisible': ~Equal(Eval('quality_type'), 'qualitative'),
'required': Equal(Eval('quality_type'), 'qualitative'),
}, depends=['quality_type'])
@staticmethod
def default_quality_type():
return 'quantitative'
class Typification(metaclass=PoolMeta):
__name__ = 'lims.typification'
_history = True
specification = fields.Text('Specification')
quality = fields.Boolean('Quality')
quality_template = fields.Many2One('lims.quality.template',
'Quality Template')
quality_type = fields.Function(fields.Selection(_PROOF_TYPES,
'Quality Type', states={'invisible': True}),
'on_change_with_quality_type')
valid_value = fields.Many2One('lims.quality.qualitative.value',
'Valid Value',
states={
'invisible': ~Equal(Eval('quality_type'), 'qualitative'),
'required': Equal(Eval('quality_type'), 'qualitative'),
},
domain=[('id', 'in', Eval('valid_value_domain'))],
depends=['valid_value_domain', 'quality_type'])
valid_value_domain = fields.Function(fields.Many2Many(
'lims.quality.qualitative.value',
None, None, 'Valid Value domain',
states={'invisible': True}), 'on_change_with_valid_value_domain')
quality_test_report = fields.Boolean('Quality Test Report')
quality_order = fields.Integer('Quality Order')
quality_min = fields.Float('Min',
digits=(16, Eval('limit_digits', 2)), depends=['limit_digits'])
quality_max = fields.Float('Max',
digits=(16, Eval('limit_digits', 2)), depends=['limit_digits'])
@classmethod
def __setup__(cls):
super(Typification, cls).__setup__()
cls._sql_constraints = []
@classmethod
def __register__(cls, module_name):
super(Typification, cls).__register__(module_name)
table = cls.__table_handler__(module_name)
table.drop_constraint('product_matrix_analysis_method_uniq')
@fields.depends('analysis')
def on_change_with_valid_value_domain(self, name=None):
values = []
if self.analysis and self.analysis.quality_possible_values:
for value in self.analysis.quality_possible_values:
values.append(value.id)
return values
@fields.depends('analysis')
def on_change_with_quality_type(self, name=None):
if self.analysis:
return self.analysis.quality_type
@fields.depends('analysis')
def on_change_analysis(self):
if self.analysis:
if self.analysis.quality_type == 'quantitative':
self.start_uom = self.analysis.quality_uom
self.end_uom = self.analysis.quality_uom
@fields.depends('method')
def on_change_method(self):
if self.method:
self.specification = self.method.specification
def check_default(self):
if self.quality:
return
if self.by_default:
typifications = self.search([
('product_type', '=', self.product_type.id),
('matrix', '=', self.matrix.id),
('analysis', '=', self.analysis.id),
('valid', '=', True),
('by_default', '=', True),
('id', '!=', self.id),
])
if typifications:
raise UserError(gettext('lims.msg_default_typification'))
else:
if self.valid:
typifications = self.search([
('product_type', '=', self.product_type.id),
('matrix', '=', self.matrix.id),
('analysis', '=', self.analysis.id),
('valid', '=', True),
('id', '!=', self.id),
])
if not typifications:
raise UserError(
gettext('lims.msg_not_default_typification'))
@classmethod
def create(cls, vlist):
Template = Pool().get('lims.quality.template')
vlist = [x.copy() for x in vlist]
for values in vlist:
if values.get('quality_template'):
template, = Template.browse([values.get('quality_template')])
values['product_type'] = template.product.product_type.id
values['matrix'] = template.product.matrix.id
return super(Typification, cls).create(vlist)
class NotebookLine(metaclass=PoolMeta):
__name__ = 'lims.notebook.line'
typification = fields.Many2One('lims.typification', 'Typification')
quality_test = fields.Many2One('lims.quality.test', 'Quality Test',
select=True)
test_value = fields.Many2One('lims.quality.qualitative.value',
'Test Value',
states={
'readonly': True,
})
qualitative_value = fields.Many2One('lims.quality.qualitative.value',
'Qualitative Value',
domain=[
('analysis', '=', Eval('analysis')),
], depends=['analysis'])
success = fields.Function(fields.Boolean('Success',
depends=['success_icon']), 'get_success')
success_icon = fields.Function(fields.Char('Success Icon',
depends=['success']), 'get_success_icon')
quality_min = fields.Float('Min',
digits=(16, Eval('decimals', 2)), depends=['decimals'])
quality_max = fields.Float('Max',
digits=(16, Eval('decimals', 2)), depends=['decimals'])
quality_test_report = fields.Boolean('Quality Test Report')
@staticmethod
def default_quality_test_report():
return True
@classmethod
def get_success(self, lines, name):
res = {}
for line in lines:
res[line.id] = False
if line.analysis.quality_type == 'quantitative':
if line.result:
value = float(line.result)
quality_min = line.quality_min
if not isinstance(quality_min, (int, float)):
quality_min = float('-inf')
quality_max = line.quality_max
if not isinstance(quality_max, (int, float)):
quality_max = float('inf')
if (value >= quality_min and value <= quality_max):
res[line.id] = True
else:
if line.qualitative_value == line.test_value:
res[line.id] = True
return res
def get_success_icon(self, name):
if not self.accepted:
return 'lims-white'
if self.success:
return 'lims-green'
return 'lims-red'
@fields.depends('qualitative_value')
def on_change_qualitative_value(self):
if self.qualitative_value:
self.literal_result = self.qualitative_value.name
@classmethod
def fields_view_get(cls, view_id=None, view_type='form'):
pool = Pool()
User = pool.get('res.user')
Config = pool.get('lims.configuration')
UiView = pool.get('ir.ui.view')
result = super(NotebookLine, cls).fields_view_get(view_id=view_id,
view_type=view_type)
# All Notebook Lines view
if view_id and UiView(view_id).name == 'notebook_line_all_list':
return result
notebook_view = User(Transaction().user).notebook_view
if not notebook_view:
notebook_view = Config(1).default_notebook_view
if not notebook_view:
return result
if view_type == 'tree':
xml = '<?xml version="1.0"?>\n' \
'<tree editable="bottom">\n'
fields = set()
for column in notebook_view.columns:
fields.add(column.field.name)
attrs = []
if column.field.name == 'analysis':
attrs.append('icon="icon"')
if column.field.name == 'success':
attrs.append('icon="success_icon"')
if column.field.name in ('acceptance_date', 'annulment_date'):
attrs.append('widget="date"')
xml += ('<field name="%s" %s/>\n'
% (column.field.name, ' '.join(attrs)))
for depend in getattr(cls, column.field.name).depends:
fields.add(depend)
for field in ('report_date', 'result', 'converted_result',
'result_modifier', 'converted_result_modifier',
'literal_result', 'backup', 'verification', 'uncertainty',
'accepted', 'acceptance_date', 'end_date', 'report',
'annulled', 'annulment_date', 'icon'):
fields.add(field)
xml += '</tree>'
result['arch'] = xml
result['fields'] = cls.fields_get(fields_names=list(fields))
return result
class EntryDetailAnalysis(metaclass=PoolMeta):
__name__ = 'lims.entry.detail.analysis'
@classmethod
def create_notebook_lines(cls, details, fraction):
cursor = Transaction().connection.cursor()
pool = Pool()
Typification = pool.get('lims.typification')
Method = pool.get('lims.lab.method')
WaitingTime = pool.get('lims.lab.method.results_waiting')
AnalysisLaboratory = pool.get('lims.analysis-laboratory')
ProductType = pool.get('lims.product.type')
2020-04-11 23:19:16 +02:00
Notebook = pool.get('lims.notebook')
Company = pool.get('company.company')
def _str_value(val=None):
return str(val) if val is not None else None
2020-04-11 23:19:16 +02:00
lines_create = []
template_id = None
if Transaction().context.get('template'):
template_id = Transaction().context.get('template')
for detail in details:
query = 'SELECT default_repetitions, ' \
'initial_concentration, final_concentration, start_uom, ' \
'end_uom, detection_limit, quantification_limit, ' \
'lower_limit, upper_limit, calc_decimals, report, id ' \
2020-04-11 23:19:16 +02:00
'FROM "' + Typification._table + '" ' \
'WHERE product_type = %s ' \
'AND matrix = %s ' \
'AND analysis = %s ' \
'AND method = %s ' \
'AND valid'
if template_id:
query += ' AND quality_template = %s'
cursor.execute(query,
(fraction.product_type.id, fraction.matrix.id,
detail.analysis.id, detail.method.id, template_id))
else:
cursor.execute(query,
(fraction.product_type.id, fraction.matrix.id,
detail.analysis.id, detail.method.id))
typifications = cursor.fetchall()
typification = (typifications[0] if len(typifications) == 1
else None)
if typification:
repetitions = typification[0]
initial_concentration = _str_value(typification[1])
final_concentration = _str_value(typification[2])
initial_unit = typification[3] or None
final_unit = typification[4] or None
detection_limit = _str_value(typification[5])
quantification_limit = _str_value(typification[6])
lower_limit = _str_value(typification[7])
upper_limit = _str_value(typification[8])
decimals = typification[9]
report = typification[10]
2020-04-11 23:19:16 +02:00
else:
repetitions = 0
initial_concentration = None
final_concentration = None
initial_unit = None
final_unit = None
detection_limit = None
quantification_limit = None
lower_limit = None
upper_limit = None
2020-04-11 23:19:16 +02:00
decimals = 2
report = False
results_estimated_waiting = None
2020-04-11 23:19:16 +02:00
cursor.execute('SELECT results_estimated_waiting '
'FROM "' + WaitingTime._table + '" '
'WHERE method = %s '
'AND party = %s',
(detail.method.id, detail.party.id))
res = cursor.fetchone()
if res:
results_estimated_waiting = res[0]
else:
cursor.execute('SELECT results_estimated_waiting '
'FROM "' + Method._table + '" '
'WHERE id = %s', (detail.method.id,))
res = cursor.fetchone()
if res:
results_estimated_waiting = res[0]
2020-04-11 23:19:16 +02:00
department = None
2020-04-11 23:19:16 +02:00
cursor.execute('SELECT department '
'FROM "' + AnalysisLaboratory._table + '" '
'WHERE analysis = %s '
'AND laboratory = %s',
(detail.analysis.id, detail.laboratory.id))
res = cursor.fetchone()
if res and res[0]:
department = res[0]
else:
cursor.execute('SELECT department '
'FROM "' + ProductType._table + '" '
'WHERE id = %s', (fraction.product_type.id,))
res = cursor.fetchone()
if res and res[0]:
department = res[0]
2020-04-11 23:19:16 +02:00
for i in range(0, repetitions + 1):
notebook_line = {
'analysis_detail': detail.id,
'service': detail.service.id,
'analysis': detail.analysis.id,
'analysis_origin': detail.analysis_origin,
'repetition': i,
'laboratory': detail.laboratory.id,
'method': detail.method.id,
'device': detail.device.id if detail.device else None,
'initial_concentration': initial_concentration,
'final_concentration': final_concentration,
'initial_unit': initial_unit,
'final_unit': final_unit,
'detection_limit': detection_limit,
'quantification_limit': quantification_limit,
'decimals': decimals,
'report': report,
'results_estimated_waiting': results_estimated_waiting,
'department': department,
}
if template_id:
quality_typification = Typification(typification[11])
2020-04-11 23:19:16 +02:00
notebook_line['typification'] = quality_typification.id
notebook_line['test_value'] = \
quality_typification.valid_value.id \
if quality_typification.valid_value else None
notebook_line['quality_test'] = Transaction().context.get(
'test')
notebook_line['quality_min'] = \
quality_typification.quality_min
notebook_line['quality_max'] = \
quality_typification.quality_max
notebook_line['quality_test_report'] = \
quality_typification.quality_test_report
lines_create.append(notebook_line)
if not lines_create:
companies = Company.search([])
if fraction.party.id not in [c.party.id for c in companies]:
raise UserError(gettext(
'lims.msg_not_services', fraction=fraction.rec_name))
with Transaction().set_user(0):
notebook = Notebook.search([
('fraction', '=', fraction.id),
])
Notebook.write(notebook, {
'lines': [('create', lines_create)],
})
class ResultReport(metaclass=PoolMeta):
__name__ = 'lims.result_report'
@classmethod
def get_reference(cls, range_type, notebook_line, language,
report_section):
res = super(ResultReport, cls).get_reference(
range_type, notebook_line, language, report_section)
if res:
return res
res = ''
if notebook_line.quality_min:
with Transaction().set_context(language=language):
resf = float(notebook_line.quality_min)
resd = abs(resf) - abs(int(resf))
if resd > 0:
res1 = str(round(notebook_line.quality_min, 2))
else:
res1 = str(int(notebook_line.quality_min))
res = gettext('lims.msg_caa_min', min=res1)
if notebook_line.quality_max:
if res:
res += ' - '
with Transaction().set_context(language=language):
resf = float(notebook_line.quality_max)
resd = abs(resf) - abs(int(resf))
if resd > 0:
res1 = str(round(notebook_line.quality_max, 2))
else:
res1 = str(int(notebook_line.quality_max))
res += gettext('lims.msg_caa_max', max=res1)
return res
class NotebookLoadResultsManualLine(metaclass=PoolMeta):
__name__ = 'lims.notebook.load_results_manual.line'
qualitative_value = fields.Many2One('lims.quality.qualitative.value',
'Qualitative Value',
domain=[
('analysis', '=', Eval('analysis')),
], depends=['analysis'])
analysis = fields.Function(fields.Many2One('lims.analysis', 'Analysis'),
'get_analysis')
def get_analysis(self, name):
return self.line.analysis.id if self.line else None
@fields.depends('result', 'literal_result', 'result_modifier', 'end_date',
'qualitative_value')
def on_change_with_end_date(self):
pool = Pool()
Date = pool.get('ir.date')
if self.end_date:
return self.end_date
if (self.result or self.literal_result or self.qualitative_value or
self.result_modifier not in ('eq', 'low')):
return Date.today()
return None
@fields.depends('qualitative_value')
def on_change_qualitative_value(self):
if self.qualitative_value:
self.literal_result = self.qualitative_value.name
class NotebookLoadResultsManual(metaclass=PoolMeta):
__name__ = 'lims.notebook.load_results_manual'
def transition_confirm_(self):
pool = Pool()
NotebookLoadResultsManualLine = pool.get(
'lims.notebook.load_results_manual.line')
NotebookLine = pool.get('lims.notebook.line')
LabProfessionalMethod = pool.get('lims.lab.professional.method')
LabProfessionalMethodRequalification = pool.get(
'lims.lab.professional.method.requalification')
Date = pool.get('ir.date')
# Write Results to Notebook lines
actions = NotebookLoadResultsManualLine.search([
('session_id', '=', self._session_id),
])
for data in actions:
notebook_line = NotebookLine(data.line.id)
if not notebook_line:
continue
notebook_line_write = {
'result': data.result,
'qualitative_value': data.qualitative_value,
'result_modifier': data.result_modifier,
'end_date': data.end_date,
'chromatogram': data.chromatogram,
'initial_unit': (data.initial_unit.id if
data.initial_unit else None),
'comments': data.comments,
'literal_result': data.literal_result,
'converted_result': None,
'converted_result_modifier': 'eq',
'backup': None,
'verification': None,
'uncertainty': None,
}
if data.result_modifier == 'na':
notebook_line_write['annulled'] = True
notebook_line_write['annulment_date'] = datetime.now()
notebook_line_write['report'] = False
professionals = [{'professional': self.result.professional.id}]
notebook_line_write['professionals'] = (
[('delete', [p.id for p in notebook_line.professionals])] +
[('create', professionals)])
NotebookLine.write([notebook_line], notebook_line_write)
# Write Supervisors to Notebook lines
supervisor_lines = {}
if hasattr(self.sit2, 'supervisor'):
supervisor_lines[self.sit2.supervisor.id] = [
l.id for l in self.sit2.lines]
for prof_id, lines in supervisor_lines.items():
notebook_lines = NotebookLine.search([
('id', 'in', lines),
])
if notebook_lines:
professionals = [{'professional': prof_id}]
notebook_line_write = {
'professionals': [('create', professionals)],
}
NotebookLine.write(notebook_lines, notebook_line_write)
# Write the execution of method
all_prof = {}
key = (self.result.professional.id, self.result.method.id)
all_prof[key] = []
if hasattr(self.sit2, 'supervisor'):
for detail in self.sit2.lines:
key = (self.sit2.supervisor.id, detail.method.id)
if key not in all_prof:
all_prof[key] = []
key = (self.result.professional.id, detail.method.id)
if self.sit2.supervisor.id not in all_prof[key]:
all_prof[key].append(self.sit2.supervisor.id)
today = Date.today()
for key, sup in all_prof.items():
professional_method, = LabProfessionalMethod.search([
('professional', '=', key[0]),
('method', '=', key[1]),
('type', '=', 'analytical'),
])
if professional_method.state == 'training':
history = LabProfessionalMethodRequalification.search([
('professional_method', '=', professional_method.id),
('type', '=', 'training'),
])
if history:
prev_supervisors = [s.supervisor.id for s in
history[0].supervisors]
supervisors = [{'supervisor': s} for s in sup
if s not in prev_supervisors]
LabProfessionalMethodRequalification.write(history, {
'last_execution_date': today,
'supervisors': [('create', supervisors)],
})
else:
supervisors = [{'supervisor': s} for s in sup]
to_create = [{
'professional_method': professional_method.id,
'type': 'training',
'date': today,
'last_execution_date': today,
'supervisors': [('create', supervisors)],
}]
LabProfessionalMethodRequalification.create(to_create)
elif professional_method.state == 'qualified':
history = LabProfessionalMethodRequalification.search([
('professional_method', '=', professional_method.id),
('type', '=', 'qualification'),
])
if history:
LabProfessionalMethodRequalification.write(history, {
'last_execution_date': today,
})
else:
to_create = [{
'professional_method': professional_method.id,
'type': 'qualification',
'date': today,
'last_execution_date': today,
}]
LabProfessionalMethodRequalification.create(to_create)
else:
history = LabProfessionalMethodRequalification.search([
('professional_method', '=', professional_method.id),
('type', '=', 'requalification'),
])
if history:
LabProfessionalMethodRequalification.write(history, {
'last_execution_date': today,
})
else:
to_create = [{
'professional_method': professional_method.id,
'type': 'requalification',
'date': today,
'last_execution_date': today,
}]
LabProfessionalMethodRequalification.create(to_create)
return 'end'