kalenislims/lims_quality_control/sample.py

383 lines
14 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.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from trytond.model import fields, ModelView
from trytond.wizard import Wizard, StateView, StateTransition, StateAction, \
StateReport, Button
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
from trytond.pyson import PYSONEncoder, Bool, Not, Eval
from trytond.report import Report
from trytond.exceptions import UserError
from trytond.i18n import gettext
class LabWorkYear(metaclass=PoolMeta):
__name__ = 'lims.lab.workyear'
default_entry_quality = fields.Many2One('lims.entry',
'Default entry quality')
class Fraction(metaclass=PoolMeta):
__name__ = 'lims.fraction'
def _get_stock_move(self):
move = super()._get_stock_move()
if self.sample.quality:
move.product = self.sample.lot.product.id
move.lot = self.sample.lot.id
return move
class Sample(metaclass=PoolMeta):
__name__ = 'lims.sample'
quality = fields.Boolean('Quality')
lot = fields.Many2One('stock.lot', 'Lot', readonly=True)
test_state = fields.Selection([
('pending', 'Pending'),
('done', 'Done'),
('countersample', 'Countersample'),
], 'Test State', readonly=True)
product = fields.Function(fields.Many2One('product.product',
'Product'), 'on_change_with_product', searcher='search_product')
quality_test = fields.Many2One('lims.quality.test', 'Test', readonly=True)
countersample_original_sample = fields.Many2One('lims.sample',
'Countersample Original sample', readonly=True,
states={'invisible': Not(Bool(Eval('countersample_original_sample')))})
countersamples = fields.One2Many('lims.sample',
'countersample_original_sample', 'Countersamples', readonly=True)
countersample = fields.Function(fields.Many2One('lims.sample',
'Countersample'), 'get_countersample')
@staticmethod
def default_test_state():
return 'pending'
@fields.depends('lot')
def on_change_with_product(self, name=None):
if self.lot:
return self.lot.product.id
@classmethod
def search_product(cls, name, clause):
return [('lot.' + name,) + tuple(clause[1:])]
@fields.depends('countersamples')
def get_countersample(self, name):
if self.countersamples:
return self.countersamples[0].id
class TakeSampleStart(ModelView):
'Take Sample Start'
__name__ = 'lims.take.sample.start'
date = fields.Date('Date', required=True)
label = fields.Char('Label', required=True)
attributes = fields.Dict('lims.sample.attribute', 'Attributes')
sample = fields.Many2One('lims.sample', 'Sample')
@staticmethod
def default_date():
Date = Pool().get('ir.date')
return Date.today()
class TakeSampleResult(ModelView):
'Take Sample Result'
__name__ = 'lims.take.sample.result'
message = fields.Text('Message', readonly=True)
class TakeSample(Wizard):
'Take Sample'
__name__ = 'lims.take.sample'
start = StateView('lims.take.sample.start',
'lims_quality_control.lims_take_sample_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Confirm', 'confirm', 'tryton-ok', default=True),
])
confirm = StateTransition()
result = StateView('lims.take.sample.result',
'lims_quality_control.lims_take_sample_result_view_form', [
Button('Close', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-print', default=True),
])
print_ = StateReport('lims.sample.labels.report')
def transition_confirm(self):
self.start.sample = self.create_sample()
return 'result'
def create_sample(self):
pool = Pool()
Config = pool.get('lims.configuration')
QualityConfig = pool.get('lims.quality.configuration')
Lot = pool.get('stock.lot')
LabWorkYear = pool.get('lims.lab.workyear')
Entry = pool.get('lims.entry')
Sample = pool.get('lims.sample')
Fraction = pool.get('lims.fraction')
lot = Lot(Transaction().context.get('active_id'))
if not lot.product.template.product_type:
raise UserError(gettext('lims.msg_no_product_product_type'))
if not lot.product.template.matrix:
raise UserError(gettext('lims.msg_no_product_matrix'))
config = Config(1)
if not config.qc_fraction_type:
raise UserError(gettext(
'lims_quality_control.msg_no_qc_fraction_type'))
fraction_type = config.qc_fraction_type
if (not fraction_type.default_package_type or
not fraction_type.default_fraction_state):
raise UserError(gettext(
'lims_quality_control.msg_no_qc_default_configuration'))
quality_config = QualityConfig(1)
workyear_id = LabWorkYear.find()
workyear = LabWorkYear(workyear_id)
if not workyear.default_entry_quality:
raise UserError(gettext(
'lims_quality_control.msg_no_entry_quality'))
entry = Entry(workyear.default_entry_quality.id)
if not entry.party.entry_zone and config.zone_required:
raise UserError(gettext('lims.msg_no_party_zone',
party=entry.party.rec_name))
zone_id = entry.party.entry_zone and entry.party.entry_zone.id or None
obj_description = self._get_obj_description(lot.product)
# new sample
new_sample, = Sample.create([{
'quality': True,
'lot': lot.id,
'attributes': self.start.attributes,
'entry': entry.id,
'party': entry.party.id,
'date': datetime.now(),
'product_type': lot.product.template.product_type.id,
'matrix': lot.product.template.matrix.id,
'zone': zone_id,
'label': self.start.label,
'obj_description': obj_description,
'packages_quantity': 1,
'fractions': [],
}])
# new fraction
fraction_default = {
'sample': new_sample.id,
'type': fraction_type.id,
'storage_location': quality_config.sample_location.id,
'packages_quantity': 1,
'package_type': fraction_type.default_package_type.id,
'fraction_state': fraction_type.default_fraction_state.id,
'services': [],
}
if fraction_type.max_storage_time:
fraction_default['storage_time'] = fraction_type.max_storage_time
elif quality_config.sample_location.storage_time:
fraction_default['storage_time'] = (
quality_config.sample_location.storage_time)
else:
fraction_default['storage_time'] = 3
new_fraction, = Fraction.create([fraction_default])
return new_sample
def _get_obj_description(self, product):
cursor = Transaction().connection.cursor()
ObjectiveDescription = Pool().get('lims.objective_description')
cursor.execute('SELECT id '
'FROM "' + ObjectiveDescription._table + '" '
'WHERE product_type = %s '
'AND matrix = %s',
(product.template.product_type.id, product.template.matrix.id))
res = cursor.fetchone()
return res and res[0] or None
def default_result(self, fields):
default = {'message': '%s: %s' % (
gettext('lims_quality_control.msg_sample_created'),
self.start.sample.number)}
return default
def do_print_(self, action):
data = {
'sample': self.start.sample.id,
}
return action, data
def transition_print_(self):
return 'end'
class CountersampleCreateStart(ModelView):
'Countersample Create Start'
__name__ = 'lims.countersample.create.start'
location = fields.Many2One('stock.location', 'Location', required=True)
quantity = fields.Float('Quantity')
comments = fields.Text('Comments')
countersamples = fields.Many2Many(
'lims.sample', None, None, 'Countersamples')
class CountersampleCreate(Wizard):
'Countersample Create'
__name__ = 'lims.countersample.create'
start = StateTransition()
ask = StateView('lims.countersample.create.start',
'lims_quality_control.lims_countersample_create_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Create', 'create_', 'tryton-ok', default=True),
])
create_ = StateTransition()
open_ = StateAction('lims_quality_control.act_lims_sample_list')
def transition_start(self):
Sample = Pool().get('lims.sample')
if not Transaction().context['active_ids']:
raise UserError(gettext(
'lims_quality_control.msg_records_not_selected'))
samples = Sample.browse(Transaction().context['active_ids'])
for sample in samples:
if sample.test_state != 'done':
raise UserError(gettext(
'lims_quality_control.msg_not_test_sample'))
if sample.countersample:
raise UserError(gettext(
'lims_quality_control.msg_has_countersample'))
return 'ask'
def transition_create_(self):
countersamples = self.create_countersample()
self.ask.countersamples = [sample.id for sample in countersamples]
return 'open_'
def create_countersample(self):
pool = Pool()
Sample = pool.get('lims.sample')
Fraction = pool.get('lims.fraction')
Move = pool.get('stock.move')
Date = pool.get('ir.date')
samples = Sample.browse(Transaction().context.get('active_ids'))
countersamples = []
for sample in samples:
# new countersample
new_countersample, = Sample.create([{
'quality': True,
'lot': sample.lot.id,
'attributes': sample.attributes,
'entry': sample.entry.id,
'party': sample.party.id,
'date': datetime.now(),
'product_type': sample.product_type.id,
'matrix': sample.matrix.id,
'zone': sample.zone and sample.zone.id or None,
'label': sample.label,
'obj_description': sample.obj_description,
'packages_quantity': sample.packages_quantity,
'countersample_original_sample': sample.id,
'test_state': 'countersample',
'comments': self.ask.comments,
'fractions': [],
}])
# new fraction
fraction_default = {
'sample': new_countersample.id,
'type': sample.fractions[0].type.id,
'storage_location': self.ask.location.id,
'packages_quantity': sample.packages_quantity,
'package_type': sample.fractions[0].package_type.id,
'fraction_state': sample.fractions[0].fraction_state.id,
'storage_time': sample.fractions[0].storage_time,
'countersample_location': self.ask.location.id,
'countersample_date': Date.today(),
'expiry_date': Date.today() + relativedelta(
months=sample.fractions[0].storage_time),
'services': [],
}
new_fraction, = Fraction.create([fraction_default])
moves = self._get_stock_moves([new_fraction], self.ask.quantity)
Move.do(moves)
countersamples.append(new_countersample)
return countersamples
def do_open_(self, action):
action['pyson_domain'] = PYSONEncoder().encode([
('id', 'in', [sample.id for sample in self.ask.countersamples]),
])
action['views'].reverse()
return action, {
'res_id': [self.ask.countersamples[0].id],
}
def _get_stock_moves(self, fractions, quantity):
pool = Pool()
User = pool.get('res.user')
Move = pool.get('stock.move')
company = User(Transaction().user).company
moves = []
for fraction in fractions:
with Transaction().set_user(0, set_context=True):
move = Move()
move.product = fraction.sample.lot.product.id
move.lot = fraction.sample.lot.id
move.fraction = fraction.id
move.quantity = quantity or 1
move.uom = fraction.sample.lot.product.default_uom
move.from_location = \
fraction.sample.countersample_original_sample.fractions[
0].storage_location.id
move.to_location = fraction.countersample_location.id
move.company = company
move.planned_date = fraction.countersample_date
move.origin = fraction
move.state = 'draft'
move.save()
moves.append(move)
return moves
class SampleLabels(Report):
'Sample Labels'
__name__ = 'lims.sample.labels.report'
@classmethod
def get_context(cls, records, header, data):
Sample = Pool().get('lims.sample')
report_context = super().get_context(records, header, data)
labels = []
if data.get('sample'):
records = [Sample(data.get('sample'))]
for sample in records:
labels.append(sample)
report_context['labels'] = labels
return report_context