kalenislims/lims_quality_control/lims.py

752 lines
30 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
2020-04-11 23:19:16 +02:00
from trytond.model import fields
from trytond.pyson import Eval, Equal, Bool
2020-04-11 23:19:16 +02:00
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
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'
2020-04-11 23:19:16 +02:00
specification = fields.Text('Specification')
class Analysis(metaclass=PoolMeta):
__name__ = 'lims.analysis'
quality_type = fields.Selection([
('qualitative', 'Qualitative'),
('quantitative', 'Quantitative'),
], 'Quality Type', required=True)
2020-04-11 23:19:16 +02:00
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([
('qualitative', 'Qualitative'),
('quantitative', 'Quantitative')
], 'Quality Type', states={'invisible': True}),
2020-04-11 23:19:16 +02:00
'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)),
states={
'invisible': ~Equal(Eval('quality_type'), 'quantitative'),
'required': Equal(Eval('quality_type'), 'quantitative'),
}, depends=['quality_type', 'limit_digits'])
2020-04-11 23:19:16 +02:00
quality_max = fields.Float('Max',
digits=(16, Eval('limit_digits', 2)),
states={
'invisible': ~Equal(Eval('quality_type'), 'quantitative'),
'required': Equal(Eval('quality_type'), 'quantitative'),
}, depends=['quality_type', 'limit_digits'])
interface = fields.Function(fields.Many2One('lims.interface',
'Interface'), 'get_interface')
2020-04-11 23:19:16 +02:00
@classmethod
def __setup__(cls):
2020-08-06 19:52:36 +02:00
super().__setup__()
2020-04-11 23:19:16 +02:00
cls._sql_constraints = []
cls.start_uom.states = {
'invisible': ~Equal(Eval('quality_type'), 'quantitative'),
'required': Equal(Eval('quality_type'), 'quantitative'),
}
cls.start_uom.depends = ['quality_type']
cls.end_uom.states = {
'invisible': ~Equal(Eval('quality_type'), 'quantitative'),
}
cls.end_uom.depends = ['quality_type']
cls.initial_concentration.states = {
'invisible': ~Equal(Eval('quality_type'), 'quantitative'),
}
cls.initial_concentration.depends = ['quality_type']
cls.final_concentration.states = {
'invisible': ~Equal(Eval('quality_type'), 'quantitative'),
}
cls.final_concentration.depends = ['quality_type']
cls.limit_digits.states = {
'invisible': ~Equal(Eval('quality_type'), 'quantitative'),
}
cls.limit_digits.depends = ['quality_type']
cls.calc_decimals.states = {
'invisible': ~Equal(Eval('quality_type'), 'quantitative'),
}
cls.calc_decimals.depends = ['quality_type']
2020-04-11 23:19:16 +02:00
@classmethod
def __register__(cls, module_name):
2020-08-06 19:52:36 +02:00
super().__register__(module_name)
2020-04-11 23:19:16 +02:00
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('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 get_interface(cls, typifications, name):
cursor = Transaction().connection.cursor()
pool = Pool()
Template = pool.get('lims.template.analysis_sheet')
TemplateAnalysis = pool.get('lims.template.analysis_sheet.analysis')
result = {}
for t in typifications:
cursor.execute('SELECT t.id '
'FROM "' + Template._table + '" t '
'INNER JOIN "' + TemplateAnalysis._table + '" ta '
'ON t.id = ta.template '
'WHERE t.active IS TRUE '
'AND ta.analysis = %s '
'AND ta.method = %s',
(t.analysis.id, t.method.id))
template_id = cursor.fetchone()
result[t.id] = None
if not template_id:
cursor.execute('SELECT t.id '
'FROM "' + Template._table + '" t '
'INNER JOIN "' + TemplateAnalysis._table + '" ta '
'ON t.id = ta.template '
'WHERE t.active IS TRUE '
'AND ta.analysis = %s '
'AND ta.method IS NULL',
(t.analysis.id,))
template_id = cursor.fetchone()
if template_id:
template = Template(template_id[0])
result[t.id] = template.interface.id
return result
2020-04-11 23:19:16 +02:00
@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
2020-08-06 19:52:36 +02:00
return super().create(vlist)
2020-04-11 23:19:16 +02:00
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')
specification = fields.Text('Specification', readonly=True)
test_result = fields.Function(fields.Char('Test Result'),
'get_test_result')
2020-04-11 23:19:16 +02:00
@classmethod
def __setup__(cls):
super().__setup__()
cls.result.states = {
'invisible': Bool(Eval('qualitative_value')),
'readonly': Bool(Eval('accepted')),
}
cls.result.depends = ['accepted', 'qualitative_value']
2020-04-11 23:19:16 +02:00
@staticmethod
def default_quality_test_report():
return True
@classmethod
def get_test_result(cls, lines, name):
result = {}
for line in lines:
result[line.id] = line.formated_result
return result
2020-04-11 23:19:16 +02:00
@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')
2020-08-06 19:52:36 +02:00
result = super().fields_view_get(view_id=view_id, view_type=view_type)
2020-04-11 23:19:16 +02:00
# 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="1">\n'
2020-04-11 23:19:16 +02:00
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')
NotebookLine = pool.get('lims.notebook.line')
2020-04-11 23:19:16 +02:00
Company = pool.get('company.company')
Config = pool.get('lims.configuration')
Lang = pool.get('ir.lang')
2020-04-11 23:19:16 +02:00
with Transaction().set_user(0):
notebook, = Notebook.search([('fraction', '=', fraction.id)])
lines_to_create = []
2020-04-11 23:19:16 +02:00
template_id = None
if Transaction().context.get('template'):
template_id = Transaction().context.get('template')
for detail in details:
clause = ('product_type = %s '
'AND matrix = %s '
'AND analysis = %s '
'AND method = %s '
'AND valid')
params = [fraction.product_type.id, fraction.matrix.id,
detail.analysis.id, detail.method.id]
2020-04-11 23:19:16 +02:00
if template_id:
clause += ' AND quality_template = %s'
params.append(template_id)
cursor.execute('SELECT id '
'FROM "' + Typification._table + '" '
'WHERE ' + clause,
tuple(params))
res = cursor.fetchone()
t = res and Typification(res[0]) or None
if t:
repetitions = t.default_repetitions
initial_concentration = t.initial_concentration
final_concentration = t.final_concentration
initial_unit = t.start_uom and t.start_uom.id or None
final_unit = t.end_uom and t.end_uom.id or None
detection_limit = t.detection_limit
quantification_limit = t.quantification_limit
lower_limit = t.lower_limit
upper_limit = t.upper_limit
decimals = t.calc_decimals
significant_digits = t.significant_digits
scientific_notation = t.scientific_notation
report = t.report
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
decimals = 2
significant_digits = None
scientific_notation = False
report = False
2020-04-11 23:19:16 +02:00
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 = {
'notebook': notebook.id,
2020-04-11 23:19:16 +02:00
'analysis_detail': detail.id,
'service': detail.service.id,
'analysis': detail.analysis.id,
'analysis_origin': detail.analysis_origin,
'urgent': detail.service.urgent,
2020-04-11 23:19:16 +02:00
'repetition': i,
'laboratory': detail.laboratory.id,
'method': detail.method.id,
'device': detail.device and detail.device.id or None,
2020-04-11 23:19:16 +02:00
'initial_concentration': initial_concentration,
'final_concentration': final_concentration,
'initial_unit': initial_unit,
'final_unit': final_unit,
'detection_limit': detection_limit,
'quantification_limit': quantification_limit,
2020-08-06 01:03:43 +02:00
'lower_limit': lower_limit,
'upper_limit': upper_limit,
2020-04-11 23:19:16 +02:00
'decimals': decimals,
'significant_digits': significant_digits,
'scientific_notation': scientific_notation,
2020-04-11 23:19:16 +02:00
'report': report,
'results_estimated_waiting': results_estimated_waiting,
'department': department,
}
if template_id and t:
notebook_line['typification'] = t.id
notebook_line['test_value'] = (t.valid_value and
t.valid_value.id or None)
2020-04-11 23:19:16 +02:00
notebook_line['quality_test'] = Transaction().context.get(
'test')
notebook_line['quality_min'] = t.quality_min
notebook_line['quality_max'] = t.quality_max
notebook_line['quality_test_report'] = (
t.quality_test_report)
notebook_line['specification'] = t.specification
lines_to_create.append(notebook_line)
if not lines_to_create:
2020-04-11 23:19:16 +02:00
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):
lines = NotebookLine.create(lines_to_create)
# copy translated fields from typification
default_language = Config(1).results_report_language
for lang in Lang.search([
('translatable', '=', True),
('code', '!=', default_language.code),
]):
with Transaction().set_context(language=lang.code):
lines_to_save = []
for line in lines:
t = Typification.get_valid_typification(
line.product_type.id, line.matrix.id,
line.analysis.id, line.method.id)
if not t:
continue
line_lang = NotebookLine(line.id)
line_lang.initial_concentration = (
t.initial_concentration)
line_lang.final_concentration = (
t.final_concentration)
lines_to_save.append(line_lang)
NotebookLine.save(lines_to_save)
2020-04-11 23:19:16 +02:00
class AnalysisSheet(metaclass=PoolMeta):
__name__ = 'lims.analysis_sheet'
@classmethod
def delete(cls, sheets):
raise UserError(gettext('lims_quality_control.delete_sheet'))
2020-04-11 23:19:16 +02:00
class ResultReport(metaclass=PoolMeta):
__name__ = 'lims.result_report'
@classmethod
def get_reference(cls, range_type, notebook_line, language,
report_section):
2020-08-06 19:52:36 +02:00
res = super().get_reference(range_type, notebook_line, language,
report_section)
2020-04-11 23:19:16 +02:00
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'
2020-04-11 23:19:16 +02:00
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'