kalenislims/lims_quality_control/quality.py

815 lines
27 KiB
Python

# 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.
import datetime
from io import BytesIO
from PyPDF2 import PdfFileMerger
from trytond.model import Workflow, ModelView, ModelSQL, DeactivableMixin, \
fields
from trytond.wizard import Wizard, StateView, StateTransition, StateAction, \
StateReport, Button
from trytond.pyson import PYSONEncoder, Bool, Equal, Eval, Not
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.report import Report
from trytond.tools import grouped_slice, reduce_ids
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.modules.company import CompanyReport
class QualitativeValue(DeactivableMixin, ModelSQL, ModelView):
'Quality Value'
__name__ = 'lims.quality.qualitative.value'
name = fields.Char('Name', required=True, translate=True,
select=True)
analysis = fields.Many2One('lims.analysis', 'Analysis')
typification = fields.Many2One('lims.typification', 'Typification')
class Template(Workflow, ModelSQL, ModelView):
'Quality Template'
__name__ = 'lims.quality.template'
_history = True
_states = {'readonly': Eval('state') != 'draft'}
_depends = ['state']
name = fields.Char('Name', required=True, translate=True,
select=True, states=_states, depends=_depends)
product = fields.Many2One('product.product', 'Product', required=True,
select=True, states=_states, depends=_depends)
company = fields.Many2One('company.company', 'Company', required=True,
select=True, states=_states, depends=_depends)
comments = fields.Text('Comments', states=_states, depends=_depends)
lines = fields.One2Many('lims.typification', 'quality_template', 'Lines',
domain=[
('quality', '=', True),
],
context={'quality': True},
states=_states, depends=_depends + ['product'],
order=[('quality_order', 'ASC'), ('analysis.code', 'ASC')])
revision = fields.Integer(
"Revision", required=True, readonly=True)
countersample_required = fields.Boolean('Countersample Required',
states=_states, depends=_depends)
results_report_required = fields.Boolean('Results Report Required',
states=_states, depends=_depends)
range_validate = fields.Boolean('Ranges Validate',
states=_states, depends=_depends)
range_type = fields.Many2One('lims.range.type', 'Range Type',
states={
'readonly': Eval('state') != 'draft',
'invisible': Not(Bool(Eval('range_validate'))),
'required': Bool(Eval('range_validate')),
}, depends=_depends + ['range_validate'])
start_date = fields.Date('Start Date', states=_states, depends=_depends)
end_date = fields.Date('End Date', states=_states, depends=_depends)
state = fields.Selection([
('draft', 'Draft'),
('active', 'Active'),
('not_active', 'Not Active'),
], 'State', readonly=True, required=True)
del _states, _depends
@classmethod
def __setup__(cls):
super().__setup__()
cls._transitions |= set((
('draft', 'active'),
('active', 'draft'),
('active', 'not_active'),
('not_active', 'active'),
))
cls._buttons.update({
'active': {
'invisible': (Eval('state') == 'active'),
'icon': 'tryton-forward',
},
'not_active': {
'invisible': (Eval('state') != 'active'),
'icon': 'tryton-forward',
},
'draft': {
'invisible': (Eval('state') != 'active'),
'icon': 'tryton-clear',
},
'copy_lines': {
'invisible': Eval('state') != 'draft',
},
})
@classmethod
def default_revision(cls):
return 1
@classmethod
def default_start_date(cls):
Date = Pool().get('ir.date')
return Date.today()
@staticmethod
def default_state():
return 'draft'
@staticmethod
def default_company():
return Transaction().context.get('company')
@classmethod
def copy(cls, templates, default=None):
if default is None:
default = {}
if 'lines' not in default:
default['lines'] = None
default['revision'] = 1
return super().copy(templates, default)
@classmethod
def check_delete(cls, templates):
for t in templates:
if t.state != 'draft' or t.revision > 1:
raise UserError(gettext(
'lims_quality_control.msg_delete_template',
template=t.rec_name))
@classmethod
def delete(cls, templates):
cls.check_delete(templates)
super().delete(templates)
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, templates):
cursor = Transaction().connection.cursor()
table = cls.__table__()
# Use SQL and before super to avoid two history entries
for sub_sales in grouped_slice(templates):
cursor.execute(*table.update(
[table.revision],
[table.revision + 1],
where=reduce_ids(table.id, sub_sales)))
@classmethod
@ModelView.button
@Workflow.transition('active')
def active(cls, templates):
for template in templates:
if not template.lines:
raise UserError(gettext(
'lims_quality_control.msg_missing_template_lines'))
@classmethod
@ModelView.button
@Workflow.transition('not_active')
def not_active(cls, templates):
pass
@classmethod
@ModelView.button_action('lims_quality_control.wiz_quality_template_copy_line')
def copy_lines(cls, typifications):
pass
class QualityTest(Workflow, ModelSQL, ModelView):
'Quality Test'
__name__ = 'lims.quality.test'
_rec_name = 'number'
_states = {'readonly': Eval('state') != 'draft'}
_depends = ['state']
number = fields.Char('Number', readonly=True, select=True,
states={'required': Not(Equal(Eval('state'), 'draft'))})
company = fields.Many2One('company.company', 'Company', required=True,
select=True, states=_states, depends=_depends)
test_date = fields.DateTime('Date', states=_states, depends=_depends)
comments = fields.Text('Comments', states=_states, depends=_depends)
lines = fields.One2Many('lims.notebook.line', 'quality_test', 'Lines',
domain=[
('quality_test_report', '=', True),
],
readonly=True, context={'quality': True})
template = fields.Many2One('lims.quality.template', 'Template',
required=True, states=_states, depends=_depends)
sample = fields.Many2One('lims.sample', 'Sample',
domain=[('lot.product', '=', Eval('product'))],
states=_states, depends=_depends + ['product'])
countersample = fields.Function(fields.Many2One('lims.sample',
'Countersample',
states={
'invisible': Not(Bool(Eval('countersample'))),
}), 'get_countersample')
product = fields.Function(fields.Many2One('product.product', 'Product',
states=_states, depends=_depends), 'on_change_with_product',
searcher='search_product')
lot = fields.Function(fields.Many2One('stock.lot', 'Lot',
states=_states, depends=_depends), 'on_change_with_lot',
searcher='search_lot')
success = fields.Function(fields.Boolean('Success'), 'get_success')
confirmed_date = fields.DateTime('Confirmed Date', readonly=True,
states={
'invisible': Eval('state') == 'draft',
},
depends=_depends)
validated_date = fields.DateTime('Validated Date', readonly=True,
states={
'invisible': Eval('state') == 'draft',
},
depends=_depends)
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('successful', 'Successful'),
('failed', 'Failed'),
], 'State', readonly=True, required=True)
state_string = state.translated('state')
del _states, _depends
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('number', 'DESC'))
cls._transitions |= set((
('draft', 'confirmed'),
('confirmed', 'successful'),
('confirmed', 'failed'),
))
cls._buttons.update({
'confirm': {
'invisible': (Eval('state') != 'draft'),
'icon': 'tryton-forward',
},
'manager_validate': {
'invisible': (Eval('state') != 'confirmed'),
'icon': 'tryton-ok',
},
})
def get_success(self, name):
if not self.lines:
return False
for line in self.lines:
if not line.success:
return False
return True
@fields.depends('sample')
def get_countersample(self, name):
if self.sample and self.sample.countersample:
return self.sample.countersample.id
@staticmethod
def default_test_date():
return datetime.datetime.now()
@staticmethod
def default_state():
return 'draft'
@staticmethod
def default_company():
return Transaction().context.get('company')
@fields.depends('template')
def on_change_with_product(self, name=None):
if self.template:
return self.template.product.id
@classmethod
def search_product(cls, name, clause):
return ['OR', ('template.product.name',) + tuple(clause[1:]),
('template.product.code',) + tuple(clause[1:]),
]
@fields.depends('sample')
def on_change_with_lot(self, name=None):
if self.sample:
return self.sample.lot.id
@classmethod
def search_lot(cls, name, clause):
return [('sample.lot',) + tuple(clause[1:])]
@classmethod
def check_delete(cls, tests):
for t in tests:
if t.state != 'draft':
raise UserError(gettext(
'lims_quality_control.msg_delete_test',
test=t.rec_name))
@classmethod
def delete(cls, tests):
cls.check_delete(tests)
super().delete(tests)
@classmethod
@ModelView.button
@Workflow.transition('confirmed')
def confirm(cls, tests):
for test in tests:
test.apply_template_values()
test.save()
cls.set_number(tests)
cls.write(tests, {'confirmed_date': datetime.datetime.now()})
@classmethod
@Workflow.transition('successful')
def successful(cls, tests):
cls.write(tests, {'validated_date': datetime.datetime.now()})
@classmethod
@Workflow.transition('failed')
def failed(cls, tests):
cls.write(tests, {'validated_date': datetime.datetime.now()})
@classmethod
@ModelView.button
def manager_validate(cls, tests):
for test in tests:
results_report_required = test.template.results_report_required
for line in test.lines:
if not line.accepted:
raise UserError(gettext(
'lims_quality_control.msg_missing_accepted_lines'))
if (results_report_required and
line.report and not line.results_report):
raise UserError(gettext(
'lims_quality_control.msg_missing_results_report'))
if (test.template.countersample_required and
test.sample.test_state != 'countersample' and
not test.sample.countersamples):
raise UserError(gettext(
'lims_quality_control.msg_missing_countersample'))
if test.success:
cls.successful(tests)
else:
cls.failed(tests)
@classmethod
def set_number(cls, tests):
pool = Pool()
Config = pool.get('lims.quality.configuration')
config = Config(1)
for test in tests:
test.number = config.quality_sequence.get()
test.save()
def apply_template_values(self):
pool = Pool()
Service = pool.get('lims.service')
Fraction = pool.get('lims.fraction')
# new services
services_default = []
for line in self.template.lines:
if not line.interface:
raise UserError(gettext(
'lims_quality_control.msg_missing_interface',
analysis=line.analysis.rec_name))
laboratory_id = (line.analysis.laboratories[0].laboratory.id
if line.analysis.type != 'group' else None)
services_default.append({
'fraction': self.sample.fractions[0].id,
'analysis': line.analysis.id,
'laboratory': laboratory_id,
'method': line.method.id,
'device': (line.analysis.devices[0].device.id
if line.analysis.devices else None),
})
for service in services_default:
new_service, = Service.create([service])
# new analysis details (on service create)
# confirm fraction: new notebook and stock move
context = {
'template': self.template.id,
'test': self.id,
}
with Transaction().set_context(context):
Fraction.confirm(self.sample.fractions)
sample = self.sample
if sample.test_state != 'countersample':
sample.test_state = 'done'
sample.quality_test = self.id
sample.save()
@classmethod
def copy(cls, tests, default=None):
if default is None:
default = {}
if 'templates' not in default:
default['templates'] = None
return super().copy(tests, default)
class CreateQualityTestStart(ModelView):
'Create Quality Test Start'
__name__ = 'lims.create.quality.test.start'
template = fields.Many2One('lims.quality.template', 'Template',
required=True,
domain=[
('product', '=', Eval('product')),
('state', '=', 'active'),
('end_date', '>=', Eval('date')),
],
depends=['product', 'date'])
product = fields.Many2One('product.product', 'Product')
test_created = fields.Many2One('lims.quality.test', 'Test created')
date = fields.Date('Date')
@staticmethod
def default_date():
Date = Pool().get('ir.date')
return Date.today()
class CreateQualityTest(Wizard):
'Create Quality Test'
__name__ = 'lims.create.quality.test'
start = StateTransition()
ask = StateView('lims.create.quality.test.start',
'lims_quality_control.lims_create_quality_test_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Confirm', 'confirm', 'tryton-ok', default=True),
])
confirm = StateTransition()
open_ = StateAction('lims_quality_control.act_quality_test')
def transition_start(self):
Sample = Pool().get('lims.sample')
sample = Sample(Transaction().context['active_id'])
if sample.test_state == 'done':
raise UserError(gettext(
'lims_quality_control.msg_sample_used'))
return 'ask'
def default_ask(self, fields):
pool = Pool()
Sample = pool.get('lims.sample')
Template = pool.get('lims.quality.template')
Date = pool.get('ir.date')
sample = Sample(Transaction().context['active_id'])
product_id = sample.lot.product.id
res = {
'product': product_id,
}
templates = Template.search([
('product', '=', product_id),
('state', '=', 'active'),
('end_date', '>=', Date.today()),
])
if len(templates) == 1:
res['template'] = templates[0].id
return res
def transition_confirm(self):
test = self.create_test()
self.ask.test_created = test.id
return 'open_'
def create_test(self):
pool = Pool()
Sample = pool.get('lims.sample')
Test = pool.get('lims.quality.test')
sample = Sample(Transaction().context.get('active_id'))
new_test, = Test.create([{
'template': self.ask.template.id,
'sample': sample.id,
}])
return new_test
def do_open_(self, action):
action['views'].reverse()
return action, {
'res_id': [self.ask.test_created.id],
}
class TemplateAddServiceStart(ModelView):
'Template Add Service Start'
__name__ = 'lims.template.add.service.start'
service = fields.Many2One('lims.analysis', 'Service',
required=True,
domain=[
('type', 'in', ['set', 'group']),
('state', '=', 'active'),
])
class TemplateAddService(Wizard):
'Template Add Service'
__name__ = 'lims.template.add.service'
start = StateTransition()
ask = StateView('lims.template.add.service.start',
'lims_quality_control.lims_template_add_service_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Add', 'add', 'tryton-ok', default=True),
])
add = StateTransition()
def transition_start(self):
Template = Pool().get('lims.quality.template')
template = Template(Transaction().context['active_id'])
if template.state != 'draft':
raise UserError(gettext(
'lims_quality_control.msg_template_not_draft'))
return 'ask'
def transition_add(self):
self.add_service()
return 'end'
def add_service(self):
pool = Pool()
Template = pool.get('lims.quality.template')
Typification = pool.get('lims.typification')
template = Template(Transaction().context.get('active_id'))
for analysis in self.ask.service.all_included_analysis:
typification = Typification()
typification.quality = True
typification.quality_template = template.id
typification.analysis = analysis.id
typification.method = analysis.methods[0]
if analysis.quality_type == 'qualitative':
typification.valid_value = \
analysis.quality_possible_values[0].id
typification.save()
class TestResultsReport(Wizard):
'Test Results Report'
__name__ = 'lims.test.results_report'
start = StateAction('lims.act_lims_results_report_list')
def do_start(self, action):
pool = Pool()
Test = pool.get('lims.quality.test')
EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
active_ids = Transaction().context['active_ids']
tests = Test.browse(active_ids)
sample_ids = [test.sample.id for test in tests]
results_report_ids = []
details = EntryDetailAnalysis.search([('sample', 'in', sample_ids)])
if details:
results_report_ids = [d.results_report.id for d in details
if d.results_report]
action['pyson_domain'] = PYSONEncoder().encode([
('id', 'in', results_report_ids),
])
action['name'] += ' (%s)' % ', '.join(
t.rec_name for t in tests)
return action, {}
class OpenTestAttachment(Wizard):
'Test Attachment'
__name__ = 'lims.quality.test.open_attachment'
start = StateAction('lims.act_attachment')
def do_start(self, action):
Test = Pool().get('lims.quality.test')
active_ids = Transaction().context['active_ids']
tests = Test.browse(active_ids)
resources = self.get_resource(tests)
action['pyson_domain'] = PYSONEncoder().encode([
('resource', 'in', resources),
])
action['name'] += ' (%s)' % ', '.join(t.rec_name for t in tests)
return action, {}
def get_resource(self, tests):
res = []
for test in tests:
res.append(self._get_resource(test))
for line in test.lines:
if line.analysis_sheet:
res.append(self._get_resource(line.analysis_sheet))
return res
def _get_resource(self, obj):
return '%s,%s' % (obj.__name__, obj.id)
class PrintTest(Wizard):
'Print Test'
__name__ = 'lims.print_test'
start = StateTransition()
print_ = StateReport('lims.quality.control.report')
def transition_start(self):
Test = Pool().get('lims.quality.test')
test = Test(Transaction().context['active_id'])
if test.state not in ['successful', 'failed']:
raise UserError(gettext(
'lims_quality_control.msg_can_not_print_test'))
return 'print_'
def do_print_(self, action):
return action, {
'ids': Transaction().context['active_ids'],
}
class TestReport(CompanyReport):
'Test Report'
__name__ = 'lims.quality.control.report'
@classmethod
def execute(cls, ids, data):
pool = Pool()
Test = pool.get('lims.quality.test')
result = super().execute(ids, data)
if len(ids) == 1:
test, = Test.browse(ids)
result = result[:3] + (
test.product.product_type.code + ' - ' + test.lot.number,)
return result
@classmethod
def get_context(cls, records, header, data):
Test = Pool().get('lims.quality.test')
report_context = super().get_context(records, header, data)
report_context['records'] = Test.browse(data['ids'])
report_context['get_professionals'] = cls.get_professionals
return report_context
@classmethod
def get_professionals(cls, test):
professionals = []
for line in test.lines:
for professional in line.professionals:
professionals.append(professional)
professionals = list(set([
p.professional.party.name for p in professionals]))
return ' / '.join(professionals)
class TestAttachmentReport(Report):
'Test Attachment Report'
__name__ = 'lims.quality.test.attachment.report'
@classmethod
def execute(cls, ids, data):
pool = Pool()
Test = pool.get('lims.quality.test')
Attachment = pool.get('ir.attachment')
if len(ids) > 1:
raise UserError(gettext('lims.msg_multiple_reports'))
test = Test(ids[0])
resources = []
for line in test.lines:
if line.analysis_sheet:
resources.append(cls._get_resource(line.analysis_sheet))
attachments = Attachment.search([
('resource', 'in', resources),
])
merger = PdfFileMerger(strict=False)
for attachment in attachments:
filedata = BytesIO(attachment.data)
merger.append(filedata)
output = BytesIO()
merger.write(output)
document = output.getvalue()
return 'pdf', document
@classmethod
def _get_resource(cls, obj):
return '%s,%s' % (obj.__name__, obj.id)
class CopyQualityTemplateLineStart(ModelView):
'Copy Quality Template Line'
__name__ = 'lims.quality.template.copy_line.start'
origin_quality_template = fields.Many2One('lims.quality.template', 'Origin Template',
required=True,
)
class CopyQualityTemplateLine(Wizard):
'Copy Quality Template Line'
__name__ = 'lims.quality.template.copy_line'
start = StateTransition()
ask = StateView('lims.quality.template.copy_line.start',
'lims_quality_control.quality_template_copy_line_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Copy', 'copy', 'tryton-ok', default=True),
])
copy = StateTransition()
def transition_start(self):
QualityTemplate = Pool().get('lims.quality.template')
quality_template_id = Transaction().context.get('active_id', None)
if not quality_template_id:
return 'end'
quality_template = QualityTemplate(quality_template_id)
if quality_template.state != 'draft':
return 'end'
return 'ask'
def transition_copy(self):
Line = Pool().get('lims.typification')
quality_template_id = Transaction().context.get('active_id', None)
count = Line.search_count([('quality_template', '=', quality_template_id)])
new_lines = []
origin_lines = Line.search([
('quality_template', '=', self.ask.origin_quality_template.id),
], order=[('quality_order', 'ASC')])
for origin in origin_lines:
count += 1
line = self._get_line(origin)
line['quality_template'] = quality_template_id
line['quality_order'] = count
new_lines.append(line)
if new_lines:
Line.create(new_lines)
return 'end'
def _get_line(self, origin):
res = {
'product_type': origin.product_type,
'matrix': origin.matrix,
'analysis': origin.analysis,
'method': origin.method,
'detection_limit': origin.detection_limit,
'quantification_limit': origin.quantification_limit,
'lower_limit': origin.lower_limit,
'upper_limit': origin.upper_limit,
'limit_digits': origin.limit_digits,
'check_result_limits': origin.check_result_limits,
'initial_concentration': origin.initial_concentration,
'start_uom': origin.start_uom,
'final_concentration': origin.final_concentration,
'end_uom': origin.end_uom,
'default_repetitions': origin.default_repetitions,
'comments': origin.comments,
'additional': origin.additional,
'additionals': origin.additionals,
'by_default': origin.by_default,
'calc_decimals': origin.calc_decimals,
'significant_digits': origin.significant_digits,
'scientific_notation': origin.scientific_notation,
'report': origin.report,
'report_type': origin.report_type,
'report_result_type': origin.report_result_type,
'referable': origin.referable,
'valid': origin.valid,
'specification': origin.specification,
'quality': origin.quality,
'valid_value': origin.valid_value,
'quality_test_report': origin.quality_test_report,
'quality_order': origin.quality_order,
#problemas al copiar algunos registros que no tienen valor en quality_min
'quality_min': origin.quality_min,
'quality_max': origin.quality_max,
}
return res