# -*- coding: utf-8 -*-
# This file is part of lims module for Tryton.
# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.
import logging
from datetime import datetime, date
from dateutil.relativedelta import relativedelta

from trytond.model import Workflow, ModelView, ModelSQL, fields, Unique
from trytond.wizard import Wizard, StateTransition, StateView, StateAction, \
    Button
from trytond.report import Report
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.pyson import PYSONEncoder, Eval, Equal, Bool, Not, Or
from trytond.exceptions import UserError
from trytond.i18n import gettext
from .configuration import get_print_date


class Planification(Workflow, ModelSQL, ModelView):
    'Planification'
    __name__ = 'lims.planification'
    _rec_name = 'code'

    _states = {'readonly': Not(Bool(Equal(Eval('state'), 'draft')))}
    _depends = ['state']

    code = fields.Char('Code', select=True, readonly=True)
    date = fields.Date('Date', readonly=True)
    laboratory = fields.Many2One('lims.laboratory', 'Laboratory',
        required=True, states={
            'readonly': ((Eval('state') != 'draft')
                | (Eval('analysis', [0]) & Eval('laboratory'))),
            }, depends=['state'])
    analysis = fields.Many2Many('lims.planification-analysis',
        'planification', 'analysis', 'Analysis/Sets/Groups',
        context={'date_from': Eval('date_from'), 'date_to': Eval('date_to'),
            'calculate': Bool(Equal(Eval('state'), 'draft'))},
        domain=['OR', ('id', 'in', Eval('analysis')), [
            ('id', 'in', Eval('analysis_domain'))]],
        states=_states,
        depends=['state', 'date_from', 'date_to', 'analysis_domain'])
    analysis_domain = fields.Function(fields.Many2Many('lims.analysis',
        None, None, 'Analysis domain'),
        'on_change_with_analysis_domain')
    technicians = fields.One2Many('lims.planification.technician',
        'planification', 'Technicians', depends=['method_domain',
        'technicians_domain'])
    date_from = fields.Date('Date from', states=_states, depends=_depends)
    date_to = fields.Date('Date to', states=_states, depends=_depends)
    start_date = fields.Date('Start date', depends=_depends,
        states={'readonly': Bool(Equal(Eval('state'), 'confirmed'))})
    details = fields.One2Many('lims.planification.detail',
        'planification', 'Fractions to plan',
        states=_states, depends=_depends)
    controls = fields.Many2Many('lims.planification-fraction',
        'planification', 'fraction', 'Controls', readonly=True)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('preplanned', 'Pre-Planned'),
        ('confirmed', 'Confirmed'),
        ('not_executed', 'Not executed'),
        ], 'State', required=True, readonly=True)
    waiting_process = fields.Boolean('Waiting process')
    wizard_executed = fields.Boolean('Wizard executed')
    method_domain = fields.Function(fields.One2Many('lims.lab.method',
        None, 'Method domain'),
        'on_change_with_method_domain', setter='set_method_domain')
    technicians_domain = fields.Function(fields.One2Many(
        'lims.laboratory.professional', None, 'Technicians domain'),
        'on_change_with_technicians_domain', setter='set_technicians_domain')
    comments = fields.Text('Comments')

    del _states, _depends

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('code', 'DESC'))
        cls._transitions |= set((
            ('draft', 'preplanned'),
            ('preplanned', 'confirmed'),
            ('confirmed', 'not_executed'),
            ))
        cls._buttons.update({
            'add_analysis': {
                'readonly': (Eval('state') != 'draft'),
                },
            'search_fractions': {
                'readonly': (Eval('state') != 'draft'),
                },
            'search_planned_fractions': {
                'readonly': (Eval('state') != 'draft'),
                },
            'preplan': {
                'invisible': (Eval('state') != 'draft'),
                },
            'confirm': {
                'invisible': (Eval('state') != 'preplanned'),
                },
            'release_controls': {
                'invisible': (Eval('state') != 'confirmed'),
                },
            'relate_technicians': {
                'readonly': (Eval('state') != 'preplanned'),
                },
            'unlink_technicians': {
                'readonly': (Eval('state') != 'preplanned'),
                },
            'replace_technician': {
                'readonly': (Eval('state') != 'confirmed'),
                },
            'add_fraction_con': {
                'readonly': (Eval('state') != 'preplanned'),
                },
            'add_fraction_rm_bmz': {
                'readonly': (Eval('state') != 'preplanned'),
                },
            'add_fraction_bre': {
                'readonly': (Eval('state') != 'preplanned'),
                },
            'add_fraction_mrt': {
                'readonly': (Eval('state') != 'preplanned'),
                },
            'remove_control': {
                'readonly': (Eval('state') != 'preplanned'),
                },
            })

    @classmethod
    def __register__(cls, module_name):
        super().__register__(module_name)
        table_h = cls.__table_handler__(module_name)
        table_h.not_null_action('date_from', action='remove')
        table_h.not_null_action('date_to', action='remove')

    @staticmethod
    def default_state():
        return 'draft'

    @staticmethod
    def default_waiting_process():
        return False

    @staticmethod
    def default_wizard_executed():
        return False

    @staticmethod
    def default_date():
        Date = Pool().get('ir.date')
        return Date.today()

    @staticmethod
    def default_laboratory():
        return Transaction().context.get('laboratory', None)

    @fields.depends('laboratory', 'state',
        methods=['_get_analysis_domain'])
    def on_change_with_analysis_domain(self, name=None):
        if not self.laboratory or self.state != 'draft':
            return []
        return self._get_analysis_domain(self.laboratory)

    @staticmethod
    def _get_analysis_domain(laboratory):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        AnalysisLaboratory = pool.get('lims.analysis-laboratory')
        Analysis = pool.get('lims.analysis')

        if not laboratory:
            return []

        cursor.execute('SELECT al.analysis '
            'FROM "' + AnalysisLaboratory._table + '" al '
                'INNER JOIN "' + Analysis._table + '" a '
                'ON a.id = al.analysis '
            'WHERE al.laboratory = %s '
                'AND a.type = \'analysis\' '
                'AND a.behavior != \'internal_relation\'',
            (laboratory.id,))
        analyzes_list = [a[0] for a in cursor.fetchall()]

        others_list = []
        cursor.execute('SELECT id '
            'FROM "' + Analysis._table + '" '
            'WHERE type != \'analysis\'')
        others_list_ids = [x[0] for x in cursor.fetchall()]
        for set_group_id in others_list_ids:
            if Planification._get_set_group_available(set_group_id,
                    analyzes_list):
                others_list.append(set_group_id)

        return analyzes_list + others_list

    @staticmethod
    def _get_set_group_available(set_group_id, analyzes_list):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        AnalysisIncluded = pool.get('lims.analysis.included')
        Analysis = pool.get('lims.analysis')

        cursor.execute('SELECT ia.included_analysis, a.type '
            'FROM "' + AnalysisIncluded._table + '" ia '
                'INNER JOIN "' + Analysis._table + '" a '
                'ON a.id = ia.included_analysis '
            'WHERE ia.analysis = %s '
                'AND a.behavior != \'internal_relation\'',
            (set_group_id,))
        included_analysis = cursor.fetchall()
        if not included_analysis:
            return False
        for analysis in included_analysis:
            if (analysis[1] == 'analysis' and analysis[0] not in
                    analyzes_list):
                return False
            if (analysis[1] != 'analysis' and not
                    Planification._get_set_group_available(analysis[0],
                    analyzes_list)):
                return False
        return True

    @classmethod
    def create(cls, vlist):
        pool = Pool()
        Config = pool.get('lims.configuration')

        vlist = [x.copy() for x in vlist]
        config = Config(1)
        for values in vlist:
            values['code'] = config.planification_sequence.get()
        return super().create(vlist)

    @classmethod
    def check_delete(cls, planifications):
        for planification in planifications:
            if planification.state not in ['draft', 'preplanned']:
                raise UserError(gettext('lims.msg_delete_planification',
                    planification=planification.rec_name))

    @classmethod
    def delete(cls, planifications):
        cls.check_delete(planifications)
        super().delete(planifications)

    @classmethod
    def copy(cls, planifications, default=None):
        raise UserError(gettext('lims.msg_copy_planification'))

    @classmethod
    @ModelView.button_action('lims.wiz_lims_add_analysis')
    def add_analysis(cls, planifications):
        pass

    @classmethod
    @ModelView.button_action('lims.wiz_lims_search_fractions')
    def search_fractions(cls, planifications):
        pass

    @classmethod
    @ModelView.button_action('lims.wiz_lims_search_planned_fractions')
    def search_planned_fractions(cls, planifications):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('preplanned')
    def preplan(cls, planifications):
        for planification in planifications:
            planification.check_start_date()

    @classmethod
    @ModelView.button_action('lims.wiz_lims_technicians_qualification')
    def confirm(cls, planifications):
        for planification in planifications:
            planification.check_wizard_executed()
            planification.check_start_date()
            planification.check_technicians()

    def check_wizard_executed(self):
        if self.wizard_executed:
            raise UserError(gettext('lims.msg_wizard_executed'))
        self.wizard_executed = True
        self.save()

    def check_start_date(self):
        if not self.start_date:
            raise UserError(gettext('lims.msg_not_start_date'))
        for detail in self.details:
            if detail.fraction.sample.date2 > self.start_date:
                raise UserError(gettext('lims.msg_invalid_start_date',
                    date=detail.fraction.sample.date2,
                    sample=detail.fraction.sample.rec_name))

    def check_technicians(self):
        fractions = {}
        for detail in self.details:
            for service_detail in detail.details:
                if not service_detail.staff_responsible:
                    key = (detail.fraction.id,
                        service_detail.notebook_line.method.id)
                    if key not in fractions:
                        fractions[key] = '%s (%s)' % (detail.fraction.rec_name,
                            service_detail.notebook_line.method.code)
        if fractions:
            sorted_fractions = sorted(
                list(fractions.values()), key=lambda x: x)
            raise UserError(gettext('lims.msg_no_technician',
                fractions='\n' + '\n'.join(sorted_fractions) + '\n'))

    @classmethod
    def process_waiting_planifications(cls):
        '''
        Cron - Process Waiting Planifications
        '''
        logger = logging.getLogger('lims_planification')

        planifications = cls.search([
            ('waiting_process', '=', True),
            ], order=[('id', 'ASC')])
        if planifications:
            logger.info('Cron - Processing planifications:INIT')
            for planification in planifications:
                if planification.state == 'confirmed':
                    cls.do_confirm([planification])
                elif planification.state == 'not_executed':
                    cls.do_release_controls([planification])
            logger.info('Cron - Processing planifications:END')

    @classmethod
    def do_confirm(cls, planifications):
        for planification in planifications:
            planification.update_laboratory_notebook()
            if planification.waiting_process:
                planification.waiting_process = False
                planification.save()

    def pre_update_laboratory_notebook(self):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationDetail = pool.get('lims.planification.detail')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        NotebookLine = pool.get('lims.notebook.line')

        cursor.execute('SELECT sd.notebook_line '
            'FROM "' + PlanificationServiceDetail._table + '" sd '
                'INNER JOIN "' + PlanificationDetail._table + '" pd '
                'ON pd.id = sd.detail '
            'WHERE pd.planification = %s '
                'AND sd.notebook_line IS NOT NULL',
            (self.id,))
        notebook_lines = NotebookLine.browse(x[0]
            for x in cursor.fetchall())
        if notebook_lines:
            NotebookLine.write(notebook_lines, {
                'start_date': self.start_date,
                'planification': self.id,
                })

    def update_laboratory_notebook(self):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationDetail = pool.get('lims.planification.detail')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        ServiceDetailProfessional = pool.get(
            'lims.planification.service_detail-laboratory.professional')
        NotebookLineProfessional = pool.get(
            'lims.notebook.line-laboratory.professional')
        NotebookLineControl = pool.get('lims.notebook.line-fraction')

        # Professionals
        cursor.execute('SELECT sd.notebook_line, sdp.professional '
            'FROM "' + ServiceDetailProfessional._table + '" sdp '
                'INNER JOIN "' + PlanificationServiceDetail._table + '" sd '
                'ON sd.id = sdp.detail '
                'INNER JOIN "' + PlanificationDetail._table + '" pd '
                'ON pd.id = sd.detail '
            'WHERE pd.planification = %s '
                'AND sd.notebook_line IS NOT NULL',
            (self.id,))
        res = cursor.fetchall()
        if res:
            notebook_lines_ids = ', '.join(str(nl_id)
                for nl_id, sd_id in res)
            cursor.execute('DELETE FROM "' +
                NotebookLineProfessional._table + '" '
                'WHERE notebook_line IN (' + notebook_lines_ids + ')')

            to_create = []
            for notebook_line, professional in res:
                to_create.append({
                    'notebook_line': notebook_line,
                    'professional': professional,
                    })
            NotebookLineProfessional.create(to_create)

        # Controls
        controls = [f.id for f in self.controls]
        if controls:
            cursor.execute('SELECT sd.notebook_line '
                'FROM "' + PlanificationServiceDetail._table + '" sd '
                    'INNER JOIN "' + PlanificationDetail._table + '" pd '
                    'ON pd.id = sd.detail '
                'WHERE pd.planification = %s '
                    'AND sd.notebook_line IS NOT NULL '
                    'AND sd.is_control = FALSE',
                (self.id,))
            res = [x[0] for x in cursor.fetchall()]
            if res:
                notebook_lines_ids = ', '.join(str(nl_id)
                    for nl_id in res)
                cursor.execute('DELETE FROM "' +
                    NotebookLineControl._table + '" '
                    'WHERE notebook_line IN (' + notebook_lines_ids + ')')

                to_create = []
                for notebook_line in res:
                    for fraction in controls:
                        to_create.append({
                            'notebook_line': notebook_line,
                            'fraction': fraction,
                            })
                NotebookLineControl.create(to_create)

    def update_analysis_detail(self):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationDetail = pool.get('lims.planification.detail')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        NotebookLine = pool.get('lims.notebook.line')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')

        cursor.execute('SELECT nl.analysis_detail '
            'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + PlanificationServiceDetail._table + '" sd '
                'ON nl.id = sd.notebook_line '
                'INNER JOIN "' + PlanificationDetail._table + '" pd '
                'ON pd.id = sd.detail '
            'WHERE pd.planification = %s '
                'AND nl.analysis_detail IS NOT NULL',
            (self.id,))
        analysis_details = EntryDetailAnalysis.browse(x[0]
            for x in cursor.fetchall())
        if analysis_details:
            EntryDetailAnalysis.write(analysis_details, {'state': 'planned'})

    @classmethod
    @ModelView.button
    @Workflow.transition('not_executed')
    def release_controls(cls, planifications):
        for planification in planifications:
            # Check if is still waiting for confirmation
            if planification.waiting_process:
                raise UserError(gettext('lims.msg_waiting_process',
                    planification=planification.code))
        cls.__queue__.do_release_controls(planifications)

    @classmethod
    def do_release_controls(cls, planifications):
        for planification in planifications:
            planification.re_update_laboratory_notebook()
            planification.re_update_analysis_detail()
            planification.unlink_controls()
            if planification.waiting_process:
                planification.waiting_process = False
                planification.save()

    def re_update_laboratory_notebook(self):
        NotebookLine = Pool().get('lims.notebook.line')
        for detail in self.details:
            for service_detail in detail.details:
                if service_detail.is_control and service_detail.notebook_line:
                    notebook_line = NotebookLine(
                        service_detail.notebook_line.id)
                    notebook_line.start_date = None
                    notebook_line.laboratory_professionals = []
                    notebook_line.planification = None
                    notebook_line.controls = []
                    notebook_line.save()

    def re_update_analysis_detail(self):
        EntryDetailAnalysis = Pool().get('lims.entry.detail.analysis')
        analysis_detail_ids = []
        for detail in self.details:
            for service_detail in detail.details:
                if (service_detail.is_control and
                        service_detail.notebook_line and
                        service_detail.notebook_line.analysis_detail):
                    analysis_detail_ids.append(
                        service_detail.notebook_line.analysis_detail.id)
        analysis_details = EntryDetailAnalysis.search([
            ('id', 'in', analysis_detail_ids),
            ])
        if analysis_details:
            EntryDetailAnalysis.write(analysis_details, {
                'state': 'unplanned',
                })

    def unlink_controls(self):
        pool = Pool()
        PlanificationFraction = pool.get('lims.planification-fraction')
        PlanificationDetail = pool.get('lims.planification.detail')

        controls = PlanificationFraction.search([
            ('planification', '=', self.id),
            ])
        if controls:
            PlanificationFraction.delete(controls)

        controls_details = PlanificationDetail.search([
            ('planification', '=', self.id),
            ('details.is_control', '=', True),
            ])
        if controls_details:
            PlanificationDetail.delete(controls_details)

    @fields.depends('analysis')
    def on_change_with_method_domain(self, name=None):
        methods = []
        if self.analysis:
            for a in self.analysis:
                if a.methods:
                    methods.extend([m.id for m in a.methods])
        return methods

    @classmethod
    def set_method_domain(cls, records, name, value):
        return

    @fields.depends('laboratory')
    def on_change_with_technicians_domain(self, name=None):
        pool = Pool()
        UserLaboratory = pool.get('lims.user-laboratory')
        LaboratoryProfessional = pool.get('lims.laboratory.professional')

        if not self.laboratory:
            return []
        users = UserLaboratory.search([
            ('laboratory', '=', self.laboratory.id),
            ])
        if not users:
            return []
        professionals = LaboratoryProfessional.search([
            ('party.lims_user', 'in', [u.user.id for u in users]),
            ])
        if not professionals:
            return []
        return [p.id for p in professionals]

    @classmethod
    def set_technicians_domain(cls, records, name, value):
        return

    @classmethod
    @ModelView.button_action('lims.wiz_lims_relate_technicians')
    def relate_technicians(cls, planifications):
        pass

    @classmethod
    @ModelView.button_action('lims.wiz_lims_unlink_technicians')
    def unlink_technicians(cls, planifications):
        pass

    @classmethod
    @ModelView.button_action('lims.wiz_lims_replace_technician')
    def replace_technician(cls, planifications):
        pass

    @classmethod
    @ModelView.button_action('lims.wiz_lims_add_fraction_con')
    def add_fraction_con(cls, planifications):
        pass

    @classmethod
    @ModelView.button_action('lims.wiz_lims_add_fraction_rm_bmz')
    def add_fraction_rm_bmz(cls, planifications):
        pass

    @classmethod
    @ModelView.button_action('lims.wiz_lims_add_fraction_bre')
    def add_fraction_bre(cls, planifications):
        pass

    @classmethod
    @ModelView.button_action('lims.wiz_lims_add_fraction_mrt')
    def add_fraction_mrt(cls, planifications):
        pass

    @classmethod
    @ModelView.button_action('lims.wiz_lims_remove_control')
    def remove_control(cls, planifications):
        pass


class PlanificationTechnician(ModelSQL, ModelView):
    'Technician'
    __name__ = 'lims.planification.technician'

    planification = fields.Many2One('lims.planification', 'Planification',
        ondelete='CASCADE', select=True, required=True)
    laboratory_professional = fields.Many2One('lims.laboratory.professional',
        'Laboratory professional', required=True, domain=[
            ('id', 'in', Eval('_parent_planification',
                {}).get('technicians_domain')),
            ])
    details = fields.Function(fields.One2Many(
        'lims.planification.technician.detail', 'technician',
        'Fractions to plan', readonly=True),
        'get_details', setter='set_details')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('planification_professional_uniq',
                Unique(t, t.planification, t.laboratory_professional),
                'lims.msg_planification_professional_unique_id'),
            ]

    def get_details(self, name=None):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationDetailProfessional = pool.get(
            'lims.planification.service_detail-laboratory.professional')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        PlanificationDetail = pool.get('lims.planification.detail')
        NotebookLine = pool.get('lims.notebook.line')
        Fraction = pool.get('lims.fraction')
        LabMethod = pool.get('lims.lab.method')

        cursor.execute('SELECT DISTINCT(f.number, nl.analysis_origin, '
                'm.code||\' - \'||m.name) '
            'FROM "' + PlanificationDetailProfessional._table + '" sdp '
                'INNER JOIN "' + PlanificationServiceDetail._table + '" sd '
                'ON sd.id = sdp.detail '
                'INNER JOIN "' + PlanificationDetail._table + '" d '
                'ON d.id = sd.detail '
                'INNER JOIN "' + NotebookLine._table + '" nl '
                'ON nl.id = sd.notebook_line '
                'INNER JOIN "' + LabMethod._table + '" m '
                'ON m.id = nl.method '
                'INNER JOIN "' + Fraction._table + '" f '
                'ON f.id = d.fraction '
            'WHERE sdp.professional = %s '
                'AND d.planification = %s',
            (self.laboratory_professional.id, self.planification.id))

        fractions = []
        for d in cursor.fetchall():
            r = d[0].split(',')
            fractions.append({
                'fraction': str(r[0][1:]),
                'analysis_origin': str(r[1]).replace('"', ''),
                'method': str(','.join(r[2:])[:-1].replace('"', '')),
                })
        return fractions

    @classmethod
    def set_details(cls, records, name, value):
        return


class PlanificationTechnicianDetail(ModelView):
    'Technician Detail'
    __name__ = 'lims.planification.technician.detail'

    fraction = fields.Char('Fraction')
    analysis_origin = fields.Char('Analysis origin')
    method = fields.Char('Method')


class PlanificationDetail(ModelSQL, ModelView):
    'Fraction to Plan'
    __name__ = 'lims.planification.detail'

    planification = fields.Many2One('lims.planification', 'Planification',
        ondelete='CASCADE', select=True, required=True)
    fraction = fields.Many2One('lims.fraction', 'Fraction', required=True,
        select=True)
    service_analysis = fields.Many2One('lims.analysis', 'Service',
        required=True, select=True)
    fraction_type = fields.Function(fields.Many2One('lims.fraction.type',
        'Fraction type'), 'get_fraction_field',
        searcher='search_fraction_field')
    label = fields.Function(fields.Char('Label'), 'get_fraction_field',
        searcher='search_fraction_field')
    product_type = fields.Function(fields.Many2One('lims.product.type',
        'Product type'), 'get_fraction_field',
        searcher='search_fraction_field')
    matrix = fields.Function(fields.Many2One('lims.matrix', 'Matrix'),
        'get_fraction_field', searcher='search_fraction_field')
    details = fields.One2Many('lims.planification.service_detail',
        'detail', 'Planification detail', states={'readonly': True})
    urgent = fields.Function(fields.Boolean('Urgent'), 'get_service_field',
        setter='set_urgent')
    priority = fields.Function(fields.Integer('Priority'), 'get_service_field')
    laboratory_date = fields.Function(fields.Date('Laboratory deadline'),
        'get_service_field')
    report_date = fields.Function(fields.Date('Date agreed for result'),
        'get_service_field')
    comments = fields.Function(fields.Text('Comments'), 'get_fraction_field')
    icon = fields.Function(fields.Char("Icon"), 'get_icon')
    laboratory = fields.Function(fields.Many2One('lims.laboratory',
        'Laboratory'), 'get_planification_field',
        searcher='search_planification_field')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('fraction', 'ASC'))
        cls._order.insert(1, ('service_analysis', 'ASC'))

    @classmethod
    def get_fraction_field(cls, details, names):
        result = {}
        for name in names:
            result[name] = {}
            if name == 'fraction_type':
                for d in details:
                    field = getattr(d.fraction, 'type', None)
                    result[name][d.id] = field.id if field else None
            elif cls._fields[name]._type == 'many2one':
                for d in details:
                    field = getattr(d.fraction, name, None)
                    result[name][d.id] = field.id if field else None
            else:
                for d in details:
                    result[name][d.id] = getattr(d.fraction, name, None)
        return result

    @classmethod
    def search_fraction_field(cls, name, clause):
        if name == 'fraction_type':
            name = 'type'
        return [('fraction.' + name,) + tuple(clause[1:])]

    def _order_sample_field(name):
        def order_field(tables):
            pool = Pool()
            Sample = pool.get('lims.sample')
            Fraction = pool.get('lims.fraction')
            field = Sample._fields[name]
            table, _ = tables[None]
            fraction_tables = tables.get('fraction')
            if fraction_tables is None:
                fraction = Fraction.__table__()
                fraction_tables = {
                    None: (fraction, fraction.id == table.fraction),
                    }
                tables['fraction'] = fraction_tables
            return field.convert_order(name, fraction_tables, Fraction)
        return staticmethod(order_field)
    order_product_type = _order_sample_field('product_type')
    order_matrix = _order_sample_field('matrix')

    @classmethod
    def get_service_field(cls, details, names):
        result = {}
        for name in names:
            result[name] = {}
            if name == 'urgent':
                for d in details:
                    result[name][d.id] = False
            elif name == 'priority':
                for d in details:
                    result[name][d.id] = 0
            else:
                for d in details:
                    result[name][d.id] = None
        for d in details:
            if d.fraction and d.service_analysis:
                for service in d.fraction.services:
                    if service.analysis == d.service_analysis:
                        for name in names:
                            result[name][d.id] = getattr(service, name)
        return result

    @classmethod
    def set_urgent(cls, details, name, value):
        pool = Pool()
        Service = pool.get('lims.service')
        NotebookLine = pool.get('lims.notebook.line')

        services_to_write = []
        notebook_lines_to_write = []
        for d in details:
            if d.fraction and d.service_analysis:
                for service in d.fraction.services:
                    if service.analysis == d.service_analysis:
                        services_to_write.append(service)
            for sd in d.details:
                notebook_lines_to_write.append(sd.notebook_line)

        if services_to_write:
            Service.write(services_to_write, {'urgent': value})
        if notebook_lines_to_write and value:
            NotebookLine.write(notebook_lines_to_write, {'urgent': value})

    def get_icon(self, name):
        if self.comments:
            return 'lims-blue'
        return 'lims-white'

    @classmethod
    def get_planification_field(cls, details, names):
        result = {}
        for name in names:
            result[name] = {}
            if cls._fields[name]._type == 'many2one':
                for d in details:
                    field = getattr(d.planification, name, None)
                    result[name][d.id] = field.id if field else None
            else:
                for d in details:
                    result[name][d.id] = getattr(d.planification, name, None)
        return result

    @classmethod
    def search_planification_field(cls, name, clause):
        return [('planification.' + name,) + tuple(clause[1:])]


class PlanificationServiceDetail(ModelSQL, ModelView):
    'Planification Detail'
    __name__ = 'lims.planification.service_detail'

    detail = fields.Many2One('lims.planification.detail', 'Planification',
        ondelete='CASCADE', select=True, required=True)
    planification = fields.Function(fields.Many2One('lims.planification',
        'Planification'), 'get_planification', searcher='search_planification')
    notebook_line = fields.Many2One('lims.notebook.line', 'Notebook line',
        required=True, select=True)
    staff_responsible = fields.Many2Many(
        'lims.planification.service_detail-laboratory.professional', 'detail',
        'professional', 'Laboratory professionals')
    is_control = fields.Boolean('Is Control', select=True)
    is_replanned = fields.Boolean('Is Replanned', select=True)
    planned_service = fields.Many2One('lims.analysis', 'Planned service')
    repetition = fields.Function(fields.Integer('Repetition'),
        'get_notebook_line_field')

    @staticmethod
    def default_is_control():
        return False

    @staticmethod
    def default_is_replanned():
        return False

    def get_planification(self, name=None):
        if self.detail:
            if self.detail.planification:
                return self.detail.planification.id
        return None

    @classmethod
    def search_planification(cls, name, clause):
        return [('detail.' + name,) + tuple(clause[1:])]

    @classmethod
    def get_notebook_line_field(cls, details, names):
        result = {}
        for name in names:
            result[name] = {}
            if cls._fields[name]._type == 'many2one':
                for d in details:
                    if d.notebook_line:
                        field = getattr(d.notebook_line, name, None)
                        result[name][d.id] = field.id if field else None
                    else:
                        result[name][d.id] = None
            elif cls._fields[name]._type == 'boolean':
                for d in details:
                    if d.notebook_line:
                        result[name][d.id] = getattr(d.notebook_line,
                            name, False)
                    else:
                        result[name][d.id] = False
            else:
                for d in details:
                    if d.notebook_line:
                        result[name][d.id] = getattr(d.notebook_line,
                            name, None)
                    else:
                        result[name][d.id] = None
        return result


class PlanificationServiceDetailLaboratoryProfessional(ModelSQL):
    'Planification Detail - Laboratory Professional'
    __name__ = 'lims.planification.service_detail-laboratory.professional'
    _table = 'lims_plan_service_d-laboratory_professional'

    detail = fields.Many2One('lims.planification.service_detail',
        'Planification detail', ondelete='CASCADE', select=True,
        required=True)
    professional = fields.Many2One('lims.laboratory.professional',
        'Laboratory professional', ondelete='CASCADE', select=True,
        required=True)


class PlanificationAnalysis(ModelSQL):
    'Planification - Analysis'
    __name__ = 'lims.planification-analysis'

    planification = fields.Many2One('lims.planification', 'Planification',
        ondelete='CASCADE', select=True, required=True)
    analysis = fields.Many2One('lims.analysis', 'Analysis',
        ondelete='CASCADE', select=True, required=True)


class PlanificationFraction(ModelSQL):
    'Planification - Fraction'
    __name__ = 'lims.planification-fraction'

    planification = fields.Many2One('lims.planification', 'Planification',
        ondelete='CASCADE', select=True, required=True)
    fraction = fields.Many2One('lims.fraction', 'Fraction',
        ondelete='CASCADE', select=True, required=True)


class NotebookLineFraction(ModelSQL):
    'Laboratory Notebook Line - Fraction'
    __name__ = 'lims.notebook.line-fraction'

    notebook_line = fields.Many2One('lims.notebook.line', 'Notebook Line',
        ondelete='CASCADE', select=True, required=True)
    fraction = fields.Many2One('lims.fraction', 'Fraction',
        ondelete='CASCADE', select=True, required=True)


class FractionReagent(ModelSQL, ModelView):
    'Fraction Reagent'
    __name__ = 'lims.fraction.reagent'

    fraction = fields.Many2One('lims.fraction', 'Fraction', required=True,
        ondelete='CASCADE', select=True)
    product = fields.Many2One('product.product', 'Reagent', required=True,
        domain=[('account_category', 'in', Eval('reagent_domain'))],
        depends=['reagent_domain'])
    reagent_domain = fields.Function(fields.Many2Many('product.category',
        None, None, 'Reagent domain'), 'get_reagent_domain')
    lot = fields.Many2One('stock.lot', 'Lot',
        domain=[('product', '=', Eval('product'))],
        depends=['product'])
    quantity = fields.Function(fields.Float('Current quantity'),
        'on_change_with_quantity')
    default_uom = fields.Function(fields.Many2One('product.uom',
        'Default UOM'),
        'on_change_with_default_uom')

    @staticmethod
    def default_reagent_domain():
        Config = Pool().get('lims.configuration')
        config = Config(1)
        return config.get_reagents()

    def get_reagent_domain(self, name=None):
        return self.default_reagent_domain()

    @fields.depends('product', '_parent_product.quantity')
    def on_change_with_quantity(self, name=None):
        pool = Pool()
        Location = pool.get('stock.location')
        Product = pool.get('product.product')
        Date = pool.get('ir.date')

        if not self.product:
            return 0
        locations = Location.search([('type', '=', 'storage')])
        with Transaction().set_context(locations=[l.id for l in locations],
                stock_date_end=Date.today()):
            product = Product(self.product.id)
        return product.quantity or 0

    @fields.depends('product', '_parent_product.default_uom')
    def on_change_with_default_uom(self, name=None):
        if self.product:
            return self.product.default_uom.id


class LabProfessionalMethod(ModelSQL, ModelView):
    'Laboratory Professional Method'
    __name__ = 'lims.lab.professional.method'

    professional = fields.Many2One('lims.laboratory.professional',
        'Professional', required=True, select=True)
    method = fields.Many2One('lims.lab.method', 'Method', required=True,
        select=True)
    state = fields.Selection([
        ('training', 'Training'),
        ('qualified', 'Qualified'),
        ('requalified', 'Requalified'),
        ], 'State', sort=False)
    type = fields.Selection([
        ('preparation', 'Preparation'),
        ('analytical', 'Analytical'),
        ], 'Type', sort=False, select=True)
    requalification_history = fields.One2Many(
        'lims.lab.professional.method.requalification', 'professional_method',
        'Trainings/Qualifications/Requalifications')
    determination = fields.Function(fields.Char('Determination'),
        'get_determination', searcher='search_determination')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('professional', 'ASC'))
        cls._order.insert(1, ('method', 'ASC'))
        t = cls.__table__()
        cls._sql_constraints += [
            ('professional_method_type_uniq',
                Unique(t, t.professional, t.method, t.type),
                'lims.msg_professional_method_unique_id'),
            ]

    def get_determination(self, name=None):
        if self.method:
            return self.method.determination
        return None

    @classmethod
    def search_determination(cls, name, clause):
        return [('method.' + name,) + tuple(clause[1:])]


class LabProfessionalMethodRequalification(ModelSQL, ModelView):
    'Laboratory Professional Method Requalification'
    __name__ = 'lims.lab.professional.method.requalification'
    _table = 'lims_lab_pro_method_req'

    professional_method = fields.Many2One('lims.lab.professional.method',
        'Professional Method', ondelete='CASCADE', select=True, required=True)
    type = fields.Selection([
        ('training', 'Training'),
        ('qualification', 'Qualification'),
        ('requalification', 'Requalification'),
        ], 'Type', sort=False)
    date = fields.Date('Date', required=True)
    last_execution_date = fields.Date('Last execution Date')
    supervisors = fields.One2Many(
        'lims.lab.professional.method.requalification.supervisor',
        'method_requalification', 'Supervisors')
    controls = fields.One2Many(
        'lims.lab.professional.method.requalification.control',
        'method_requalification', 'Controls')


class LabProfessionalMethodRequalificationSupervisor(ModelSQL, ModelView):
    'Laboratory Professional Method Requalification Supervisor'
    __name__ = 'lims.lab.professional.method.requalification.supervisor'
    _table = 'lims_lab_pro_method_req_supervisor'

    method_requalification = fields.Many2One(
        'lims.lab.professional.method.requalification',
        'Professional method requalification', ondelete='CASCADE',
        select=True, required=True)
    supervisor = fields.Many2One('lims.laboratory.professional', 'Supervisor',
        required=True)


class LabProfessionalMethodRequalificationControl(ModelSQL, ModelView):
    'Laboratory Professional Method Requalification Control'
    __name__ = 'lims.lab.professional.method.requalification.control'
    _table = 'lims_lab_pro_method_req_control'

    method_requalification = fields.Many2One(
        'lims.lab.professional.method.requalification',
        'Professional method requalification', ondelete='CASCADE',
        select=True, required=True)
    control = fields.Many2One('lims.fraction', 'Control',
        required=True)


class BlindSample(ModelSQL, ModelView):
    'Blind Sample'
    __name__ = 'lims.blind_sample'

    line = fields.Many2One('lims.notebook.line', 'Line', required=True,
        readonly=True, ondelete='CASCADE', select=True)
    entry = fields.Many2One('lims.entry', 'Entry', readonly=True)
    sample = fields.Many2One('lims.sample', 'Sample', readonly=True)
    fraction = fields.Many2One('lims.fraction', 'Fraction', readonly=True)
    service = fields.Many2One('lims.service', 'Service', readonly=True)
    analysis = fields.Many2One('lims.analysis', 'Analysis', readonly=True)
    repetition = fields.Integer('Repetition', readonly=True)
    date = fields.Date('Date', readonly=True)
    original_line = fields.Many2One('lims.notebook.line', 'Original line')
    original_sample = fields.Many2One('lims.sample', 'Original sample',
        readonly=True)
    original_fraction = fields.Many2One('lims.fraction', 'Original fraction',
        readonly=True)
    original_repetition = fields.Integer('Repetition', readonly=True)
    min_value = fields.Char('Minimum value', readonly=True)
    max_value = fields.Char('Maximum value', readonly=True)


class RelateTechniciansStart(ModelView):
    'Relate Technicians'
    __name__ = 'lims.planification.relate_technicians.start'

    exclude_relateds = fields.Boolean('Exclude fractions already related')
    grouping = fields.Selection([
        ('none', 'None'),
        ('origin_method', 'Analysis origin and Method'),
        ('origin', 'Analysis origin'),
        ], 'Grouping', sort=False, required=True)


class RelateTechniciansResult(ModelView):
    'Relate Technicians'
    __name__ = 'lims.planification.relate_technicians.result'

    technicians = fields.Many2Many('lims.laboratory.professional',
        None, None, 'Technicians', required=True,
        domain=[('id', 'in', Eval('technicians_domain'))],
        depends=['technicians_domain'])
    technicians_domain = fields.One2Many('lims.laboratory.professional',
        None, 'Technicians domain')
    grouping = fields.Selection([
        ('none', 'None'),
        ('origin_method', 'Analysis origin and Method'),
        ('origin', 'Analysis origin'),
        ], 'Grouping', sort=False, readonly=True)
    details1 = fields.Many2Many(
        'lims.planification.relate_technicians.detail1', None, None,
        'Fractions to plan', domain=[('id', 'in', Eval('details1_domain'))],
        states={'invisible': Not(Bool(Equal(Eval('grouping'), 'none')))},
        depends=['details1_domain', 'grouping'])
    details1_domain = fields.One2Many(
        'lims.planification.relate_technicians.detail1', None,
        'Fractions domain')
    details2 = fields.Many2Many(
        'lims.planification.relate_technicians.detail2', None, None,
        'Fractions to plan', domain=[('id', 'in', Eval('details2_domain'))],
        states={
            'invisible': Not(Bool(Equal(Eval('grouping'), 'origin_method'))),
        }, depends=['details2_domain', 'grouping'])
    details2_domain = fields.One2Many(
        'lims.planification.relate_technicians.detail2', None,
        'Fractions domain')
    details3 = fields.Many2Many(
        'lims.planification.relate_technicians.detail3', None, None,
        'Fractions to plan', domain=[('id', 'in', Eval('details3_domain'))],
        states={'invisible': Not(Bool(Equal(Eval('grouping'), 'origin')))},
        depends=['details3_domain', 'grouping'])
    details3_domain = fields.One2Many(
        'lims.planification.relate_technicians.detail3', None,
        'Fractions domain')

    @fields.depends('grouping')
    def on_change_grouping(self):
        self.details1 = []
        self.details2 = []
        self.details3 = []


class RelateTechniciansDetail1(ModelSQL, ModelView):
    'Fraction Detail'
    __name__ = 'lims.planification.relate_technicians.detail1'
    _table = 'lims_planification_relate_technicians_d1'

    fraction = fields.Many2One('lims.fraction', 'Fraction')
    service_analysis = fields.Many2One('lims.analysis', 'Service')
    fraction_type = fields.Function(fields.Many2One('lims.fraction.type',
        'Fraction type'), 'get_fraction_field')
    label = fields.Function(fields.Char('Label'), 'get_fraction_field')
    session_id = fields.Integer('Session ID')

    @classmethod
    def __register__(cls, module_name):
        super().__register__(module_name)
        cursor = Transaction().connection.cursor()
        cursor.execute('DELETE FROM "' + cls._table + '"')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('fraction', 'ASC'))
        cls._order.insert(1, ('service_analysis', 'ASC'))

    @classmethod
    def get_fraction_field(cls, details, names):
        result = {}
        for name in names:
            result[name] = {}
            if name == 'label':
                for d in details:
                    result[name][d.id] = getattr(d.fraction, name, None)
            elif name == 'fraction_type':
                for d in details:
                    field = getattr(d.fraction, 'type', None)
                    result[name][d.id] = field.id if field else None
            else:
                for d in details:
                    field = getattr(d.fraction, name, None)
                    result[name][d.id] = field.id if field else None
        return result


class RelateTechniciansDetail2(ModelSQL, ModelView):
    'Fraction Detail'
    __name__ = 'lims.planification.relate_technicians.detail2'
    _table = 'lims_planification_relate_technicians_d2'

    fraction = fields.Many2One('lims.fraction', 'Fraction')
    analysis_origin = fields.Char('Analysis origin')
    method = fields.Many2One('lims.lab.method', 'Method')
    session_id = fields.Integer('Session ID')

    @classmethod
    def __register__(cls, module_name):
        super().__register__(module_name)
        cursor = Transaction().connection.cursor()
        cursor.execute('DELETE FROM "' + cls._table + '"')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('fraction', 'ASC'))
        cls._order.insert(1, ('analysis_origin', 'ASC'))
        cls._order.insert(2, ('method', 'ASC'))


class RelateTechniciansDetail3(ModelSQL, ModelView):
    'Fraction Detail'
    __name__ = 'lims.planification.relate_technicians.detail3'
    _table = 'lims_planification_relate_technicians_d3'

    fraction = fields.Many2One('lims.fraction', 'Fraction')
    analysis_origin = fields.Char('Analysis origin')
    session_id = fields.Integer('Session ID')

    @classmethod
    def __register__(cls, module_name):
        super().__register__(module_name)
        cursor = Transaction().connection.cursor()
        cursor.execute('DELETE FROM "' + cls._table + '"')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('fraction', 'ASC'))
        cls._order.insert(1, ('analysis_origin', 'ASC'))


class RelateTechnicians(Wizard):
    'Relate Technicians'
    __name__ = 'lims.planification.relate_technicians'

    start = StateView('lims.planification.relate_technicians.start',
        'lims.lims_relate_technicians_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Search', 'search', 'tryton-ok', default=True),
            ])
    search = StateTransition()
    result = StateView('lims.planification.relate_technicians.result',
        'lims.lims_relate_technicians_result_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Relate', 'relate', 'tryton-ok', default=True),
            ])
    relate = StateTransition()

    def default_start(self, fields):
        return {
            'exclude_relateds': True,
            'grouping': 'none',
            }

    def transition_search(self):
        planification_id = Transaction().context['active_id']

        self.result.grouping = self.start.grouping
        self.result.details1_domain = []
        self.result.details2_domain = []
        self.result.details3_domain = []

        if self.start.grouping == 'none':
            self.result.details1_domain = self._view_details1(planification_id,
                self.start.exclude_relateds)
        elif self.start.grouping == 'origin_method':
            self.result.details2_domain = self._view_details2(planification_id,
                self.start.exclude_relateds)
        elif self.start.grouping == 'origin':
            self.result.details3_domain = self._view_details3(planification_id,
                self.start.exclude_relateds)
        return 'result'

    def default_result(self, fields):
        Planification = Pool().get('lims.planification')

        planification = Planification(Transaction().context['active_id'])

        details1_domain = []
        if self.result.details1_domain:
            details1_domain = [d.id for d in self.result.details1_domain]
        details2_domain = []
        if self.result.details2_domain:
            details2_domain = [d.id for d in self.result.details2_domain]
        details3_domain = []
        if self.result.details3_domain:
            details3_domain = [d.id for d in self.result.details3_domain]

        return {
            'technicians_domain': [t.laboratory_professional.id
                for t in planification.technicians],
            'details1_domain': details1_domain,
            'details2_domain': details2_domain,
            'details3_domain': details3_domain,
            'grouping': self.start.grouping,
            }

    def _view_details1(self, planification_id, exclude_relateds):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationDetail = pool.get(
            'lims.planification.detail')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        ServiceDetailProfessional = pool.get(
            'lims.planification.service_detail-laboratory.professional')
        RelateTechniciansDetail1 = pool.get(
            'lims.planification.relate_technicians.detail1')

        exclude_relateds_clause = ''
        if exclude_relateds:
            exclude_relateds_clause = (' AND sd.id NOT IN ('
                'SELECT sdp.detail '
                'FROM "' + ServiceDetailProfessional._table + '" sdp '
                    'INNER JOIN "' + PlanificationServiceDetail._table + '" sd'
                    ' ON sdp.detail = sd.id '
                    'INNER JOIN "' + PlanificationDetail._table + '" d'
                    ' ON sd.detail = d.id '
                 'WHERE d.planification = %s'
                ')' % planification_id)

        details1 = {}
        cursor.execute('SELECT d.fraction, d.service_analysis, sd.id '
            'FROM "' + PlanificationDetail._table + '" d '
                'INNER JOIN "' + PlanificationServiceDetail._table + '" sd '
                    'ON sd.detail = d.id '
            'WHERE d.planification = %s' +
            exclude_relateds_clause,
            (planification_id,))
        for x in cursor.fetchall():
            f, s = x[0], x[1]
            if (f, s) not in details1:
                details1[(f, s)] = {
                    'fraction': f,
                    'service_analysis': s,
                    }

        to_create = []
        for d in details1.values():
            to_create.append({
                'session_id': self._session_id,
                'fraction': d['fraction'],
                'service_analysis': d['service_analysis'],
                })
        return RelateTechniciansDetail1.create(to_create)

    def _view_details2(self, planification_id, exclude_relateds):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationDetail = pool.get(
            'lims.planification.detail')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        ServiceDetailProfessional = pool.get(
            'lims.planification.service_detail-laboratory.professional')
        NotebookLine = pool.get(
            'lims.notebook.line')
        RelateTechniciansDetail2 = pool.get(
            'lims.planification.relate_technicians.detail2')

        exclude_relateds_clause = ''
        if exclude_relateds:
            exclude_relateds_clause = (' AND sd.id NOT IN ('
                'SELECT sdp.detail '
                'FROM "' + ServiceDetailProfessional._table + '" sdp '
                    'INNER JOIN "' + PlanificationServiceDetail._table + '" sd'
                    ' ON sdp.detail = sd.id '
                    'INNER JOIN "' + PlanificationDetail._table + '" d'
                    ' ON sd.detail = d.id '
                 'WHERE d.planification = %s'
                ')' % planification_id)

        details2 = {}
        cursor.execute('SELECT d.fraction, nl.analysis_origin, nl.method, '
                'sd.id '
            'FROM "' + PlanificationDetail._table + '" d '
                'INNER JOIN "' + PlanificationServiceDetail._table + '" sd '
                    'ON sd.detail = d.id '
                'INNER JOIN "' + NotebookLine._table + '" nl '
                    'ON sd.notebook_line = nl.id '
            'WHERE d.planification = %s' +
            exclude_relateds_clause,
            (planification_id,))
        for x in cursor.fetchall():
            f, a, m = x[0], x[1], x[2]
            if (f, a, m) not in details2:
                details2[(f, a, m)] = {
                    'fraction': f,
                    'analysis_origin': a,
                    'method': m,
                    }

        to_create = []
        for d in details2.values():
            to_create.append({
                'session_id': self._session_id,
                'fraction': d['fraction'],
                'analysis_origin': d['analysis_origin'],
                'method': d['method'],
                })
        return RelateTechniciansDetail2.create(to_create)

    def _view_details3(self, planification_id, exclude_relateds):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationDetail = pool.get(
            'lims.planification.detail')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        ServiceDetailProfessional = pool.get(
            'lims.planification.service_detail-laboratory.professional')
        NotebookLine = pool.get(
            'lims.notebook.line')
        RelateTechniciansDetail3 = pool.get(
            'lims.planification.relate_technicians.detail3')

        exclude_relateds_clause = ''
        if exclude_relateds:
            exclude_relateds_clause = (' AND sd.id NOT IN ('
                'SELECT sdp.detail '
                'FROM "' + ServiceDetailProfessional._table + '" sdp '
                    'INNER JOIN "' + PlanificationServiceDetail._table + '" sd'
                    ' ON sdp.detail = sd.id '
                    'INNER JOIN "' + PlanificationDetail._table + '" d'
                    ' ON sd.detail = d.id '
                 'WHERE d.planification = %s'
                ')' % planification_id)

        details3 = {}
        cursor.execute('SELECT d.fraction, nl.analysis_origin, sd.id '
            'FROM "' + PlanificationDetail._table + '" d '
                'INNER JOIN "' + PlanificationServiceDetail._table + '" sd '
                    'ON sd.detail = d.id '
                'INNER JOIN "' + NotebookLine._table + '" nl '
                    'ON sd.notebook_line = nl.id '
            'WHERE d.planification = %s' +
            exclude_relateds_clause,
            (planification_id,))
        for x in cursor.fetchall():
            f, a = x[0], x[1]
            if (f, a) not in details3:
                details3[(f, a)] = {
                    'fraction': f,
                    'analysis_origin': a,
                    }

        to_create = []
        for d in details3.values():
            to_create.append({
                'session_id': self._session_id,
                'fraction': d['fraction'],
                'analysis_origin': d['analysis_origin'],
                })
        return RelateTechniciansDetail3.create(to_create)

    def transition_relate(self):
        PlanificationServiceDetail = Pool().get(
            'lims.planification.service_detail')

        planification_id = Transaction().context['active_id']
        details = self._get_details(planification_id)
        if not details:
            return 'end'

        PlanificationServiceDetail.write(details, {
            'staff_responsible': [('remove',
                [t.id for t in self.result.technicians])],
            })
        PlanificationServiceDetail.write(details, {
            'staff_responsible': [('add',
                [t.id for t in self.result.technicians])],
            })
        return 'end'

    def _get_details(self, planification_id):
        details = []
        if self.start.grouping == 'none':
            details = self._get_details1(planification_id,
                self.start.exclude_relateds)
        elif self.start.grouping == 'origin_method':
            details = self._get_details2(planification_id,
                self.start.exclude_relateds)
        elif self.start.grouping == 'origin':
            details = self._get_details3(planification_id,
                self.start.exclude_relateds)
        return details

    def _get_details1(self, planification_id, exclude_relateds):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationDetail = pool.get(
            'lims.planification.detail')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        ServiceDetailProfessional = pool.get(
            'lims.planification.service_detail-laboratory.professional')

        exclude_relateds_clause = ''
        if exclude_relateds:
            exclude_relateds_clause = (' AND sd.id NOT IN ('
                'SELECT sdp.detail '
                'FROM "' + ServiceDetailProfessional._table + '" sdp '
                    'INNER JOIN "' + PlanificationServiceDetail._table + '" sd'
                    ' ON sdp.detail = sd.id '
                    'INNER JOIN "' + PlanificationDetail._table + '" d'
                    ' ON sd.detail = d.id '
                 'WHERE d.planification = %s'
                ')' % planification_id)

        details = []
        for detail in self.result.details1:
            cursor.execute('SELECT sd.id '
                'FROM "' + PlanificationDetail._table + '" d '
                    'INNER JOIN "' + PlanificationServiceDetail._table + '" sd'
                        ' ON sd.detail = d.id '
                'WHERE d.planification = %s '
                    'AND d.fraction = %s '
                    'AND d.service_analysis = %s' +
                    exclude_relateds_clause,
                (planification_id, detail.fraction.id,
                    detail.service_analysis.id))
            for x in cursor.fetchall():
                details.append(x[0])

        return PlanificationServiceDetail.browse(details)

    def _get_details2(self, planification_id, exclude_relateds):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationDetail = pool.get(
            'lims.planification.detail')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        ServiceDetailProfessional = pool.get(
            'lims.planification.service_detail-laboratory.professional')
        NotebookLine = pool.get(
            'lims.notebook.line')

        exclude_relateds_clause = ''
        if exclude_relateds:
            exclude_relateds_clause = (' AND sd.id NOT IN ('
                'SELECT sdp.detail '
                'FROM "' + ServiceDetailProfessional._table + '" sdp '
                    'INNER JOIN "' + PlanificationServiceDetail._table + '" sd'
                    ' ON sdp.detail = sd.id '
                    'INNER JOIN "' + PlanificationDetail._table + '" d'
                    ' ON sd.detail = d.id '
                 'WHERE d.planification = %s'
                ')' % planification_id)

        details = []
        for detail in self.result.details2:
            cursor.execute('SELECT sd.id '
                'FROM "' + PlanificationDetail._table + '" d '
                    'INNER JOIN "' + PlanificationServiceDetail._table + '" sd'
                        ' ON sd.detail = d.id '
                    'INNER JOIN "' + NotebookLine._table + '" nl '
                        'ON sd.notebook_line = nl.id '
                'WHERE d.planification = %s '
                    'AND d.fraction = %s '
                    'AND nl.analysis_origin = %s '
                    'AND nl.method = %s' +
                    exclude_relateds_clause,
                (planification_id, detail.fraction.id,
                    detail.analysis_origin, detail.method.id))
            for x in cursor.fetchall():
                details.append(x[0])

        return PlanificationServiceDetail.browse(details)

    def _get_details3(self, planification_id, exclude_relateds):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationDetail = pool.get(
            'lims.planification.detail')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        ServiceDetailProfessional = pool.get(
            'lims.planification.service_detail-laboratory.professional')
        NotebookLine = pool.get(
            'lims.notebook.line')

        exclude_relateds_clause = ''
        if exclude_relateds:
            exclude_relateds_clause = (' AND sd.id NOT IN ('
                'SELECT sdp.detail '
                'FROM "' + ServiceDetailProfessional._table + '" sdp '
                    'INNER JOIN "' + PlanificationServiceDetail._table + '" sd'
                    ' ON sdp.detail = sd.id '
                    'INNER JOIN "' + PlanificationDetail._table + '" d'
                    ' ON sd.detail = d.id '
                 'WHERE d.planification = %s'
                ')' % planification_id)

        details = []
        for detail in self.result.details3:
            cursor.execute('SELECT sd.id '
                'FROM "' + PlanificationDetail._table + '" d '
                    'INNER JOIN "' + PlanificationServiceDetail._table + '" sd'
                        ' ON sd.detail = d.id '
                    'INNER JOIN "' + NotebookLine._table + '" nl '
                        'ON sd.notebook_line = nl.id '
                'WHERE d.planification = %s '
                    'AND d.fraction = %s '
                    'AND nl.analysis_origin = %s' +
                    exclude_relateds_clause,
                (planification_id, detail.fraction.id,
                    detail.analysis_origin))
            for x in cursor.fetchall():
                details.append(x[0])

        return PlanificationServiceDetail.browse(details)


class UnlinkTechniciansStart(ModelView):
    'Unlink Technicians Start'
    __name__ = 'lims.planification.unlink_technicians.start'

    technicians = fields.Many2Many('lims.laboratory.professional',
        None, None, 'Technicians', required=True,
        domain=[('id', 'in', Eval('technicians_domain'))],
        depends=['technicians_domain'])
    technicians_domain = fields.One2Many('lims.laboratory.professional',
        None, 'Technicians domain')
    details1 = fields.Many2Many(
        'lims.planification.unlink_technicians.detail1', None, None,
        'Assigned fractions', domain=[('id', 'in', Eval('details1_domain'))],
        depends=['details1_domain'])
    details1_domain = fields.Many2Many(
        'lims.planification.unlink_technicians.detail1', None, None,
        'Fractions domain')


class UnlinkTechniciansDetail1(ModelSQL, ModelView):
    'Fraction Detail Unlink'
    __name__ = 'lims.planification.unlink_technicians.detail1'
    _table = 'lims_planification_unlink_technicians_d1'

    fraction = fields.Many2One('lims.fraction', 'Fraction')
    service_analysis = fields.Many2One('lims.analysis', 'Service')
    fraction_type = fields.Function(fields.Many2One('lims.fraction.type',
        'Fraction type'), 'get_fraction_field')
    label = fields.Function(fields.Char('Label'), 'get_fraction_field')
    session_id = fields.Integer('Session ID')

    @classmethod
    def __register__(cls, module_name):
        super().__register__(module_name)
        cursor = Transaction().connection.cursor()
        cursor.execute('DELETE FROM "' + cls._table + '"')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('fraction', 'ASC'))
        cls._order.insert(1, ('service_analysis', 'ASC'))

    @classmethod
    def get_fraction_field(cls, details, names):
        result = {}
        for name in names:
            result[name] = {}
            if name == 'label':
                for d in details:
                    result[name][d.id] = getattr(d.fraction, name, None)
            elif name == 'fraction_type':
                for d in details:
                    field = getattr(d.fraction, 'type', None)
                    result[name][d.id] = field.id if field else None
            else:
                for d in details:
                    field = getattr(d.fraction, name, None)
                    result[name][d.id] = field.id if field else None
        return result


class UnlinkTechnicians(Wizard):
    'Unlink Technicians'
    __name__ = 'lims.planification.unlink_technicians'

    start = StateView('lims.planification.unlink_technicians.start',
        'lims.lims_unlink_technicians_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Unlink', 'unlink', 'tryton-ok', default=True),
            ])
    unlink = StateTransition()

    def default_start(self, fields):
        Planification = Pool().get('lims.planification')

        planification = Planification(Transaction().context['active_id'])

        details_domain = self._view_details(planification.id)

        return {
            'technicians_domain': [t.laboratory_professional.id
                for t in planification.technicians],
            'details1_domain': [d.id for d in details_domain],
            }

    def _view_details(self, planification_id):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationDetail = pool.get(
            'lims.planification.detail')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        UnlinkTechniciansDetail1 = pool.get(
            'lims.planification.unlink_technicians.detail1')

        details1 = {}
        cursor.execute('SELECT d.fraction, d.service_analysis, sd.id '
            'FROM "' + PlanificationDetail._table + '" d '
                'INNER JOIN "' + PlanificationServiceDetail._table + '" sd '
                    'ON sd.detail = d.id '
            'WHERE d.planification = %s',
            (planification_id,))
        for x in cursor.fetchall():
            f, s = x[0], x[1]
            if (f, s) not in details1:
                details1[(f, s)] = {
                    'fraction': f,
                    'service_analysis': s,
                    }

        to_create = []
        for d in details1.values():
            to_create.append({
                'session_id': self._session_id,
                'fraction': d['fraction'],
                'service_analysis': d['service_analysis'],
                })
        return UnlinkTechniciansDetail1.create(to_create)

    def transition_unlink(self):
        pool = Pool()
        Planification = pool.get('lims.planification')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')

        planification = Planification(Transaction().context['active_id'])

        details = self._get_details(planification.id)

        PlanificationServiceDetail.write(details, {
            'staff_responsible': [('remove',
                [t.id for t in self.start.technicians])],
            })
        return 'end'

    def _get_details(self, planification_id):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationDetail = pool.get(
            'lims.planification.detail')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')

        details = []
        for detail in self.start.details1:
            cursor.execute('SELECT sd.id '
                'FROM "' + PlanificationDetail._table + '" d '
                    'INNER JOIN "' + PlanificationServiceDetail._table + '" sd'
                        ' ON sd.detail = d.id '
                'WHERE d.planification = %s '
                    'AND d.fraction = %s '
                    'AND d.service_analysis = %s',
                (planification_id, detail.fraction.id,
                    detail.service_analysis.id))
            for x in cursor.fetchall():
                details.append(x[0])

        return PlanificationServiceDetail.browse(details)


class AddFractionControlStart(ModelView):
    'Add Fraction Control'
    __name__ = 'lims.planification.add_fraction_con.start'

    planification = fields.Many2One('lims.planification', 'Planification')
    type = fields.Selection([
        ('exist', 'Existing CON'),
        ('coi', 'COI'),
        ('mrc', 'MRC'),
        ('sla', 'SLA'),
        ('itc', 'ITC'),
        ('itl', 'ITL'),
        ], 'Control type', sort=False, required=True)
    original_fraction = fields.Many2One('lims.fraction', 'Original fraction',
        required=True, domain=[('id', 'in', Eval('fraction_domain'))],
        depends=['fraction_domain'])
    fraction_domain = fields.Function(fields.One2Many('lims.fraction',
        None, 'Fraction domain'), 'on_change_with_fraction_domain')
    label = fields.Char('Label', depends=['type'],
        states={'readonly': Eval('type') == 'exist'})
    concentration_level = fields.Many2One('lims.concentration.level',
        'Concentration level', states={
            'invisible': Bool(Eval('concentration_level_invisible')),
            }, depends=['concentration_level_invisible'])
    concentration_level_invisible = fields.Boolean(
        'Concentration level invisible')
    generate_repetition = fields.Boolean('Generate repetition',
        states={'readonly': Eval('type') == 'exist'}, depends=['type'])

    @fields.depends('planification', 'type', '_parent_planification.analysis')
    def on_change_with_fraction_domain(self, name=None):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        Fraction = pool.get('lims.fraction')
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')

        if not self.type:
            return []

        p_analysis_ids = []
        for p_analysis in self.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))

        stored_fractions_ids = Fraction.get_stored_fractions()

        special_type = 'con' if self.type == 'exist' else self.type
        clause = [
            ('notebook.fraction.special_type', '=', special_type),
            ('notebook.fraction.id', 'in', stored_fractions_ids),
            ('analysis', 'in', p_analysis_ids),
            ]
        if self.type == 'exist':
            deadline = datetime.now() - relativedelta(days=5)
            clause.extend([
                ('result', 'in', (None, '')),
                ('end_date', '=', None),
                ('annulment_date', '=', None),
                ('notebook.fraction.sample.date2', '>=', deadline),
                ])
        notebook_lines = NotebookLine.search(clause)
        if not notebook_lines:
            return []

        notebook_lines_ids = ', '.join(str(nl.id) for nl in notebook_lines)
        cursor.execute('SELECT DISTINCT(n.fraction) '
            'FROM "' + Notebook._table + '" n '
                'INNER JOIN "' + NotebookLine._table + '" nl '
                'ON nl.notebook = n.id '
            'WHERE nl.id IN (' + notebook_lines_ids + ')')
        return [x[0] for x in cursor.fetchall()]

    @fields.depends('type', 'original_fraction', 'concentration_level',
        '_parent_original_fraction.label',
        '_parent_concentration_level.description')
    def on_change_with_label(self, name=None):
        if self.type == 'exist':
            return ''
        label = ''
        date = get_print_date().strftime("%Y-%m-%d %H:%M:%S")
        if self.original_fraction:
            label += self.original_fraction.label
        if self.concentration_level:
            label += (' (' +
                    self.concentration_level.description + ')')
        label += ' ' + date
        return label


class AddFractionControl(Wizard):
    'Add Fraction Control'
    __name__ = 'lims.planification.add_fraction_con'

    start = StateView('lims.planification.add_fraction_con.start',
        'lims.lims_add_fraction_con_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Add', 'add', 'tryton-ok', default=True),
            ])
    add = StateTransition()

    def default_start(self, fields):
        Config = Pool().get('lims.configuration')
        config = Config(1)
        defaults = {
            'planification': Transaction().context['active_id'],
            'concentration_level_invisible': True,
            }
        if (config.con_fraction_type and
                config.con_fraction_type.control_charts):
            defaults['concentration_level_invisible'] = False
        return defaults

    def transition_add(self):
        fraction = self.start.original_fraction
        if self.start.type != 'exist':
            fraction = self.create_control()
        self.add_control(fraction)
        self.add_planification_detail(fraction)
        return 'end'

    def create_control(self):
        pool = Pool()
        Config = pool.get('lims.configuration')
        LabWorkYear = pool.get('lims.lab.workyear')
        Entry = pool.get('lims.entry')
        Sample = pool.get('lims.sample')
        Fraction = pool.get('lims.fraction')
        Service = pool.get('lims.service')
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')

        config = Config(1)
        fraction_type = config.con_fraction_type
        if not fraction_type:
            raise UserError(gettext('lims.msg_no_con_fraction_type'))

        if (fraction_type.control_charts and not
                self.start.concentration_level):
            raise UserError(gettext('lims.msg_no_concentration_level'))

        workyear_id = LabWorkYear.find()
        workyear = LabWorkYear(workyear_id)
        if not workyear.default_entry_control:
            raise UserError(gettext('lims.msg_no_entry_control'))

        entry = Entry(workyear.default_entry_control.id)
        original_fraction = self.start.original_fraction
        original_sample = Sample(original_fraction.sample.id)
        obj_description = self._get_obj_description(original_sample)

        # new sample
        new_sample, = Sample.copy([original_sample], default={
            'entry': entry.id,
            'date': datetime.now(),
            'label': self.start.label,
            'obj_description': obj_description,
            'fractions': [],
            })

        # new fraction
        new_fraction, = Fraction.copy([original_fraction], default={
            'sample': new_sample.id,
            'type': fraction_type.id,
            'services': [],
            'con_type': self.start.type,
            'con_original_fraction': original_fraction.id,
            })

        # new services
        p_analysis_ids = []
        for p_analysis in self.start.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))

        services = Service.search([
            ('fraction', '=', original_fraction),
            ('annulled', '=', False),
            ])
        for service in services:
            if not Analysis.is_typified(service.analysis,
                    new_sample.product_type, new_sample.matrix):
                continue

            method_id = service.method and service.method.id or None
            device_id = service.device and service.device.id or None
            if service.analysis.type == 'analysis':
                original_lines = NotebookLine.search([
                    ('notebook.fraction', '=', original_fraction.id),
                    ('analysis', '=', service.analysis.id),
                    ('repetition', '=', 0),
                    ], limit=1)
                original_line = original_lines[0] if original_lines else None
                if original_line:
                    method_id = original_line.method.id
                    if original_line.device:
                        device_id = original_line.device.id

            new_service, = Service.copy([service], default={
                'fraction': new_fraction.id,
                'method': method_id,
                'device': device_id,
                })

            # delete services/details not related to planification
            to_delete = EntryDetailAnalysis.search([
                ('service', '=', new_service.id),
                ('analysis', 'not in', p_analysis_ids),
                ])
            if to_delete:
                with Transaction().set_user(0, set_context=True):
                    EntryDetailAnalysis.delete(to_delete)
            if EntryDetailAnalysis.search_count([
                    ('service', '=', new_service.id),
                    ]) == 0:
                with Transaction().set_user(0, set_context=True):
                    Service.delete([new_service])

        # confirm fraction: new notebook and stock move
        Fraction.confirm([new_fraction])

        # Edit notebook lines
        if fraction_type.control_charts:
            notebook_lines = NotebookLine.search([
                ('notebook.fraction', '=', new_fraction.id),
                ])
            if notebook_lines:
                defaults = {
                    'concentration_level': self.start.concentration_level.id,
                    }
                NotebookLine.write(notebook_lines, defaults)

        # Generate repetition
        if self.start.generate_repetition:
            notebook_lines = NotebookLine.search([
                ('notebook.fraction', '=', new_fraction.id),
                ])
            if notebook_lines:
                self.generate_repetition(notebook_lines)

        return new_fraction

    def _get_obj_description(self, sample):
        cursor = Transaction().connection.cursor()
        ObjectiveDescription = Pool().get('lims.objective_description')

        if not sample.product_type or not sample.matrix:
            return None

        cursor.execute('SELECT id '
            'FROM "' + ObjectiveDescription._table + '" '
            'WHERE product_type = %s '
                'AND matrix = %s',
            (sample.product_type.id, sample.matrix.id))
        res = cursor.fetchone()
        return res and res[0] or None

    def generate_repetition(self, notebook_lines):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        Notebook = pool.get('lims.notebook')

        p_analysis_ids = []
        for p_analysis in self.start.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))

        analysis_to_repeat = {}
        for notebook_line in notebook_lines:
            if notebook_line.analysis.id not in p_analysis_ids:
                continue
            if notebook_line.analysis.id not in analysis_to_repeat:
                analysis_to_repeat[notebook_line.analysis.id] = notebook_line
            elif (notebook_line.repetition >
                    analysis_to_repeat[notebook_line.analysis.id].repetition):
                analysis_to_repeat[notebook_line.analysis.id] = notebook_line

        notebook = Notebook(notebook_lines[0].notebook.id)

        to_create = []
        for analysis_id, nline in analysis_to_repeat.items():
            to_create.append({
                'analysis_detail': nline.analysis_detail.id,
                'service': nline.service.id,
                'analysis': analysis_id,
                'analysis_origin': nline.analysis_origin,
                'urgent': nline.urgent,
                'repetition': nline.repetition + 1,
                'laboratory': nline.laboratory.id,
                'method': nline.method.id,
                'device': nline.device.id if nline.device else None,
                'initial_concentration': nline.initial_concentration,
                'final_concentration': nline.final_concentration,
                'literal_final_concentration': (
                    nline.literal_final_concentration),
                'initial_unit': (nline.initial_unit.id if
                    nline.initial_unit else None),
                'final_unit': (nline.final_unit.id if
                    nline.final_unit else None),
                'detection_limit': nline.detection_limit,
                'quantification_limit': nline.quantification_limit,
                'lower_limit': nline.lower_limit,
                'upper_limit': nline.upper_limit,
                'decimals': nline.decimals,
                'significant_digits': nline.significant_digits,
                'scientific_notation': nline.scientific_notation,
                'report': nline.report,
                'concentration_level': (nline.concentration_level.id if
                    nline.concentration_level else None),
                'results_estimated_waiting': nline.results_estimated_waiting,
                'department': (nline.department.id if
                    nline.department else None),
                })
        Notebook.write([notebook], {
            'lines': [('create', to_create)],
            })

    def add_control(self, fraction):
        Planification = Pool().get('lims.planification')
        Planification.write([self.start.planification], {
            'controls': [('add', [fraction.id])],
            })

    def add_planification_detail(self, fraction):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')
        PlanificationDetail = pool.get('lims.planification.detail')
        Service = pool.get('lims.service')

        p_analysis_ids = []
        for p_analysis in self.start.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))
        clause = [
            ('notebook.fraction', '=', fraction.id),
            ('analysis', 'in', p_analysis_ids),
            ('analysis.behavior', '!=', 'internal_relation'),
            ]
        if self.start.type == 'exist':
            clause.extend([
                ('result', 'in', (None, '')),
                ('end_date', '=', None),
                ('annulment_date', '=', None),
                ])
        else:
            clause.append(('planification', '=', None))
        notebook_lines = NotebookLine.search(clause)
        if notebook_lines:
            details_to_create = {}
            for nl in notebook_lines:
                f = nl.notebook.fraction.id
                s = nl.service.analysis.id
                if (f, s) not in details_to_create:
                    details_to_create[(f, s)] = []
                details_to_create[(f, s)].append({
                    'notebook_line': nl.id,
                    'planned_service': s,
                    'is_control': True,
                    })
            if details_to_create:
                for k, v in details_to_create.items():
                    details = PlanificationDetail.search([
                        ('planification', '=', self.start.planification.id),
                        ('fraction', '=', k[0]),
                        ('service_analysis', '=', k[1]),
                        ])
                    if details:
                        PlanificationDetail.write([details[0]], {
                            'details': [('create', v)],
                            })
                    else:
                        PlanificationDetail.create([{
                            'planification': self.start.planification.id,
                            'fraction': k[0],
                            'service_analysis': k[1],
                            'urgent': Service.is_service_urgent(k[0], k[1]),
                            'details': [('create', v)],
                            }])


class AddFractionRMBMZStart(ModelView):
    'Add Fraction RM/BMZ'
    __name__ = 'lims.planification.add_fraction_rm_bmz.start'

    planification = fields.Many2One('lims.planification', 'Planification')
    type = fields.Selection([
        ('rm', 'RM'),
        ('bmz', 'BMZ'),
        ], 'Control type', sort=False, required=True)
    rm_bmz_type = fields.Selection([
        ('sla', 'SLA'),
        ('noref', 'No Reference'),
        ('exist', 'Existing RM/BMZ'),
        ], 'RM/BMZ type', sort=False, required=True)
    reference_fraction = fields.Many2One('lims.fraction',
        'Reference fraction', depends=['fraction_domain', 'rm_bmz_type'],
        states={
            'readonly': Bool(Equal(Eval('rm_bmz_type'), 'noref')),
            'required': Not(Bool(Equal(Eval('rm_bmz_type'), 'noref'))),
        }, domain=[('id', 'in', Eval('fraction_domain'))])
    fraction_domain = fields.Function(fields.One2Many('lims.fraction',
        None, 'Fraction domain'), 'on_change_with_fraction_domain')
    product_type = fields.Many2One('lims.product.type', 'Product type',
        states={
            'readonly': Not(Bool(Equal(Eval('rm_bmz_type'), 'noref'))),
            'required': Bool(Equal(Eval('rm_bmz_type'), 'noref'))},
        domain=[('id', 'in', Eval('product_type_domain'))],
        depends=['rm_bmz_type', 'product_type_domain'])
    product_type_domain = fields.Function(fields.Many2Many(
        'lims.product.type', None, None, 'Product type domain'),
        'on_change_with_product_type_domain')
    matrix = fields.Many2One('lims.matrix', 'Matrix', required=True,
        states={
            'readonly': Not(Bool(Equal(Eval('rm_bmz_type'), 'noref'))),
            'required': Bool(Equal(Eval('rm_bmz_type'), 'noref'))},
        domain=[('id', 'in', Eval('matrix_domain'))],
        depends=['rm_bmz_type', 'matrix_domain'])
    matrix_domain = fields.Function(fields.Many2Many('lims.matrix',
        None, None, 'Matrix domain'),
        'on_change_with_matrix_domain')
    repetitions = fields.Integer('Repetitions',
        states={'readonly': Or(Eval('type') == 'bmz',
            Eval('rm_bmz_type') == 'exist')},
        depends=['type', 'rm_bmz_type'])
    label = fields.Char('Label', depends=['rm_bmz_type'], states={
        'readonly': Eval('rm_bmz_type') == 'exist'})
    concentration_level = fields.Many2One('lims.concentration.level',
        'Concentration level', states={
            'invisible': Bool(Eval('concentration_level_invisible')),
            }, depends=['concentration_level_invisible'])
    concentration_level_invisible = fields.Boolean(
        'Concentration level invisible')

    @fields.depends('type')
    def on_change_with_concentration_level_invisible(self, name=None):
        Config = Pool().get('lims.configuration')
        config = Config(1)
        if self.type == 'rm':
            if (config.rm_fraction_type and
                    config.rm_fraction_type.control_charts):
                return False
        elif self.type == 'bmz':
            if (config.bmz_fraction_type and
                    config.bmz_fraction_type.control_charts):
                return False
        return True

    @fields.depends('planification', 'type', 'rm_bmz_type',
        '_parent_planification.analysis')
    def on_change_with_fraction_domain(self, name=None):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        Fraction = pool.get('lims.fraction')
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')

        if not self.type or not self.rm_bmz_type:
            return []
        if self.rm_bmz_type == 'noref':
            return []

        p_analysis_ids = []
        for p_analysis in self.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))

        stored_fractions_ids = Fraction.get_stored_fractions()

        special_type = 'sla' if self.rm_bmz_type == 'sla' else self.type
        clause = [
            ('notebook.fraction.special_type', '=', special_type),
            ('notebook.fraction.id', 'in', stored_fractions_ids),
            ('analysis', 'in', p_analysis_ids),
            ]
        if self.rm_bmz_type == 'exist':
            deadline = datetime.now() - relativedelta(days=5)
            clause.extend([
                ('result', 'in', (None, '')),
                ('end_date', '=', None),
                ('annulment_date', '=', None),
                ('notebook.fraction.sample.date2', '>=', deadline),
                ])
        notebook_lines = NotebookLine.search(clause)
        if not notebook_lines:
            return []

        notebook_lines_ids = ', '.join(str(nl.id) for nl in notebook_lines)
        cursor.execute('SELECT DISTINCT(n.fraction) '
            'FROM "' + Notebook._table + '" n '
                'INNER JOIN "' + NotebookLine._table + '" nl '
                'ON nl.notebook = n.id '
            'WHERE nl.id IN (' + notebook_lines_ids + ')')
        return [x[0] for x in cursor.fetchall()]

    @staticmethod
    def default_product_type_domain():
        cursor = Transaction().connection.cursor()
        Typification = Pool().get('lims.typification')

        cursor.execute('SELECT DISTINCT(product_type) '
                'FROM "' + Typification._table + '" '
                'WHERE valid')
        return [x[0] for x in cursor.fetchall()]

    def on_change_with_product_type_domain(self, name=None):
        return self.default_product_type_domain()

    @fields.depends('product_type')
    def on_change_product_type(self):
        matrix = None
        if self.product_type:
            matrixs = self.on_change_with_matrix_domain()
            if len(matrixs) == 1:
                matrix = matrixs[0]
        self.matrix = matrix

    @fields.depends('product_type')
    def on_change_with_matrix_domain(self, name=None):
        cursor = Transaction().connection.cursor()
        Typification = Pool().get('lims.typification')

        if not self.product_type:
            return []

        cursor.execute('SELECT DISTINCT(matrix) '
                'FROM "' + Typification._table + '" '
                'WHERE product_type = %s '
                'AND valid',
                (self.product_type.id,))
        return [x[0] for x in cursor.fetchall()]

    @fields.depends('type', 'rm_bmz_type', 'reference_fraction',
        'product_type', 'matrix', 'concentration_level',
        '_parent_reference_fraction.label',
        '_parent_concentration_level.description')
    def on_change_with_label(self, name=None):
        if self.rm_bmz_type == 'exist':
            return ''
        label = ''
        date = get_print_date().strftime("%Y-%m-%d %H:%M:%S")
        if self.type == 'rm':
            label = 'RM'
            if self.concentration_level:
                label += (' (' +
                        self.concentration_level.description + ')')
        elif self.type == 'bmz':
            label = 'BMZ'
        if self.rm_bmz_type == 'sla':
            if self.reference_fraction:
                label += (' ' +
                       self.reference_fraction.label)
            label += ' ' + date
        elif self.rm_bmz_type == 'noref':
            label += ' ' + date
        return label


class AddFractionRMBMZ(Wizard):
    'Add Fraction RM/BMZ'
    __name__ = 'lims.planification.add_fraction_rm_bmz'

    start = StateView('lims.planification.add_fraction_rm_bmz.start',
        'lims.lims_add_fraction_rm_bmz_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Add', 'add', 'tryton-ok', default=True),
            ])
    add = StateTransition()

    def default_start(self, fields):
        defaults = {
            'planification': Transaction().context['active_id'],
            'concentration_level_invisible': True,
            }
        return defaults

    def transition_add(self):
        fraction = self.start.reference_fraction
        if self.start.rm_bmz_type != 'exist':
            fraction = self.create_control()
        self.add_control(fraction)
        self.add_planification_detail(fraction)
        return 'end'

    def create_control(self):
        if self.start.rm_bmz_type == 'sla':
            return self._create_control_sla()
        if self.start.rm_bmz_type == 'noref':
            return self._create_control_noref()

    def _create_control_sla(self):
        pool = Pool()
        Config = pool.get('lims.configuration')
        LabWorkYear = pool.get('lims.lab.workyear')
        Entry = pool.get('lims.entry')
        Sample = pool.get('lims.sample')
        Fraction = pool.get('lims.fraction')
        Service = pool.get('lims.service')
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')

        config = Config(1)
        if self.start.type == 'rm':
            if not config.rm_fraction_type:
                raise UserError(gettext('lims.msg_no_rm_fraction_type'))
            fraction_type = config.rm_fraction_type
        elif self.start.type == 'bmz':
            if not config.bmz_fraction_type:
                raise UserError(gettext('lims.msg_no_bmz_fraction_type'))
            fraction_type = config.bmz_fraction_type

        if (fraction_type.control_charts and not
                self.start.concentration_level):
            raise UserError(gettext('lims.msg_no_concentration_level'))

        workyear_id = LabWorkYear.find()
        workyear = LabWorkYear(workyear_id)
        if not workyear.default_entry_control:
            raise UserError(gettext('lims.msg_no_entry_control'))

        entry = Entry(workyear.default_entry_control.id)
        original_fraction = self.start.reference_fraction
        original_sample = Sample(original_fraction.sample.id)
        obj_description = self._get_obj_description(original_sample)

        # new sample
        new_sample, = Sample.copy([original_sample], default={
            'entry': entry.id,
            'date': datetime.now(),
            'label': self.start.label,
            'obj_description': obj_description,
            'fractions': [],
            })

        # new fraction
        fraction_default = {
            'sample': new_sample.id,
            'type': fraction_type.id,
            'con_type': '',
            'services': [],
            }
        if self.start.type == 'rm':
            fraction_default['rm_type'] = 'sla'
            fraction_default['rm_product_type'] = new_sample.product_type.id
            fraction_default['rm_matrix'] = new_sample.matrix.id
            fraction_default['rm_original_fraction'] = original_fraction.id
        if self.start.type == 'bmz':
            fraction_default['bmz_type'] = 'sla'
            fraction_default['bmz_product_type'] = new_sample.product_type.id
            fraction_default['bmz_matrix'] = new_sample.matrix.id
            fraction_default['bmz_original_fraction'] = original_fraction.id
        new_fraction, = Fraction.copy([original_fraction],
            default=fraction_default)

        # new services
        p_analysis_ids = []
        for p_analysis in self.start.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))

        services = Service.search([
            ('fraction', '=', original_fraction),
            ('annulled', '=', False),
            ])
        for service in services:
            if not Analysis.is_typified(service.analysis,
                    new_sample.product_type, new_sample.matrix):
                continue

            method_id = service.method and service.method.id or None
            device_id = service.device and service.device.id or None
            if service.analysis.type == 'analysis':
                original_lines = NotebookLine.search([
                    ('notebook.fraction', '=', original_fraction.id),
                    ('analysis', '=', service.analysis.id),
                    ('repetition', '=', 0),
                    ], limit=1)
                original_line = original_lines[0] if original_lines else None
                if original_line:
                    method_id = original_line.method.id
                    if original_line.device:
                        device_id = original_line.device.id

            new_service, = Service.copy([service], default={
                'fraction': new_fraction.id,
                'method': method_id,
                'device': device_id,
                })

            # delete services/details not related to planification
            to_delete = EntryDetailAnalysis.search([
                ('service', '=', new_service.id),
                ('analysis', 'not in', p_analysis_ids),
                ])
            if to_delete:
                with Transaction().set_user(0, set_context=True):
                    EntryDetailAnalysis.delete(to_delete)
            if EntryDetailAnalysis.search_count([
                    ('service', '=', new_service.id),
                    ]) == 0:
                with Transaction().set_user(0, set_context=True):
                    Service.delete([new_service])

        # confirm fraction: new notebook and stock move
        Fraction.confirm([new_fraction])

        # Edit notebook lines
        if fraction_type.control_charts:
            notebook_lines = NotebookLine.search([
                ('notebook.fraction', '=', new_fraction.id),
                ])
            if notebook_lines:
                defaults = {
                    'concentration_level': self.start.concentration_level.id,
                    }
                NotebookLine.write(notebook_lines, defaults)
        if self.start.type == 'rm':
            notebook_lines = NotebookLine.search([
                ('notebook.fraction', '=', new_fraction.id),
                ])
            if notebook_lines:
                defaults = {
                    'final_concentration': None,
                    'literal_final_concentration': None,
                    'final_unit': None,
                    'detection_limit': None,
                    'quantification_limit': None,
                    'lower_limit': None,
                    'upper_limit': None,
                    }
                if config.rm_start_uom:
                    defaults['initial_unit'] = config.rm_start_uom.id
                NotebookLine.write(notebook_lines, defaults)

        # Generate repetition
        if self.start.repetitions and self.start.repetitions > 0:
            notebook_lines = NotebookLine.search([
                ('notebook.fraction', '=', new_fraction.id),
                ])
            if notebook_lines:
                self.generate_repetition(notebook_lines,
                    self.start.repetitions)

        return new_fraction

    def _get_obj_description(self, sample):
        cursor = Transaction().connection.cursor()
        ObjectiveDescription = Pool().get('lims.objective_description')

        if not sample.product_type or not sample.matrix:
            return None

        cursor.execute('SELECT id '
            'FROM "' + ObjectiveDescription._table + '" '
            'WHERE product_type = %s '
                'AND matrix = %s',
            (sample.product_type.id, sample.matrix.id))
        res = cursor.fetchone()
        return res and res[0] or None

    def _create_control_noref(self):
        pool = Pool()
        Config = pool.get('lims.configuration')
        LabWorkYear = pool.get('lims.lab.workyear')
        Entry = pool.get('lims.entry')
        Sample = pool.get('lims.sample')
        Fraction = pool.get('lims.fraction')
        Service = pool.get('lims.service')
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')

        config = Config(1)
        if self.start.type == 'rm':
            if not config.rm_fraction_type:
                raise UserError(gettext('lims.msg_no_rm_fraction_type'))
            fraction_type = config.rm_fraction_type
            if (not fraction_type.default_package_type or
                    not fraction_type.default_fraction_state):
                raise UserError(gettext(
                    'lims.msg_no_rm_default_configuration'))
        elif self.start.type == 'bmz':
            if not config.bmz_fraction_type:
                raise UserError(gettext('lims.msg_no_bmz_fraction_type'))
            fraction_type = config.bmz_fraction_type
            if (not fraction_type.default_package_type or
                    not fraction_type.default_fraction_state):
                raise UserError(gettext(
                    'lims.msg_no_bmz_default_configuration'))

        if (fraction_type.control_charts and not
                self.start.concentration_level):
            raise UserError(gettext('lims.msg_no_concentration_level'))

        workyear_id = LabWorkYear.find()
        workyear = LabWorkYear(workyear_id)
        if not workyear.default_entry_control:
            raise UserError(gettext('lims.msg_no_entry_control'))

        entry = Entry(workyear.default_entry_control.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

        laboratory = self.start.planification.laboratory
        obj_description = self._get_obj_description(self.start)

        # new sample
        new_sample, = Sample.create([{
            'entry': entry.id,
            'party': entry.party.id,
            'date': datetime.now(),
            'product_type': self.start.product_type.id,
            'matrix': self.start.matrix.id,
            'zone': zone_id,
            'label': self.start.label,
            'obj_description': obj_description,
            'packages': [('create', [{
                'quantity': 1,
                'type': fraction_type.default_package_type.id,
                'state': fraction_type.default_fraction_state.id,
                }])],
            'fractions': [],
            }])

        # new fraction
        fraction_default = {
            'sample': new_sample.id,
            'type': fraction_type.id,
            'storage_location': laboratory.related_location.id,
            'services': [],
            }
        if fraction_type.max_storage_time:
            fraction_default['storage_time'] = fraction_type.max_storage_time
        elif laboratory.related_location.storage_time:
            fraction_default['storage_time'] = (
                laboratory.related_location.storage_time)
        else:
            fraction_default['storage_time'] = 3
        if self.start.type == 'rm':
            fraction_default['rm_type'] = 'noinitialrm'
            fraction_default['rm_product_type'] = new_sample.product_type.id
            fraction_default['rm_matrix'] = new_sample.matrix.id
        if self.start.type == 'bmz':
            fraction_default['bmz_type'] = 'noinitialbmz'
            fraction_default['bmz_product_type'] = new_sample.product_type.id
            fraction_default['bmz_matrix'] = new_sample.matrix.id
        new_fraction, = Fraction.create([fraction_default])

        # new services
        services_default = []
        for p_analysis in self.start.planification.analysis:
            if not Analysis.is_typified(p_analysis,
                    new_sample.product_type, new_sample.matrix):
                raise UserError(gettext('lims.msg_not_typified',
                    analysis=p_analysis.rec_name,
                    product_type=new_sample.product_type.rec_name,
                    matrix=new_sample.matrix.rec_name,
                    ))
            laboratory_id = (laboratory.id if p_analysis.type != 'group'
                else None)
            method_id = None
            if new_sample.typification_domain:
                for t in new_sample.typification_domain:
                    if (t.analysis.id == p_analysis.id and
                            t.by_default is True):
                        method_id = t.method.id
            device_id = None
            if p_analysis.devices:
                for d in p_analysis.devices:
                    if (d.laboratory.id == laboratory.id and
                            d.by_default is True):
                        device_id = d.device.id
            services_default.append({
                'fraction': new_fraction.id,
                'analysis': p_analysis.id,
                'laboratory': laboratory_id,
                'method': method_id,
                'device': device_id,
                })
        for service in services_default:
            new_service, = Service.create([service])

            # new analysis details (on service create)

        # confirm fraction: new notebook and stock move
        Fraction.confirm([new_fraction])

        # Edit notebook lines
        if fraction_type.control_charts:
            notebook_lines = NotebookLine.search([
                ('notebook.fraction', '=', new_fraction.id),
                ])
            if notebook_lines:
                defaults = {
                    'concentration_level': self.start.concentration_level.id,
                    }
                NotebookLine.write(notebook_lines, defaults)
        if self.start.type == 'rm':
            notebook_lines = NotebookLine.search([
                ('notebook.fraction', '=', new_fraction.id),
                ])
            if notebook_lines:
                defaults = {
                    'final_concentration': None,
                    'literal_final_concentration': None,
                    'final_unit': None,
                    'detection_limit': None,
                    'quantification_limit': None,
                    'lower_limit': None,
                    'upper_limit': None,
                    }
                if config.rm_start_uom:
                    defaults['initial_unit'] = config.rm_start_uom.id
                NotebookLine.write(notebook_lines, defaults)

        # Generate repetition
        if self.start.repetitions and self.start.repetitions > 0:
            notebook_lines = NotebookLine.search([
                ('notebook.fraction', '=', new_fraction.id),
                ])
            if notebook_lines:
                self.generate_repetition(notebook_lines,
                    self.start.repetitions)

        return new_fraction

    def generate_repetition(self, notebook_lines, repetitions):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        Notebook = pool.get('lims.notebook')

        p_analysis_ids = []
        for p_analysis in self.start.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))

        analysis_to_repeat = {}
        for notebook_line in notebook_lines:
            if notebook_line.analysis.id not in p_analysis_ids:
                continue
            if notebook_line.analysis.id not in analysis_to_repeat:
                analysis_to_repeat[notebook_line.analysis.id] = notebook_line
            elif (notebook_line.repetition >
                    analysis_to_repeat[notebook_line.analysis.id].repetition):
                analysis_to_repeat[notebook_line.analysis.id] = notebook_line

        notebook = Notebook(notebook_lines[0].notebook.id)

        to_create = []
        for nline in analysis_to_repeat.values():
            for i in range(1, repetitions + 1):
                to_create.append({
                    'analysis_detail': nline.analysis_detail.id,
                    'service': nline.service.id,
                    'analysis': nline.analysis.id,
                    'analysis_origin': nline.analysis_origin,
                    'urgent': nline.urgent,
                    'repetition': nline.repetition + i,
                    'laboratory': nline.laboratory.id,
                    'method': nline.method.id,
                    'device': nline.device.id if nline.device else None,
                    'initial_concentration': nline.initial_concentration,
                    'final_concentration': nline.final_concentration,
                    'literal_final_concentration': (
                        nline.literal_final_concentration),
                    'initial_unit': (nline.initial_unit.id if
                        nline.initial_unit else None),
                    'final_unit': (nline.final_unit.id if
                        nline.final_unit else None),
                    'detection_limit': nline.detection_limit,
                    'quantification_limit': nline.quantification_limit,
                    'lower_limit': nline.lower_limit,
                    'upper_limit': nline.upper_limit,
                    'decimals': nline.decimals,
                    'significant_digits': nline.significant_digits,
                    'scientific_notation': nline.scientific_notation,
                    'report': nline.report,
                    'concentration_level': (nline.concentration_level.id if
                        nline.concentration_level else None),
                    'results_estimated_waiting': (
                        nline.results_estimated_waiting),
                    'department': (nline.department.id if
                        nline.department else None),
                    })
        Notebook.write([notebook], {
            'lines': [('create', to_create)],
            })

    def add_control(self, fraction):
        Planification = Pool().get('lims.planification')
        Planification.write([self.start.planification], {
            'controls': [('add', [fraction.id])],
            })

    def add_planification_detail(self, fraction):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')
        PlanificationDetail = pool.get('lims.planification.detail')
        Service = pool.get('lims.service')

        p_analysis_ids = []
        for p_analysis in self.start.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))
        clause = [
            ('notebook.fraction', '=', fraction.id),
            ('analysis', 'in', p_analysis_ids),
            ('analysis.behavior', '!=', 'internal_relation'),
            ]
        if self.start.rm_bmz_type == 'exist':
            clause.extend([
                ('result', 'in', (None, '')),
                ('end_date', '=', None),
                ('annulment_date', '=', None),
                ])
        else:
            clause.append(('planification', '=', None))
        notebook_lines = NotebookLine.search(clause)
        if notebook_lines:
            details_to_create = {}
            for nl in notebook_lines:
                f = nl.notebook.fraction.id
                s = nl.service.analysis.id
                if (f, s) not in details_to_create:
                    details_to_create[(f, s)] = []
                details_to_create[(f, s)].append({
                    'notebook_line': nl.id,
                    'planned_service': s,
                    'is_control': True,
                    })
            if details_to_create:
                for k, v in details_to_create.items():
                    details = PlanificationDetail.search([
                        ('planification', '=', self.start.planification.id),
                        ('fraction', '=', k[0]),
                        ('service_analysis', '=', k[1]),
                        ])
                    if details:
                        PlanificationDetail.write([details[0]], {
                            'details': [('create', v)],
                            })
                    else:
                        PlanificationDetail.create([{
                            'planification': self.start.planification.id,
                            'fraction': k[0],
                            'service_analysis': k[1],
                            'urgent': Service.is_service_urgent(k[0], k[1]),
                            'details': [('create', v)],
                            }])


class AddFractionBREStart(ModelView):
    'Add Fraction BRE'
    __name__ = 'lims.planification.add_fraction_bre.start'

    planification = fields.Many2One('lims.planification', 'Planification')
    type = fields.Selection([
        ('new', 'New BRE'),
        ('exist', 'Existing BRE'),
        ], 'BRE Type', sort=False, required=True)
    bre_fraction = fields.Many2One('lims.fraction',
        'BRE fraction', depends=['fraction_domain', 'type'],
        states={
            'readonly': Bool(Equal(Eval('type'), 'new')),
            'required': Bool(Equal(Eval('type'), 'exist')),
        }, domain=[('id', 'in', Eval('fraction_domain'))])
    fraction_domain = fields.Function(fields.One2Many('lims.fraction',
        None, 'Fraction domain'), 'on_change_with_fraction_domain')
    product_type = fields.Many2One('lims.product.type', 'Product type',
        states={'required': Bool(Equal(Eval('type'), 'new'))},
        domain=[('id', 'in', Eval('product_type_domain'))],
        depends=['type', 'product_type_domain'])
    product_type_domain = fields.Function(fields.Many2Many(
        'lims.product.type', None, None, 'Product type domain'),
        'on_change_with_product_type_domain')
    matrix = fields.Many2One('lims.matrix', 'Matrix', required=True,
        states={'required': Bool(Equal(Eval('type'), 'new'))},
        domain=[('id', 'in', Eval('matrix_domain'))],
        depends=['type', 'matrix_domain'])
    matrix_domain = fields.Function(fields.Many2Many('lims.matrix',
        None, None, 'Matrix domain'),
        'on_change_with_matrix_domain')
    reagents = fields.One2Many('lims.fraction.reagent', None,
        'Reagents', states={
            'readonly': Bool(Equal(Eval('type'), 'exist')),
        }, depends=['type'], required=True)
    label = fields.Char('Label', depends=['type'], states={
        'readonly': Eval('type') == 'exist'})
    concentration_level = fields.Many2One('lims.concentration.level',
        'Concentration level', states={
            'invisible': Bool(Eval('concentration_level_invisible')),
            }, depends=['concentration_level_invisible'])
    concentration_level_invisible = fields.Boolean(
        'Concentration level invisible')

    @fields.depends('planification', 'type', '_parent_planification.analysis')
    def on_change_with_fraction_domain(self, name=None):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        Fraction = pool.get('lims.fraction')
        NotebookLine = pool.get('lims.notebook.line')

        if self.type != 'exist':
            return []

        p_analysis_ids = []
        for p_analysis in self.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))

        stored_fractions_ids = Fraction.get_stored_fractions()

        deadline = datetime.now() - relativedelta(days=5)
        clause = [
            ('notebook.fraction.special_type', '=', 'bre'),
            ('notebook.fraction.id', 'in', stored_fractions_ids),
            ('analysis', 'in', p_analysis_ids),
            ('result', 'in', (None, '')),
            ('end_date', '=', None),
            ('annulment_date', '=', None),
            ('notebook.fraction.sample.date2', '>=', deadline),
            ]
        notebook_lines = NotebookLine.search(clause)

        fractions = [nl.notebook.fraction.id for nl in notebook_lines]
        return list(set(fractions))

    @staticmethod
    def default_product_type_domain():
        cursor = Transaction().connection.cursor()
        Typification = Pool().get('lims.typification')

        cursor.execute('SELECT DISTINCT(product_type) '
                'FROM "' + Typification._table + '" '
                'WHERE valid')
        return [x[0] for x in cursor.fetchall()]

    def on_change_with_product_type_domain(self, name=None):
        return self.default_product_type_domain()

    @fields.depends('product_type')
    def on_change_product_type(self):
        matrix = None
        if self.product_type:
            matrixs = self.on_change_with_matrix_domain()
            if len(matrixs) == 1:
                matrix = matrixs[0]
        self.matrix = matrix

    @fields.depends('product_type')
    def on_change_with_matrix_domain(self, name=None):
        cursor = Transaction().connection.cursor()
        Typification = Pool().get('lims.typification')

        if not self.product_type:
            return []

        cursor.execute('SELECT DISTINCT(matrix) '
                'FROM "' + Typification._table + '" '
                'WHERE product_type = %s '
                'AND valid',
                (self.product_type.id,))
        return [x[0] for x in cursor.fetchall()]

    @fields.depends('type', 'product_type', 'matrix')
    def on_change_with_label(self, name=None):
        if self.type == 'exist':
            return ''
        label = 'BRE'
        date = get_print_date().strftime("%Y-%m-%d %H:%M:%S")
        label += ' ' + date
        return label


class AddFractionBRE(Wizard):
    'Add Fraction BRE'
    __name__ = 'lims.planification.add_fraction_bre'

    start = StateView('lims.planification.add_fraction_bre.start',
        'lims.lims_add_fraction_bre_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Add', 'add', 'tryton-ok', default=True),
            ])
    add = StateTransition()

    def default_start(self, fields):
        Config = Pool().get('lims.configuration')
        config = Config(1)
        defaults = {
            'planification': Transaction().context['active_id'],
            'concentration_level_invisible': True,
            }
        if (config.bre_fraction_type and
                config.bre_fraction_type.control_charts):
            defaults['concentration_level_invisible'] = False
        return defaults

    def transition_add(self):
        fraction = self.start.bre_fraction
        if self.start.type == 'new':
            fraction = self.create_control()
        self.add_control(fraction)
        self.add_planification_detail(fraction)
        return 'end'

    def create_control(self):
        pool = Pool()
        Config = pool.get('lims.configuration')
        LabWorkYear = pool.get('lims.lab.workyear')
        Entry = pool.get('lims.entry')
        Sample = pool.get('lims.sample')
        Fraction = pool.get('lims.fraction')
        Service = pool.get('lims.service')
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')

        config = Config(1)
        if not config.bre_fraction_type:
            raise UserError(gettext('lims.msg_no_bre_fraction_type'))
        fraction_type = config.bre_fraction_type
        if (not fraction_type.default_package_type or
                not fraction_type.default_fraction_state):
            raise UserError(gettext('lims.msg_no_bre_default_configuration'))

        if (fraction_type.control_charts and not
                self.start.concentration_level):
            raise UserError(gettext('lims.msg_no_concentration_level'))

        workyear_id = LabWorkYear.find()
        workyear = LabWorkYear(workyear_id)
        if not workyear.default_entry_control:
            raise UserError(gettext('lims.msg_no_entry_control'))

        entry = Entry(workyear.default_entry_control.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

        laboratory = self.start.planification.laboratory
        obj_description = self._get_obj_description(self.start)

        # new sample
        new_sample, = Sample.create([{
            'entry': entry.id,
            'party': entry.party.id,
            'date': datetime.now(),
            'product_type': self.start.product_type.id,
            'matrix': self.start.matrix.id,
            'zone': zone_id,
            'label': self.start.label,
            'obj_description': obj_description,
            'packages': [('create', [{
                'quantity': 1,
                'type': fraction_type.default_package_type.id,
                'state': fraction_type.default_fraction_state.id,
                }])],
            'fractions': [],
            }])

        # new fraction
        bre_reagents = [{
            'product': r.product.id,
            'lot': r.lot.id if r.lot else None,
            } for r in self.start.reagents]
        fraction_default = {
            'sample': new_sample.id,
            'type': fraction_type.id,
            'storage_location': laboratory.related_location.id,
            'services': [],
            'bre_product_type': new_sample.product_type.id,
            'bre_matrix': new_sample.matrix.id,
            'bre_reagents': [('create', bre_reagents)],
            }
        if fraction_type.max_storage_time:
            fraction_default['storage_time'] = fraction_type.max_storage_time
        elif laboratory.related_location.storage_time:
            fraction_default['storage_time'] = (
                laboratory.related_location.storage_time)
        else:
            fraction_default['storage_time'] = 3
        new_fraction, = Fraction.create([fraction_default])

        # new services
        services_default = []
        for p_analysis in self.start.planification.analysis:
            if not Analysis.is_typified(p_analysis,
                    new_sample.product_type, new_sample.matrix):
                raise UserError(gettext('lims.msg_not_typified',
                    analysis=p_analysis.rec_name,
                    product_type=new_sample.product_type.rec_name,
                    matrix=new_sample.matrix.rec_name,
                    ))
            laboratory_id = (laboratory.id if p_analysis.type != 'group'
                else None)
            method_id = None
            if new_sample.typification_domain:
                for t in new_sample.typification_domain:
                    if (t.analysis.id == p_analysis.id and
                            t.by_default is True):
                        method_id = t.method.id
            device_id = None
            if p_analysis.devices:
                for d in p_analysis.devices:
                    if (d.laboratory.id == laboratory.id and
                            d.by_default is True):
                        device_id = d.device.id
            services_default.append({
                'fraction': new_fraction.id,
                'analysis': p_analysis.id,
                'laboratory': laboratory_id,
                'method': method_id,
                'device': device_id,
                })
        for service in services_default:
            new_service, = Service.create([service])

            # new analysis details (on service create)

        # confirm fraction: new notebook and stock move
        Fraction.confirm([new_fraction])

        # Edit notebook lines
        if fraction_type.control_charts:
            notebook_lines = NotebookLine.search([
                ('notebook.fraction', '=', new_fraction.id),
                ])
            if notebook_lines:
                defaults = {
                    'concentration_level': self.start.concentration_level.id,
                    }
                NotebookLine.write(notebook_lines, defaults)

        return new_fraction

    def _get_obj_description(self, sample):
        cursor = Transaction().connection.cursor()
        ObjectiveDescription = Pool().get('lims.objective_description')

        if not sample.product_type or not sample.matrix:
            return None

        cursor.execute('SELECT id '
            'FROM "' + ObjectiveDescription._table + '" '
            'WHERE product_type = %s '
                'AND matrix = %s',
            (sample.product_type.id, sample.matrix.id))
        res = cursor.fetchone()
        return res and res[0] or None

    def add_control(self, fraction):
        Planification = Pool().get('lims.planification')
        Planification.write([self.start.planification], {
            'controls': [('add', [fraction.id])],
            })

    def add_planification_detail(self, fraction):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')
        PlanificationDetail = pool.get('lims.planification.detail')
        Service = pool.get('lims.service')

        p_analysis_ids = []
        for p_analysis in self.start.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))
        clause = [
            ('notebook.fraction', '=', fraction.id),
            ('analysis', 'in', p_analysis_ids),
            ('analysis.behavior', '!=', 'internal_relation'),
            ]
        if self.start.type == 'exist':
            clause.extend([
                ('result', 'in', (None, '')),
                ('end_date', '=', None),
                ('annulment_date', '=', None),
                ])
        else:
            clause.append(('planification', '=', None))
        notebook_lines = NotebookLine.search(clause)
        if notebook_lines:
            details_to_create = {}
            for nl in notebook_lines:
                f = nl.notebook.fraction.id
                s = nl.service.analysis.id
                if (f, s) not in details_to_create:
                    details_to_create[(f, s)] = []
                details_to_create[(f, s)].append({
                    'notebook_line': nl.id,
                    'planned_service': s,
                    'is_control': True,
                    })
            if details_to_create:
                for k, v in details_to_create.items():
                    details = PlanificationDetail.search([
                        ('planification', '=', self.start.planification.id),
                        ('fraction', '=', k[0]),
                        ('service_analysis', '=', k[1]),
                        ])
                    if details:
                        PlanificationDetail.write([details[0]], {
                            'details': [('create', v)],
                            })
                    else:
                        PlanificationDetail.create([{
                            'planification': self.start.planification.id,
                            'fraction': k[0],
                            'service_analysis': k[1],
                            'urgent': Service.is_service_urgent(k[0], k[1]),
                            'details': [('create', v)],
                            }])


class AddFractionMRTStart(ModelView):
    'Add Fraction MRT'
    __name__ = 'lims.planification.add_fraction_mrt.start'

    planification = fields.Many2One('lims.planification', 'Planification')
    type = fields.Selection([
        ('new', 'New MRT'),
        ('exist', 'Existing MRT'),
        ], 'MRT Type', sort=False, required=True)
    mrt_fraction = fields.Many2One('lims.fraction',
        'MRT fraction', depends=['fraction_domain', 'type'],
        states={
            'readonly': Bool(Equal(Eval('type'), 'new')),
            'required': Bool(Equal(Eval('type'), 'exist')),
        }, domain=[('id', 'in', Eval('fraction_domain'))])
    fraction_domain = fields.Function(fields.One2Many('lims.fraction',
        None, 'Fraction domain'), 'on_change_with_fraction_domain')
    product_type = fields.Many2One('lims.product.type', 'Product type',
        states={'required': Bool(Equal(Eval('type'), 'new'))},
        domain=[('id', 'in', Eval('product_type_domain'))],
        depends=['type', 'product_type_domain'])
    product_type_domain = fields.Function(fields.Many2Many(
        'lims.product.type', None, None, 'Product type domain'),
        'on_change_with_product_type_domain')
    matrix = fields.Many2One('lims.matrix', 'Matrix', required=True,
        states={'required': Bool(Equal(Eval('type'), 'new'))},
        domain=[('id', 'in', Eval('matrix_domain'))],
        depends=['type', 'matrix_domain'])
    matrix_domain = fields.Function(fields.Many2Many('lims.matrix',
        None, None, 'Matrix domain'),
        'on_change_with_matrix_domain')
    repetitions = fields.Integer('Repetitions')
    label = fields.Char('Label', depends=['type'], states={
        'readonly': Eval('type') == 'exist'})
    concentration_level = fields.Many2One('lims.concentration.level',
        'Concentration level', states={
            'invisible': Bool(Eval('concentration_level_invisible')),
            }, depends=['concentration_level_invisible'])
    concentration_level_invisible = fields.Boolean(
        'Concentration level invisible')

    @fields.depends('planification', 'type', '_parent_planification.analysis')
    def on_change_with_fraction_domain(self, name=None):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        Fraction = pool.get('lims.fraction')
        NotebookLine = pool.get('lims.notebook.line')

        if self.type != 'exist':
            return []

        p_analysis_ids = []
        for p_analysis in self.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))

        stored_fractions_ids = Fraction.get_stored_fractions()

        deadline = datetime.now() - relativedelta(days=5)
        clause = [
            ('notebook.fraction.special_type', '=', 'mrt'),
            ('notebook.fraction.id', 'in', stored_fractions_ids),
            ('analysis', 'in', p_analysis_ids),
            ('result', 'in', (None, '')),
            ('end_date', '=', None),
            ('annulment_date', '=', None),
            ('notebook.fraction.sample.date2', '>=', deadline),
            ]
        notebook_lines = NotebookLine.search(clause)

        fractions = [nl.notebook.fraction.id for nl in notebook_lines]
        return list(set(fractions))

    @staticmethod
    def default_product_type_domain():
        cursor = Transaction().connection.cursor()
        Typification = Pool().get('lims.typification')

        cursor.execute('SELECT DISTINCT(product_type) '
                'FROM "' + Typification._table + '" '
                'WHERE valid')
        return [x[0] for x in cursor.fetchall()]

    def on_change_with_product_type_domain(self, name=None):
        return self.default_product_type_domain()

    @fields.depends('product_type')
    def on_change_product_type(self):
        matrix = None
        if self.product_type:
            matrixs = self.on_change_with_matrix_domain()
            if len(matrixs) == 1:
                matrix = matrixs[0]
        self.matrix = matrix

    @fields.depends('product_type')
    def on_change_with_matrix_domain(self, name=None):
        cursor = Transaction().connection.cursor()
        Typification = Pool().get('lims.typification')

        if not self.product_type:
            return []

        cursor.execute('SELECT DISTINCT(matrix) '
                'FROM "' + Typification._table + '" '
                'WHERE product_type = %s '
                'AND valid',
                (self.product_type.id,))
        return [x[0] for x in cursor.fetchall()]

    @fields.depends('type', 'product_type', 'matrix')
    def on_change_with_label(self, name=None):
        if self.type == 'exist':
            return ''
        label = 'MRT'
        date = get_print_date().strftime("%Y-%m-%d %H:%M:%S")
        label += ' ' + date
        return label


class AddFractionMRT(Wizard):
    'Add Fraction MRT'
    __name__ = 'lims.planification.add_fraction_mrt'

    start = StateView('lims.planification.add_fraction_mrt.start',
        'lims.lims_add_fraction_mrt_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Add', 'add', 'tryton-ok', default=True),
            ])
    add = StateTransition()

    def default_start(self, fields):
        Config = Pool().get('lims.configuration')
        config = Config(1)
        defaults = {
            'planification': Transaction().context['active_id'],
            'concentration_level_invisible': True,
            }
        if (config.mrt_fraction_type and
                config.mrt_fraction_type.control_charts):
            defaults['concentration_level_invisible'] = False
        return defaults

    def transition_add(self):
        fraction = self.start.mrt_fraction
        if self.start.type == 'new':
            fraction = self.create_control()
        self.add_control(fraction)
        self.add_planification_detail(fraction)
        return 'end'

    def create_control(self):
        pool = Pool()
        Config = pool.get('lims.configuration')
        LabWorkYear = pool.get('lims.lab.workyear')
        Entry = pool.get('lims.entry')
        Sample = pool.get('lims.sample')
        Fraction = pool.get('lims.fraction')
        Service = pool.get('lims.service')
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')

        config = Config(1)
        if not config.mrt_fraction_type:
            raise UserError(gettext('lims.msg_no_mrt_fraction_type'))
        fraction_type = config.mrt_fraction_type
        if (not fraction_type.default_package_type or
                not fraction_type.default_fraction_state):
            raise UserError(gettext('lims.msg_no_mrt_default_configuration'))

        if (fraction_type.control_charts and not
                self.start.concentration_level):
            raise UserError(gettext('lims.msg_no_concentration_level'))

        workyear_id = LabWorkYear.find()
        workyear = LabWorkYear(workyear_id)
        if not workyear.default_entry_control:
            raise UserError(gettext('lims.msg_no_entry_control'))

        entry = Entry(workyear.default_entry_control.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

        laboratory = self.start.planification.laboratory
        obj_description = self._get_obj_description(self.start)

        # new sample
        new_sample, = Sample.create([{
            'entry': entry.id,
            'party': entry.party.id,
            'date': datetime.now(),
            'product_type': self.start.product_type.id,
            'matrix': self.start.matrix.id,
            'zone': zone_id,
            'label': self.start.label,
            'obj_description': obj_description,
            'packages': [('create', [{
                'quantity': 1,
                'type': fraction_type.default_package_type.id,
                'state': fraction_type.default_fraction_state.id,
                }])],
            'fractions': [],
            }])

        # new fraction
        fraction_default = {
            'sample': new_sample.id,
            'type': fraction_type.id,
            'storage_location': laboratory.related_location.id,
            'services': [],
            'mrt_product_type': new_sample.product_type.id,
            'mrt_matrix': new_sample.matrix.id,
            }
        if fraction_type.max_storage_time:
            fraction_default['storage_time'] = fraction_type.max_storage_time
        elif laboratory.related_location.storage_time:
            fraction_default['storage_time'] = (
                laboratory.related_location.storage_time)
        else:
            fraction_default['storage_time'] = 3
        new_fraction, = Fraction.create([fraction_default])

        # new services
        services_default = []
        for p_analysis in self.start.planification.analysis:
            if not Analysis.is_typified(p_analysis,
                    new_sample.product_type, new_sample.matrix):
                raise UserError(gettext('lims.msg_not_typified',
                    analysis=p_analysis.rec_name,
                    product_type=new_sample.product_type.rec_name,
                    matrix=new_sample.matrix.rec_name,
                    ))
            laboratory_id = (laboratory.id if p_analysis.type != 'group'
                else None)
            method_id = None
            if new_sample.typification_domain:
                for t in new_sample.typification_domain:
                    if (t.analysis.id == p_analysis.id and
                            t.by_default is True):
                        method_id = t.method.id
            device_id = None
            if p_analysis.devices:
                for d in p_analysis.devices:
                    if (d.laboratory.id == laboratory.id and
                            d.by_default is True):
                        device_id = d.device.id
            services_default.append({
                'fraction': new_fraction.id,
                'analysis': p_analysis.id,
                'laboratory': laboratory_id,
                'method': method_id,
                'device': device_id,
                })
        for service in services_default:
            new_service, = Service.create([service])

            # new analysis details (on service create)

        # confirm fraction: new notebook and stock move
        Fraction.confirm([new_fraction])

        # Edit notebook lines
        if fraction_type.control_charts:
            notebook_lines = NotebookLine.search([
                ('notebook.fraction', '=', new_fraction.id),
                ])
            if notebook_lines:
                defaults = {
                    'concentration_level': self.start.concentration_level.id,
                    }
                NotebookLine.write(notebook_lines, defaults)

        # Generate repetition
        if self.start.repetitions and self.start.repetitions > 0:
            notebook_lines = NotebookLine.search([
                ('notebook.fraction', '=', new_fraction.id),
                ])
            if notebook_lines:
                self.generate_repetition(notebook_lines,
                    self.start.repetitions)

        return new_fraction

    def _get_obj_description(self, sample):
        cursor = Transaction().connection.cursor()
        ObjectiveDescription = Pool().get('lims.objective_description')

        if not sample.product_type or not sample.matrix:
            return None

        cursor.execute('SELECT id '
            'FROM "' + ObjectiveDescription._table + '" '
            'WHERE product_type = %s '
                'AND matrix = %s',
            (sample.product_type.id, sample.matrix.id))
        res = cursor.fetchone()
        return res and res[0] or None

    def generate_repetition(self, notebook_lines, repetitions):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        Notebook = pool.get('lims.notebook')

        p_analysis_ids = []
        for p_analysis in self.start.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))

        analysis_to_repeat = {}
        for notebook_line in notebook_lines:
            if notebook_line.analysis.id not in p_analysis_ids:
                continue
            if notebook_line.analysis.id not in analysis_to_repeat:
                analysis_to_repeat[notebook_line.analysis.id] = notebook_line
            elif (notebook_line.repetition >
                    analysis_to_repeat[notebook_line.analysis.id].repetition):
                analysis_to_repeat[notebook_line.analysis.id] = notebook_line

        notebook = Notebook(notebook_lines[0].notebook.id)

        to_create = []
        for nline in analysis_to_repeat.values():
            for i in range(1, repetitions + 1):
                to_create.append({
                    'analysis_detail': nline.analysis_detail.id,
                    'service': nline.service.id,
                    'analysis': nline.analysis.id,
                    'analysis_origin': nline.analysis_origin,
                    'urgent': nline.urgent,
                    'repetition': nline.repetition + i,
                    'laboratory': nline.laboratory.id,
                    'method': nline.method.id,
                    'device': nline.device.id if nline.device else None,
                    'initial_concentration': nline.initial_concentration,
                    'final_concentration': nline.final_concentration,
                    'literal_final_concentration': (
                        nline.literal_final_concentration),
                    'initial_unit': (nline.initial_unit.id if
                        nline.initial_unit else None),
                    'final_unit': (nline.final_unit.id if
                        nline.final_unit else None),
                    'detection_limit': nline.detection_limit,
                    'quantification_limit': nline.quantification_limit,
                    'lower_limit': nline.lower_limit,
                    'upper_limit': nline.upper_limit,
                    'decimals': nline.decimals,
                    'significant_digits': nline.significant_digits,
                    'scientific_notation': nline.scientific_notation,
                    'report': nline.report,
                    'concentration_level': (nline.concentration_level.id if
                        nline.concentration_level else None),
                    'results_estimated_waiting': (
                        nline.results_estimated_waiting),
                    'department': (nline.department.id if
                        nline.department else None),
                    })
        Notebook.write([notebook], {
            'lines': [('create', to_create)],
            })

    def add_control(self, fraction):
        Planification = Pool().get('lims.planification')
        Planification.write([self.start.planification], {
            'controls': [('add', [fraction.id])],
            })

    def add_planification_detail(self, fraction):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')
        PlanificationDetail = pool.get('lims.planification.detail')
        Service = pool.get('lims.service')

        p_analysis_ids = []
        for p_analysis in self.start.planification.analysis:
            if p_analysis.type == 'analysis':
                p_analysis_ids.append(p_analysis.id)
            else:
                p_analysis_ids.extend(
                    Analysis.get_included_analysis_analysis(p_analysis.id))
        clause = [
            ('notebook.fraction', '=', fraction.id),
            ('analysis', 'in', p_analysis_ids),
            ('analysis.behavior', '!=', 'internal_relation'),
            ]
        if self.start.type == 'exist':
            clause.extend([
                ('result', 'in', (None, '')),
                ('end_date', '=', None),
                ('annulment_date', '=', None),
                ])
        else:
            clause.append(('planification', '=', None))
        notebook_lines = NotebookLine.search(clause)
        if notebook_lines:
            details_to_create = {}
            for nl in notebook_lines:
                f = nl.notebook.fraction.id
                s = nl.service.analysis.id
                if (f, s) not in details_to_create:
                    details_to_create[(f, s)] = []
                details_to_create[(f, s)].append({
                    'notebook_line': nl.id,
                    'planned_service': s,
                    'is_control': True,
                    })
            if details_to_create:
                for k, v in details_to_create.items():
                    details = PlanificationDetail.search([
                        ('planification', '=', self.start.planification.id),
                        ('fraction', '=', k[0]),
                        ('service_analysis', '=', k[1]),
                        ])
                    if details:
                        PlanificationDetail.write([details[0]], {
                            'details': [('create', v)],
                            })
                    else:
                        PlanificationDetail.create([{
                            'planification': self.start.planification.id,
                            'fraction': k[0],
                            'service_analysis': k[1],
                            'urgent': Service.is_service_urgent(k[0], k[1]),
                            'details': [('create', v)],
                            }])


class RemoveControlStart(ModelView):
    'Remove Control'
    __name__ = 'lims.planification.remove_control.start'

    controls = fields.Many2Many('lims.fraction', None, None, 'Controls',
        required=True, domain=[('id', 'in', Eval('controls_domain'))],
        depends=['controls_domain'])
    controls_domain = fields.Many2Many('lims.fraction', None, None,
        'Controls domain')


class RemoveControl(Wizard):
    'Remove Control'
    __name__ = 'lims.planification.remove_control'

    start = StateView('lims.planification.remove_control.start',
        'lims.lims_remove_control_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Remove', 'remove', 'tryton-ok', default=True),
            ])
    remove = StateTransition()

    def default_start(self, fields):
        Planification = Pool().get('lims.planification')
        planification = Planification(Transaction().context['active_id'])
        controls_domain = []
        for c in planification.controls:
            controls_domain.append(c.id)
        return {
            'controls_domain': controls_domain,
            }

    def transition_remove(self):
        planification_id = Transaction().context['active_id']
        control_ids = [c.id for c in self.start.controls]
        self._unlink_controls(planification_id, control_ids)
        return 'end'

    def _unlink_controls(self, planification_id, control_ids):
        pool = Pool()
        PlanificationFraction = pool.get('lims.planification-fraction')
        PlanificationDetail = pool.get('lims.planification.detail')

        controls = PlanificationFraction.search([
            ('planification', '=', planification_id),
            ('fraction', 'in', control_ids),
            ])
        if controls:
            PlanificationFraction.delete(controls)

        controls_details = PlanificationDetail.search([
            ('planification', '=', planification_id),
            ('fraction', 'in', control_ids),
            ])
        if controls_details:
            PlanificationDetail.delete(controls_details)


class AddAnalysisStart(ModelView):
    'Add Analysis'
    __name__ = 'lims.planification.add_analysis.start'

    laboratory = fields.Many2One('lims.laboratory', 'Laboratory')
    date_from = fields.Date('Date from', readonly=True)
    date_to = fields.Date('Date to', readonly=True)
    analysis = fields.Many2Many('lims.analysis', None, None,
        'Analysis/Sets/Groups', required=True,
        domain=[('id', 'in', Eval('analysis_domain'))],
        context={'date_from': Eval('date_from'), 'date_to': Eval('date_to')},
        depends=['analysis_domain', 'date_from', 'date_to'])
    analysis_domain = fields.Many2Many('lims.analysis', None, None,
        'Analysis domain')


class AddAnalysis(Wizard):
    'Add Analysis'
    __name__ = 'lims.planification.add_analysis'

    start = StateView('lims.planification.add_analysis.start',
        'lims.lims_add_analysis_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Add', 'add', 'tryton-ok', default=True),
            ])
    add = StateTransition()

    def default_start(self, fields):
        Planification = Pool().get('lims.planification')
        planification = Planification(Transaction().context['active_id'])
        analysis_domain = self._get_analysis_domain(planification)
        return {
            'laboratory': planification.laboratory.id,
            'analysis_domain': analysis_domain,
            'date_from': planification.date_from,
            'date_to': planification.date_to,
            }

    @staticmethod
    def _get_analysis_domain(planification):
        pool = Pool()
        Planification = pool.get('lims.planification')
        Analysis = pool.get('lims.analysis')

        asg_list = Planification._get_analysis_domain(planification.laboratory)

        new_context = {}
        new_context['date_from'] = planification.date_from
        new_context['date_to'] = planification.date_to
        with Transaction().set_context(new_context):
            pending_fractions = Analysis.analysis_pending_fractions(asg_list)
        res = []
        for analysis, pending in iter(pending_fractions.items()):
            if pending > 0:
                res.append(analysis)
        return res

    def transition_add(self):
        Planification = Pool().get('lims.planification')

        planification = Planification(Transaction().context['active_id'])

        Planification.write([planification], {
            'analysis': [('remove', self.start.analysis)],
            })
        Planification.write([planification], {
            'analysis': [('add', self.start.analysis)],
            })
        return 'end'


class SearchFractionsNext(ModelView):
    'Search Fractions'
    __name__ = 'lims.planification.search_fractions.next'

    details = fields.Many2Many(
        'lims.planification.search_fractions.detail',
        None, None, 'Fractions to plan', depends=['details_domain'],
        domain=[('id', 'in', Eval('details_domain'))], required=True)
    details_domain = fields.One2Many(
        'lims.planification.search_fractions.detail',
        None, 'Fractions domain')


class SearchFractionsDetail(ModelSQL, ModelView):
    'Fraction to Plan'
    __name__ = 'lims.planification.search_fractions.detail'
    _table = 'lims_plan_search_fractions_detail'

    fraction = fields.Many2One('lims.fraction', 'Fraction', readonly=True)
    service_analysis = fields.Many2One('lims.analysis', 'Service',
        readonly=True)
    type = fields.Function(fields.Many2One('lims.fraction.type',
        'Fraction type'), 'get_fraction_field',
        searcher='search_fraction_field')
    label = fields.Function(fields.Char('Label'), 'get_fraction_field',
        searcher='search_fraction_field')
    product_type = fields.Function(fields.Many2One('lims.product.type',
        'Product type'), 'get_fraction_field',
        searcher='search_fraction_field')
    matrix = fields.Function(fields.Many2One('lims.matrix', 'Matrix'),
        'get_fraction_field', searcher='search_fraction_field')
    create_date2 = fields.Function(fields.DateTime('Create Date'),
       'get_fraction_field', searcher='search_fraction_field')
    urgent = fields.Function(fields.Boolean('Urgent'), 'get_service_field',
        searcher='search_urgent')
    priority = fields.Function(fields.Integer('Priority'), 'get_service_field')
    repetition = fields.Boolean('Repetition', readonly=True)
    laboratory_date = fields.Function(fields.Date('Laboratory deadline'),
        'get_service_field')
    report_date = fields.Function(fields.Date('Date agreed for result'),
        'get_service_field')
    results_estimated_date = fields.Function(fields.Date(
        'Estimated date of result'), 'get_results_estimated_date')
    completion_percentage = fields.Function(fields.Numeric('Complete',
        digits=(1, 4)), 'get_completion_percentage')
    department = fields.Function(fields.Many2One('company.department',
        'Department'), 'get_department', searcher='search_department')
    sample_state = fields.Function(fields.Selection(
        'get_sample_states', 'State'), 'get_sample_state')
    session_id = fields.Integer('Session ID')

    @classmethod
    def __register__(cls, module_name):
        super().__register__(module_name)
        cursor = Transaction().connection.cursor()
        cursor.execute('DELETE FROM "' + cls._table + '"')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('fraction', 'ASC'))
        cls._order.insert(1, ('service_analysis', 'ASC'))

    @classmethod
    def get_sample_states(cls):
        pool = Pool()
        Sample = pool.get('lims.sample')
        return Sample.fields_get(['state'])['state']['selection']

    @classmethod
    def get_sample_state(cls, details, name):
        result = {}
        for d in details:
            result[d.id] = getattr(d.fraction.sample, 'state', None)
        return result

    @classmethod
    def get_entry_field(cls, details, names):
        result = {}
        for name in names:
            result[name] = {}
            if cls._fields[name]._type == 'many2one':
                for d in details:
                    field = getattr(d.fraction.sample.entry, name, None)
                    result[name][d.id] = field.id if field else None
            else:
                for d in details:
                    result[name][d.id] = getattr(
                        d.fraction.sample.entry, name, None)
        return result

    @classmethod
    def search_entry_field(cls, name, clause):
        return [('fraction.sample.entry.' + name,) + tuple(clause[1:])]

    @classmethod
    def get_sample_field(cls, details, names):
        result = {}
        for name in names:
            result[name] = {}
            if cls._fields[name]._type == 'many2one':
                for d in details:
                    field = getattr(d.fraction.sample, name, None)
                    result[name][d.id] = field.id if field else None
            else:
                for d in details:
                    result[name][d.id] = getattr(d.fraction.sample, name, None)
        return result

    @classmethod
    def search_sample_field(cls, name, clause):
        return [('fraction.sample.' + name,) + tuple(clause[1:])]

    def _order_sample_field(name):
        def order_field(tables):
            pool = Pool()
            Sample = pool.get('lims.sample')
            Fraction = pool.get('lims.fraction')
            field = Sample._fields[name]
            table, _ = tables[None]
            fraction_tables = tables.get('fraction')
            if fraction_tables is None:
                fraction = Fraction.__table__()
                fraction_tables = {
                    None: (fraction, fraction.id == table.fraction),
                    }
                tables['fraction'] = fraction_tables
            return field.convert_order(name, fraction_tables, Fraction)
        return staticmethod(order_field)
    order_equipment = _order_sample_field('equipment')

    @classmethod
    def get_fraction_field(cls, details, names):
        result = {}
        for name in names:
            result[name] = {}
            if cls._fields[name]._type == 'many2one':
                for d in details:
                    field = getattr(d.fraction, name, None)
                    result[name][d.id] = field.id if field else None
            else:
                for d in details:
                    result[name][d.id] = getattr(d.fraction, name, None)
        return result

    @classmethod
    def search_fraction_field(cls, name, clause):
        return [('fraction.' + name,) + tuple(clause[1:])]

    def _order_fraction_field(name):
        def order_field(tables):
            Fraction = Pool().get('lims.fraction')
            field = Fraction._fields[name]
            table, _ = tables[None]
            fraction_tables = tables.get('fraction')
            if fraction_tables is None:
                fraction = Fraction.__table__()
                fraction_tables = {
                    None: (fraction, fraction.id == table.fraction),
                    }
                tables['fraction'] = fraction_tables
            return field.convert_order(name, fraction_tables, Fraction)
        return staticmethod(order_field)
    # Redefine convert_order function with 'order_%s' % field
    order_product_type = _order_fraction_field('product_type')
    order_matrix = _order_fraction_field('matrix')

    @classmethod
    def get_service_field(cls, details, names):
        result = {}
        for name in names:
            if name == 'urgent':
                result[name] = dict((d.id, False) for d in details)
            elif name == 'priority':
                result[name] = dict((d.id, 0) for d in details)
            else:
                result[name] = dict((d.id, None) for d in details)
        for d in details:
            if d.fraction and d.service_analysis:
                for service in d.fraction.services:
                    if service.analysis == d.service_analysis:
                        for name in names:
                            result[name][d.id] = getattr(service, name)
        return result

    @classmethod
    def search_urgent(cls, name, clause):
        cursor = Transaction().connection.cursor()
        Service = Pool().get('lims.service')

        cursor.execute('SELECT fd.id '
            'FROM "' + cls._table + '" fd '
                'INNER JOIN "' + Service._table + '" s '
                'ON (fd.fraction = s.fraction '
                'AND fd.service_analysis = s.analysis) '
            'WHERE s.urgent = TRUE')
        urgent_services = [x[0] for x in cursor.fetchall()]

        field, op, operand = clause
        if (op, operand) in (('=', True), ('!=', False)):
            return [('id', 'in', urgent_services)]
        return [('id', 'not in', urgent_services)]

    @classmethod
    def get_results_estimated_date(cls, details, name):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        Service = pool.get('lims.service')
        Notebook = pool.get('lims.notebook')

        result = {}
        for d in details:
            cursor.execute('SELECT ad.confirmation_date, '
                    'nl.results_estimated_waiting '
                'FROM "' + NotebookLine._table + '" nl '
                    'INNER JOIN "' + EntryDetailAnalysis._table + '" ad '
                    'ON ad.id = nl.analysis_detail '
                    'INNER JOIN "' + Service._table + '" srv '
                    'ON srv.id = nl.service '
                    'INNER JOIN "' + Notebook._table + '" n '
                    'ON n.id = nl.notebook '
                'WHERE n.fraction = %s '
                    'AND srv.analysis = %s '
                    'AND ad.confirmation_date IS NOT NULL '
                    'AND nl.results_estimated_waiting IS NOT NULL',
                (str(d.fraction.id), str(d.service_analysis.id)))
            date = None
            for x in cursor.fetchall():
                line_date = NotebookLine._get_results_estimated_date(
                    x[0], x[1])
                if not line_date:
                    continue
                if not date or date > line_date:
                    date = line_date
            result[d.id] = date
        return result

    @classmethod
    def get_completion_percentage(cls, details, name):
        result = {}
        for d in details:
            result[d.id] = getattr(d.fraction.sample, name, None)
        return result

    @classmethod
    def get_department(cls, details, name):
        result = {}
        for d in details:
            field = getattr(d.product_type, name, None)
            result[d.id] = field.id if field else None
        return result

    @classmethod
    def search_department(cls, name, clause):
        return [('fraction.sample.product_type.' + name,) + tuple(clause[1:])]


class SearchFractions(Wizard):
    'Search Fractions'
    __name__ = 'lims.planification.search_fractions'

    start_state = 'search'
    search = StateTransition()
    next = StateView('lims.planification.search_fractions.next',
        'lims.lims_search_fractions_next_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Add', 'add', 'tryton-ok', default=True),
            ])
    add = StateTransition()

    def transition_search(self):
        pool = Pool()
        Planification = pool.get('lims.planification')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        PlanificationDetail = pool.get('lims.planification.detail')
        SearchFractionsDetail = pool.get(
            'lims.planification.search_fractions.detail')

        planification = Planification(Transaction().context['active_id'])

        service_detail = PlanificationServiceDetail.search([
            ('planification', '=', planification.id),
            ('is_control', '=', False),
            ('is_replanned', '=', False),
            ])
        if service_detail:
            PlanificationServiceDetail.delete(service_detail)

        details = PlanificationDetail.search([
            ('planification', '=', planification.id),
            ('details', '=', None),
            ])
        if details:
            PlanificationDetail.delete(details)

        fractions_added = []
        if not planification.analysis:
            self.next.details = fractions_added
            return 'next'

        data = self._get_service_details(planification)

        to_create = []
        for k, v in data.items():
            to_create.append({
                'session_id': self._session_id,
                'fraction': k[0],
                'service_analysis': k[1],
                'repetition': v['repetition'],
                })
        fractions_added = SearchFractionsDetail.create(to_create)

        self.next.details = fractions_added
        return 'next'

    def default_next(self, fields):
        details = [d.id for d in self.next.details]
        self.next.details = None
        return {
            'details': [],
            'details_domain': details,
            }

    def transition_add(self):
        pool = Pool()
        Planification = pool.get('lims.planification')
        PlanificationDetail = pool.get('lims.planification.detail')
        Service = pool.get('lims.service')

        planification = Planification(Transaction().context['active_id'])

        records_added = ['(%s,%s)' % (d.fraction.id, d.service_analysis.id)
            for d in self.next.details]
        records_ids_added = ', '.join(str(x)
            for x in ['(0,0)'] + records_added)
        extra_where = (
            'AND (nb.fraction, srv.analysis) IN (' + records_ids_added + ') ')

        data = self._get_service_details(planification, extra_where)

        to_create = []
        for k, v in data.items():
            details = PlanificationDetail.search([
                ('planification', '=', planification.id),
                ('fraction', '=', k[0]),
                ('service_analysis', '=', k[1]),
                ])
            if details:
                PlanificationDetail.write([details[0]], {
                    'details': [('create', v)],
                    })
            else:
                to_create.append({
                    'planification': planification.id,
                    'fraction': k[0],
                    'service_analysis': k[1],
                    'urgent': Service.is_service_urgent(k[0], k[1]),
                    'details': [('create', v)],
                    })
        if to_create:
            PlanificationDetail.create(to_create)

        return 'end'

    def _get_service_details(self, planification, extra_where=''):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')
        Fraction = pool.get('lims.fraction')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        Service = pool.get('lims.service')
        Analysis = pool.get('lims.analysis')

        planification_details = PlanificationServiceDetail.search([
            ('planification.state', '=', 'preplanned'),
            ('notebook_line', '!=', None),
            ])
        planned_lines = [pd.notebook_line.id for pd in planification_details]
        planned_lines_ids = ', '.join(str(x) for x in [0] + planned_lines)
        preplanned_where = 'AND nl.id NOT IN (%s) ' % planned_lines_ids

        dates_where = ''
        if planification.date_from:
            dates_where += ('AND ad.confirmation_date::date >= \'%s\'::date ' %
                planification.date_from)
        if planification.date_to:
            dates_where += ('AND ad.confirmation_date::date <= \'%s\'::date ' %
                planification.date_to)

        result = {}
        nlines_added = []
        for a in planification.analysis:
            analysis_id = a.id

            all_included_analysis = [analysis_id]
            all_included_analysis.extend(
                Analysis.get_included_analysis_analysis(analysis_id))
            all_included_analysis_ids = ', '.join(str(x)
                for x in all_included_analysis)
            service_where = ('AND ad.analysis IN (%s) ' %
                all_included_analysis_ids)

            sql_select = (
                'SELECT nl.id, nb.fraction, srv.analysis, nl.repetition != 0 ')

            sql_from = (
                'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + Analysis._table + '" nla '
                'ON nla.id = nl.analysis '
                'INNER JOIN "' + Notebook._table + '" nb '
                'ON nb.id = nl.notebook '
                'INNER JOIN "' + Fraction._table + '" frc '
                'ON frc.id = nb.fraction '
                'INNER JOIN "' + EntryDetailAnalysis._table + '" ad '
                'ON ad.id = nl.analysis_detail '
                'INNER JOIN "' + Service._table + '" srv '
                'ON srv.id = nl.service ')

            sql_where = (
                'WHERE ad.plannable = TRUE '
                'AND nl.start_date IS NULL '
                'AND nl.annulled = FALSE '
                'AND nl.laboratory = %s '
                'AND nla.behavior != \'internal_relation\' ' +
                preplanned_where + dates_where + service_where + extra_where)

            sql_order = (
                'ORDER BY nb.fraction ASC, srv.analysis ASC')

            with Transaction().set_user(0):
                cursor.execute(sql_select + sql_from + sql_where + sql_order,
                    (planification.laboratory.id,))
            notebook_lines = cursor.fetchall()
            if not notebook_lines:
                continue
            if extra_where:
                for nl in notebook_lines:
                    f_ = nl[1]
                    s_ = nl[2]
                    if (f_, s_) not in result:
                        result[(f_, s_)] = []
                    if nl[0] not in nlines_added:
                        nlines_added.append(nl[0])
                        result[(f_, s_)].append({
                            'notebook_line': nl[0],
                            'planned_service': a.id,
                            })
            else:
                for nl in notebook_lines:
                    f_ = nl[1]
                    s_ = nl[2]
                    result[(f_, s_)] = {
                        'repetition': nl[3],
                        }

        return result


class SearchPlannedFractionsStart(ModelView):
    'Search Planned Fractions'
    __name__ = 'lims.planification.search_planned_fractions.start'

    laboratory = fields.Many2One('lims.laboratory', 'Laboratory')
    date_from = fields.Date('Date from')
    date_to = fields.Date('Date to')
    analysis = fields.Many2Many('lims.planification-analysis',
        'planification', 'analysis', 'Analysis/Sets/Groups',
        domain=[('id', 'in', Eval('analysis_domain'))],
        depends=['analysis_domain'], required=True)
    analysis_domain = fields.One2Many('lims.analysis', None,
        'Analysis domain')


class SearchPlannedFractionsNext(ModelView):
    'Search Planned Fractions'
    __name__ = 'lims.planification.search_planned_fractions.next'

    details = fields.Many2Many(
        'lims.planification.search_fractions.detail',
        None, None, 'Fractions to replan', depends=['details_domain'],
        domain=[('id', 'in', Eval('details_domain'))], required=True)
    details_domain = fields.One2Many(
        'lims.planification.search_fractions.detail',
        None, 'Fractions domain')


class SearchPlannedFractions(Wizard):
    'Search Planned Fractions'
    __name__ = 'lims.planification.search_planned_fractions'

    start = StateView('lims.planification.search_planned_fractions.start',
        'lims.lims_search_planned_fractions_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Search', 'search', 'tryton-forward', default=True),
            ])
    search = StateTransition()
    next = StateView('lims.planification.search_planned_fractions.next',
        'lims.lims_search_planned_fractions_next_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Add', 'add', 'tryton-ok', default=True),
            ])
    add = StateTransition()

    def default_start(self, fields):
        Planification = Pool().get('lims.planification')
        planification = Planification(Transaction().context['active_id'])
        analysis = [a.id for a in planification.analysis]
        return {
            'laboratory': planification.laboratory.id,
            'analysis': analysis,
            'analysis_domain': analysis,
            'date_from': planification.date_from,
            'date_to': planification.date_to,
            }

    def transition_search(self):
        pool = Pool()
        SearchFractionsDetail = pool.get(
            'lims.planification.search_fractions.detail')

        data = self._get_service_details()

        to_create = []
        for k, v in data.items():
            to_create.append({
                'session_id': self._session_id,
                'fraction': k[0],
                'service_analysis': k[1],
                'repetition': v['repetition'],
                })
        fractions_added = SearchFractionsDetail.create(to_create)
        self.next.details = fractions_added
        return 'next'

    def default_next(self, fields):
        details = [d.id for d in self.next.details]
        self.next.details = None
        return {
            'details': [],
            'details_domain': details,
            }

    def transition_add(self):
        pool = Pool()
        Planification = pool.get('lims.planification')
        PlanificationDetail = pool.get('lims.planification.detail')
        Service = pool.get('lims.service')

        planification = Planification(Transaction().context['active_id'])

        records_added = ['(%s,%s)' % (d.fraction.id, d.service_analysis.id)
            for d in self.next.details]
        records_ids_added = ', '.join(str(x)
            for x in ['(0,0)'] + records_added)
        extra_where = (
            'AND (nb.fraction, srv.analysis) IN (' + records_ids_added + ') ')

        data = self._get_service_details(extra_where)

        to_create = []
        for k, v in data.items():
            details = PlanificationDetail.search([
                ('planification', '=', planification.id),
                ('fraction', '=', k[0]),
                ('service_analysis', '=', k[1]),
                ])
            if details:
                PlanificationDetail.write([details[0]], {
                    'details': [('create', v)],
                    })
            else:
                to_create.append({
                    'planification': planification.id,
                    'fraction': k[0],
                    'service_analysis': k[1],
                    'urgent': Service.is_service_urgent(k[0], k[1]),
                    'details': [('create', v)],
                    })
        if to_create:
            PlanificationDetail.create(to_create)

        return 'end'

    def _get_service_details(self, extra_where=''):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')
        Fraction = pool.get('lims.fraction')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        Service = pool.get('lims.service')
        Analysis = pool.get('lims.analysis')

        dates_where = ''
        if self.start.date_from:
            dates_where += ('AND ad.confirmation_date::date >= \'%s\'::date ' %
                self.start.date_from)
        if self.start.date_to:
            dates_where += ('AND ad.confirmation_date::date <= \'%s\'::date ' %
                self.start.date_to)

        result = {}
        nlines_added = []
        for a in self.start.analysis:
            analysis_id = a.id

            all_included_analysis = [analysis_id]
            all_included_analysis.extend(
                Analysis.get_included_analysis_analysis(analysis_id))
            all_included_analysis_ids = ', '.join(str(x)
                for x in all_included_analysis)
            service_where = ('AND ad.analysis IN (%s) ' %
                all_included_analysis_ids)

            excluded_fractions = self.get_control_fractions_excluded(
                self.start.laboratory.id, service_where)
            excluded_fractions_ids = ', '.join(str(x)
                for x in [0] + excluded_fractions)
            excluded_where = ('AND nb.fraction NOT IN (%s) ' %
                excluded_fractions_ids)

            sql_select = (
                'SELECT nl.id, nb.fraction, srv.analysis, nl.repetition != 0 ')

            sql_from = (
                'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + Analysis._table + '" nla '
                'ON nla.id = nl.analysis '
                'INNER JOIN "' + Notebook._table + '" nb '
                'ON nb.id = nl.notebook '
                'INNER JOIN "' + Fraction._table + '" frc '
                'ON frc.id = nb.fraction '
                'INNER JOIN "' + EntryDetailAnalysis._table + '" ad '
                'ON ad.id = nl.analysis_detail '
                'INNER JOIN "' + Service._table + '" srv '
                'ON srv.id = nl.service ')

            sql_where = (
                'WHERE ad.plannable = TRUE '
                'AND nl.start_date IS NOT NULL '
                'AND nl.end_date IS NULL '
                'AND nl.laboratory = %s '
                'AND nla.behavior != \'internal_relation\' ' +
                excluded_where + dates_where + service_where + extra_where)

            sql_order = (
                'ORDER BY nb.fraction ASC, srv.analysis ASC')

            with Transaction().set_user(0):
                cursor.execute(sql_select + sql_from + sql_where + sql_order,
                    (self.start.laboratory.id,))
            notebook_lines = cursor.fetchall()
            if notebook_lines:
                if extra_where:
                    for nl in notebook_lines:
                        f_ = nl[1]
                        s_ = nl[2]
                        if (f_, s_) not in result:
                            result[(f_, s_)] = []
                        if nl[0] not in nlines_added:
                            nlines_added.append(nl[0])
                            result[(f_, s_)].append({
                                'notebook_line': nl[0],
                                'planned_service': a.id,
                                'is_replanned': True,
                                })
                else:
                    for nl in notebook_lines:
                        f_ = nl[1]
                        s_ = nl[2]
                        result[(f_, s_)] = {
                            'repetition': nl[3],
                            }

        return result

    def get_control_fractions_excluded(self, laboratory, search_clause):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')
        Fraction = pool.get('lims.fraction')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        Config = pool.get('lims.configuration')

        config = Config(1)
        special_types = []
        if config.rm_fraction_type:
            special_types.append(config.rm_fraction_type.id)
        if config.bmz_fraction_type:
            special_types.append(config.bmz_fraction_type.id)
        if config.bre_fraction_type:
            special_types.append(config.bre_fraction_type.id)
        if config.mrt_fraction_type:
            special_types.append(config.mrt_fraction_type.id)
        special_types_ids = ', '.join(str(x) for x in [0] + special_types)

        cursor.execute('SELECT nl.analysis, nl.notebook '
            'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + Notebook._table + '" nb '
                'ON nb.id = nl.notebook '
                'INNER JOIN "' + Fraction._table + '" frc '
                'ON frc.id = nb.fraction '
                'INNER JOIN "' + EntryDetailAnalysis._table + '" ad '
                'ON ad.id = nl.analysis_detail '
            'WHERE frc.type IN (' + special_types_ids + ') '
                'AND nl.start_date IS NOT NULL '
                'AND nl.end_date IS NULL '
                'AND nl.laboratory = %s ' +
                search_clause, (laboratory,))
        notebook_lines = cursor.fetchall()
        if not notebook_lines:
            return []

        excluded_fractions = []
        analysis = []
        notebooks_id = []
        for nbl in notebook_lines:
            analysis.append(nbl[0])
            notebooks_id.append(nbl[1])
        analysis = set(analysis)

        notebooks = Notebook.search([
            ('id', 'in', list(set(notebooks_id))),
            ])
        for nb in notebooks:
            cursor.execute('SELECT analysis '
                'FROM "' + NotebookLine._table + '" '
                'WHERE notebook = %s', (nb.id,))
            nbl_analysis_ids = set(cursor.fetchall())
            if not analysis.issubset(nbl_analysis_ids):
                excluded_fractions.append(nb.fraction.id)
        return list(set(excluded_fractions))


class CreateFractionControlStart(ModelView):
    'Create Fraction Control'
    __name__ = 'lims.planification.create_fraction_con.start'

    laboratory = fields.Many2One('lims.laboratory', 'Laboratory',
        required=True)
    type = fields.Selection([
        ('coi', 'COI'),
        ('mrc', 'MRC'),
        ('sla', 'SLA'),
        ], 'Control type', sort=False, required=True)
    product_type = fields.Many2One('lims.product.type', 'Product type',
        required=True,
        domain=[('id', 'in', Eval('product_type_domain'))],
        depends=['product_type_domain'])
    product_type_domain = fields.Function(fields.Many2Many(
        'lims.product.type', None, None, 'Product type domain'),
        'on_change_with_product_type_domain')
    matrix = fields.Many2One('lims.matrix', 'Matrix', required=True,
        domain=[('id', 'in', Eval('matrix_domain'))],
        depends=['matrix_domain'])
    matrix_domain = fields.Function(fields.Many2Many('lims.matrix',
        None, None, 'Matrix domain'),
        'on_change_with_matrix_domain')
    analysis = fields.Many2Many('lims.planification-analysis',
        'planification', 'analysis', 'Analysis/Sets/Groups',
        domain=[('id', 'in', Eval('analysis_domain'))],
        depends=['analysis_domain'])
    analysis_domain = fields.Function(fields.One2Many('lims.analysis',
        None, 'Analysis domain'), 'on_change_with_analysis_domain')
    label = fields.Char('Label')
    concentration_level = fields.Many2One('lims.concentration.level',
        'Concentration level', states={
            'invisible': Bool(Eval('concentration_level_invisible')),
            }, depends=['concentration_level_invisible'])
    concentration_level_invisible = fields.Boolean(
        'Concentration level invisible')
    sample_created = fields.Many2One('lims.sample', 'Sample created')

    @fields.depends('type')
    def on_change_with_concentration_level_invisible(self, name=None):
        Config = Pool().get('lims.configuration')
        config = Config(1)
        if self.type == 'coi':
            if (config.coi_fraction_type and
                    config.coi_fraction_type.control_charts):
                return False
        elif self.type == 'mrc':
            if (config.mrc_fraction_type and
                    config.mrc_fraction_type.control_charts):
                return False
        elif self.type == 'sla':
            if (config.sla_fraction_type and
                    config.sla_fraction_type.control_charts):
                return False
        return True

    @fields.depends('laboratory', 'product_type', 'matrix')
    def on_change_with_analysis_domain(self, name=None):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        AnalysisLaboratory = pool.get('lims.analysis-laboratory')
        Analysis = pool.get('lims.analysis')
        Typification = pool.get('lims.typification')
        CalculatedTypification = pool.get('lims.typification.calculated')

        if not self.laboratory or not self.product_type or not self.matrix:
            return []

        cursor.execute('SELECT DISTINCT(al.analysis) '
            'FROM "' + AnalysisLaboratory._table + '" al '
                'INNER JOIN "' + Analysis._table + '" a '
                'ON a.id = al.analysis '
            'WHERE al.laboratory = %s '
                'AND a.type = \'analysis\'',
            (self.laboratory.id,))
        analyzes_list = [a[0] for a in cursor.fetchall()]
        if not analyzes_list:
            return []
        lab_analysis_ids = ', '.join(str(a) for a in analyzes_list)

        others_list = []
        others = Analysis.search([
            ('type', '!=', 'analysis'),
            ])
        for set_group in others:
            available = True

            ia = Analysis.get_included_analysis_analysis(set_group.id)
            if not ia:
                continue
            included_ids = ', '.join(str(a) for a in ia)

            cursor.execute('SELECT id '
                'FROM "' + Analysis._table + '" '
                'WHERE id IN (' + included_ids + ') '
                    'AND id NOT IN (' + lab_analysis_ids + ')')
            if cursor.fetchone():
                available = False

            if available:
                others_list.append(set_group.id)

        analysis_domain = analyzes_list + others_list
        analysis_domain_ids = ', '.join(str(a) for a in analysis_domain)

        cursor.execute('SELECT DISTINCT(typ.analysis) '
            'FROM ('
                'SELECT t.analysis FROM "' + Typification._table + '" t '
                'WHERE t.product_type = %s AND t.matrix = %s AND t.valid '
            'UNION '
                'SELECT ct.analysis FROM "' + CalculatedTypification._table +
                '" ct '
                'WHERE ct.product_type = %s AND ct.matrix = %s'
            ') AS typ '
            'WHERE typ.analysis IN (' + analysis_domain_ids + ')',
            (self.product_type.id, self.matrix.id,
            self.product_type.id, self.matrix.id))
        typified_analysis = [a[0] for a in cursor.fetchall()]
        return typified_analysis

    @staticmethod
    def default_product_type_domain():
        cursor = Transaction().connection.cursor()
        Typification = Pool().get('lims.typification')

        cursor.execute('SELECT DISTINCT(product_type) '
                'FROM "' + Typification._table + '" '
                'WHERE valid')
        return [x[0] for x in cursor.fetchall()]

    def on_change_with_product_type_domain(self, name=None):
        return self.default_product_type_domain()

    @fields.depends('product_type')
    def on_change_product_type(self):
        matrix = None
        if self.product_type:
            matrixs = self.on_change_with_matrix_domain()
            if len(matrixs) == 1:
                matrix = matrixs[0]
        self.matrix = matrix

    @fields.depends('product_type')
    def on_change_with_matrix_domain(self, name=None):
        cursor = Transaction().connection.cursor()
        Typification = Pool().get('lims.typification')

        if not self.product_type:
            return []

        cursor.execute('SELECT DISTINCT(matrix) '
                'FROM "' + Typification._table + '" '
                'WHERE product_type = %s '
                'AND valid',
                (self.product_type.id,))
        return [x[0] for x in cursor.fetchall()]

    @fields.depends('type', 'product_type', 'matrix',
        '_parent_product_type.code', '_parent_matrix.code')
    def on_change_with_label(self, name=None):
        label = ''
        if self.type == 'coi':
            label += 'COI'
        elif self.type == 'mrc':
            label += 'MRC'
        elif self.type == 'sla':
            label += 'SLA'
        if self.product_type:
            label += ' ' + self.product_type.code
        if self.matrix:
            label += ' ' + self.matrix.code
        return label


class CreateFractionControl(Wizard):
    'Create Fraction Control'
    __name__ = 'lims.planification.create_fraction_con'

    start = StateView('lims.planification.create_fraction_con.start',
        'lims.lims_create_fraction_con_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Create', 'create_', 'tryton-ok', default=True),
            ])
    create_ = StateTransition()
    open_ = StateAction('lims.act_lims_sample_list')

    def default_start(self, fields):
        defaults = {
            'laboratory': Transaction().context.get('laboratory', None),
            'concentration_level_invisible': True,
            }
        return defaults

    def transition_create_(self):
        control = self.create_control()
        self.start.sample_created = control.sample.id
        return 'open_'

    def create_control(self):
        pool = Pool()
        Config = pool.get('lims.configuration')
        LabWorkYear = pool.get('lims.lab.workyear')
        Entry = pool.get('lims.entry')
        Sample = pool.get('lims.sample')
        Fraction = pool.get('lims.fraction')
        Service = pool.get('lims.service')
        NotebookLine = pool.get('lims.notebook.line')

        config = Config(1)
        if self.start.type == 'coi':
            if not config.coi_fraction_type:
                raise UserError(gettext('lims.msg_no_coi_fraction_type'))
            fraction_type = config.coi_fraction_type
            if (not fraction_type.default_package_type or
                    not fraction_type.default_fraction_state):
                raise UserError(gettext(
                    'lims.msg_no_coi_default_configuration'))
        elif self.start.type == 'mrc':
            if not config.mrc_fraction_type:
                raise UserError(gettext('lims.msg_no_mrc_fraction_type'))
            fraction_type = config.mrc_fraction_type
            if (not fraction_type.default_package_type or
                    not fraction_type.default_fraction_state):
                raise UserError(gettext(
                    'lims.msg_no_mrc_default_configuration'))
        elif self.start.type == 'sla':
            if not config.sla_fraction_type:
                raise UserError(gettext('lims.msg_no_sla_fraction_type'))
            fraction_type = config.sla_fraction_type
            if (not fraction_type.default_package_type or
                    not fraction_type.default_fraction_state):
                raise UserError(gettext(
                    'lims.msg_no_sla_default_configuration'))

        if (fraction_type.control_charts and not
                self.start.concentration_level):
            raise UserError(gettext('lims.msg_no_concentration_level'))

        workyear_id = LabWorkYear.find()
        workyear = LabWorkYear(workyear_id)
        if not workyear.default_entry_control:
            raise UserError(gettext('lims.msg_no_entry_control'))

        entry = Entry(workyear.default_entry_control.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

        laboratory = self.start.laboratory
        obj_description = self._get_obj_description(self.start)

        # new sample
        new_sample, = Sample.create([{
            'entry': entry.id,
            'party': entry.party.id,
            'date': datetime.now(),
            'product_type': self.start.product_type.id,
            'matrix': self.start.matrix.id,
            'zone': zone_id,
            'label': self.start.label,
            'obj_description': obj_description,
            'packages': [('create', [{
                'quantity': 1,
                'type': fraction_type.default_package_type.id,
                'state': fraction_type.default_fraction_state.id,
                }])],
            'fractions': [],
            }])

        # new fraction
        fraction_default = {
            'sample': new_sample.id,
            'type': fraction_type.id,
            'storage_location': laboratory.related_location.id,
            'services': [],
            'con_type': self.start.type,
            }
        if fraction_type.max_storage_time:
            fraction_default['storage_time'] = fraction_type.max_storage_time
        elif laboratory.related_location.storage_time:
            fraction_default['storage_time'] = (
                laboratory.related_location.storage_time)
        else:
            fraction_default['storage_time'] = 3
        new_fraction, = Fraction.create([fraction_default])

        # new services
        services_default = []
        for p_analysis in self.start.analysis:
            laboratory_id = (laboratory.id if p_analysis.type != 'group'
                else None)
            method_id = None
            if new_sample.typification_domain:
                for t in new_sample.typification_domain:
                    if (t.analysis.id == p_analysis.id and
                            t.by_default is True):
                        method_id = t.method.id
            device_id = None
            if p_analysis.devices:
                for d in p_analysis.devices:
                    if (d.laboratory.id == laboratory.id and
                            d.by_default is True):
                        device_id = d.device.id
            services_default.append({
                'fraction': new_fraction.id,
                'analysis': p_analysis.id,
                'laboratory': laboratory_id,
                'method': method_id,
                'device': device_id,
                })
        for service in services_default:
            new_service, = Service.create([service])

            # new analysis details (on service create)

        # confirm fraction: new notebook and stock move
        Fraction.confirm([new_fraction])

        # Edit notebook lines
        if fraction_type.control_charts:
            notebook_lines = NotebookLine.search([
                ('notebook.fraction', '=', new_fraction.id),
                ])
            if notebook_lines:
                defaults = {
                    'concentration_level': self.start.concentration_level.id,
                    }
                NotebookLine.write(notebook_lines, defaults)

        return new_fraction

    def _get_obj_description(self, sample):
        cursor = Transaction().connection.cursor()
        ObjectiveDescription = Pool().get('lims.objective_description')

        if not sample.product_type or not sample.matrix:
            return None

        cursor.execute('SELECT id '
            'FROM "' + ObjectiveDescription._table + '" '
            'WHERE product_type = %s '
                'AND matrix = %s',
            (sample.product_type.id, sample.matrix.id))
        res = cursor.fetchone()
        return res and res[0] or None

    def do_open_(self, action):
        action['pyson_domain'] = PYSONEncoder().encode([
            ('id', '=', self.start.sample_created.id),
            ])
        return action, {}


class ReleaseFractionStart(ModelView):
    'Release Fraction'
    __name__ = 'lims.planification.release_fraction.start'

    laboratory = fields.Many2One('lims.laboratory', 'Laboratory',
        required=True)
    date_from = fields.Date('Date from', required=True)
    date_to = fields.Date('Date to', required=True)


class ReleaseFractionEmpty(ModelView):
    'Release Fraction'
    __name__ = 'lims.planification.release_fraction.empty'


class ReleaseFractionResult(ModelView):
    'Release Fraction'
    __name__ = 'lims.planification.release_fraction.result'

    fractions = fields.Many2Many('lims.planification.detail', None, None,
        'Fractions', required=True,
        domain=[('id', 'in', Eval('fractions_domain'))],
        depends=['fractions_domain'])
    fractions_domain = fields.One2Many('lims.planification.detail', None,
        'Fractions domain')


class ReleaseFraction(Wizard):
    'Release Fraction'
    __name__ = 'lims.planification.release_fraction'

    start = StateView('lims.planification.release_fraction.start',
        'lims.lims_planification_release_fraction_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Search', 'search', 'tryton-forward', default=True),
            ])
    search = StateTransition()
    empty = StateView('lims.planification.release_fraction.empty',
        'lims.lims_planification_release_fraction_empty_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Search again', 'start', 'tryton-forward', default=True),
            ])
    result = StateView('lims.planification.release_fraction.result',
        'lims.lims_planification_release_fraction_result_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Release', 'release', 'tryton-ok', default=True),
            ])
    release = StateTransition()

    def default_start(self, fields):
        res = {}
        for field in ('date_from', 'date_to'):
            if (hasattr(self.start, field) and getattr(self.start, field)):
                res[field] = getattr(self.start, field)
        if (hasattr(self.start, 'laboratory') and
                getattr(self.start, 'laboratory')):
            res['laboratory'] = getattr(self.start, 'laboratory').id
        else:
            res['laboratory'] = Transaction().context.get('laboratory', None)
        return res

    def transition_search(self):
        PlanificationDetail = Pool().get('lims.planification.detail')

        details = PlanificationDetail.search([
            ('planification.laboratory', '=', self.start.laboratory.id),
            ('planification.start_date', '>=', self.start.date_from),
            ('planification.start_date', '<=', self.start.date_to),
            ('planification.state', '!=', 'draft'),
            ])
        if details:
            fractions = []
            for detail in details:
                available = True
                for service_detail in detail.details:
                    if service_detail.is_control:
                        available = False
                        break
                    nline = service_detail.notebook_line
                    if not nline:
                        available = False
                        break
                    if (nline.result or nline.converted_result or
                            nline.literal_result or nline.end_date or
                            nline.annulment_date):
                        available = False
                        break
                if available:
                    fractions.append(detail)

            if fractions:
                self.result.fractions = fractions
                return 'result'
        return 'empty'

    def default_result(self, fields):
        fractions = [f.id for f in self.result.fractions]
        self.result.fractions = None
        return {
            'fractions_domain': fractions,
            }

    def transition_release(self):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        NotebookLineProfessional = pool.get(
            'lims.notebook.line-laboratory.professional')
        NotebookLineControl = pool.get('lims.notebook.line-fraction')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        PlanificationDetail = pool.get('lims.planification.detail')

        notebook_lines_ids = []
        analysis_detail_ids = []
        details_ids = []
        service_details_ids = []

        for detail in self.result.fractions:
            for service_detail in detail.details:
                if service_detail.is_control:
                    continue
                if service_detail.notebook_line:
                    notebook_lines_ids.append(service_detail.notebook_line.id)
                    if service_detail.notebook_line.analysis_detail:
                        analysis_detail_ids.append(
                            service_detail.notebook_line.analysis_detail.id)
                service_details_ids.append(service_detail.id)
                details_ids.append(service_detail.detail.id)

        if notebook_lines_ids:
            notebook_lines_ids = ', '.join(str(nl)
                for nl in notebook_lines_ids)
            cursor.execute('UPDATE "' + NotebookLine._table + '" '
                'SET start_date = NULL, planification = NULL '
                'WHERE id IN (' + notebook_lines_ids + ')')
            cursor.execute('DELETE FROM "' +
                NotebookLineProfessional._table + '" '
                'WHERE notebook_line IN (' + notebook_lines_ids + ')')
            cursor.execute('DELETE FROM "' +
                NotebookLineControl._table + '" '
                'WHERE notebook_line IN (' + notebook_lines_ids + ')')

        if analysis_detail_ids:
            analysis_detail_ids = ', '.join(str(ad)
                for ad in analysis_detail_ids)
            cursor.execute('UPDATE "' + EntryDetailAnalysis._table + '" '
                'SET state = \'unplanned\' '
                'WHERE id IN (' + analysis_detail_ids + ')')

        if service_details_ids:
            service_details_ids = ', '.join(str(sd)
                for sd in service_details_ids)
            cursor.execute('DELETE FROM "' +
                PlanificationServiceDetail._table + '" '
                'WHERE id IN (' + service_details_ids + ')')

        details = PlanificationDetail.search([
            ('id', 'in', details_ids),
            ('details', '=', None),
            ])
        if details:
            PlanificationDetail.delete(details)

        return 'end'


class QualificationSituations(ModelView):
    'Technicians Qualification'
    __name__ = 'lims.planification.qualification.situations'

    situations = fields.Many2Many('lims.planification.qualification.situation',
        None, None, 'Situations')
    total = fields.Integer('Total')
    index = fields.Integer('Index')


class QualificationSituation(ModelSQL, ModelView):
    'Qualification Situation'
    __name__ = 'lims.planification.qualification.situation'

    method = fields.Many2One('lims.lab.method', 'Method', readonly=True)
    professional = fields.Many2One('lims.laboratory.professional',
        'Professional', readonly=True)
    situation = fields.Integer('Situation', readonly=True)
    session_id = fields.Integer('Session ID')

    @classmethod
    def __register__(cls, module_name):
        super().__register__(module_name)
        cursor = Transaction().connection.cursor()
        cursor.execute('DELETE FROM "' + cls._table + '"')


class QualificationAction(ModelSQL):
    'Qualification Action'
    __name__ = 'lims.planification.qualification.action'

    method = fields.Many2One('lims.lab.method', 'Method')
    professional = fields.Many2One('lims.laboratory.professional',
        'Professional')
    action = fields.Integer('Action')
    session_id = fields.Integer('Session ID')

    @classmethod
    def __register__(cls, module_name):
        super().__register__(module_name)
        cursor = Transaction().connection.cursor()
        cursor.execute('DELETE FROM "' + cls._table + '"')


class QualificationSituation2(ModelView):
    'Technicians Qualification'
    __name__ = 'lims.planification.qualification.situation.2'

    method = fields.Many2One('lims.lab.method', 'Method', readonly=True)
    professional = fields.Many2One('lims.laboratory.professional',
        'Professional', readonly=True)


class QualificationSituation3(ModelView):
    'Technicians Qualification'
    __name__ = 'lims.planification.qualification.situation.3'

    method = fields.Many2One('lims.lab.method', 'Method', readonly=True)
    professional = fields.Many2One('lims.laboratory.professional',
        'Professional', readonly=True)


class QualificationSituation4(ModelView):
    'Technicians Qualification'
    __name__ = 'lims.planification.qualification.situation.4'

    methods = fields.Text('Methods', readonly=True)


class TechniciansQualification(Wizard):
    'Technicians Qualification'
    __name__ = 'lims.planification.technicians_qualification'

    situations = StateView('lims.planification.qualification.situations',
        'lims.lims_qualification_situations_view_form', [])
    start = StateTransition()
    next_ = StateTransition()
    confirm = StateTransition()

    sit2 = StateView('lims.planification.qualification.situation.2',
        'lims.lims_qualification_situation_2_view_form', [
            Button('Qualify', 'sit2_op1', 'tryton-ok', default=True),
            Button('New Training', 'sit2_op2', 'tryton-ok'),
            Button('Cancel', 'end', 'tryton-cancel'),
            ])
    sit2_op1 = StateTransition()
    sit2_op2 = StateTransition()

    sit3 = StateView('lims.planification.qualification.situation.3',
        'lims.lims_qualification_situation_3_view_form', [
            Button('Requalify', 'sit3_op1', 'tryton-ok', default=True),
            Button('Cancel', 'end', 'tryton-cancel'),
            ])
    sit3_op1 = StateTransition()

    sit4 = StateView('lims.planification.qualification.situation.4',
        'lims.lims_qualification_situation_4_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            ])

    def transition_start(self):
        pool = Pool()
        Planification = pool.get('lims.planification')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        LabProfessionalMethod = pool.get('lims.lab.professional.method')
        QualificationSituation = pool.get(
            'lims.planification.qualification.situation')

        planification = Planification(Transaction().context['active_id'])
        planification.wizard_executed = False
        planification.save()

        planification_details = PlanificationServiceDetail.search([
            ('planification', '=', planification.id),
            ])

        situations = {}
        for detail in planification_details:
            method = (detail.notebook_line.method if detail.notebook_line
                else None)
            if method:
                for technician in detail.staff_responsible:
                    key = (technician.id, method.id)
                    if key in situations:
                        continue
                    situations[key] = 0
                    qualifications = LabProfessionalMethod.search([
                        ('professional', '=', technician.id),
                        ('method', '=', method.id),
                        ('type', '=', 'preparation'),
                        ])
                    if not qualifications:
                        situations[key] = 1
                    else:
                        if qualifications[0].state == 'training':
                            situations[key] = 2
                        elif (qualifications[0].state in
                                ('qualified', 'requalified')):
                            situations[key] = 3

        if situations:
            self.situations.situations = QualificationSituation.create([{
                'session_id': self._session_id,
                'professional': k[0],
                'method': k[1],
                'situation': v,
                } for k, v in situations.items()])
            self.situations.total = len(self.situations.situations)
            self.situations.index = 0
            return 'next_'
        return self._confirm()

    def transition_next_(self):

        data = self.situations.situations[self.situations.index]
        if data.situation == 1:
            return self.situation_1(data)
        elif data.situation == 2:
            return self.situation_2(data)
        elif data.situation == 3:
            return self.situation_3(data)
        return self._continue()

    def situation_1(self, data):
        # The technician has not the method. Write a new training directly.
        QualificationAction = Pool().get(
            'lims.planification.qualification.action')

        QualificationAction.create([{
            'session_id': self._session_id,
            'professional': data.professional.id,
            'method': data.method.id,
            'action': 1,
            }])
        return self._continue()

    def situation_2(self, data):
        # The technician has received at least one training. Ask if qualify
        # or write a new training.
        self.sit2.professional = data.professional.id
        self.sit2.method = data.method.id
        return 'sit2'

    def default_sit2(self, fields):
        professional = self.sit2.professional.id
        method = self.sit2.method.id
        self.sit2.professional = None
        self.sit2.method = None
        return {
            'professional': professional,
            'method': method,
            }

    def transition_sit2_op1(self):
        # Qualify the technician
        QualificationAction = Pool().get(
            'lims.planification.qualification.action')

        QualificationAction.create([{
            'session_id': self._session_id,
            'professional': self.sit2.professional.id,
            'method': self.sit2.method.id,
            'action': 2,
            }])
        return self._continue()

    def transition_sit2_op2(self):
        # Write a new training.
        QualificationAction = Pool().get(
            'lims.planification.qualification.action')

        QualificationAction.create([{
            'session_id': self._session_id,
            'professional': self.sit2.professional.id,
            'method': self.sit2.method.id,
            'action': 3,
            }])
        return self._continue()

    def situation_3(self, data):
        # The technician is qualified. Check the last execution date of the
        # method; if it's expired, ask if requalify or cancel the process.
        # If it is valid, write a new execution
        pool = Pool()
        LabProfessionalMethod = pool.get('lims.lab.professional.method')
        Date = pool.get('ir.date')
        QualificationAction = pool.get(
            'lims.planification.qualification.action')

        deadline = Date.today() - relativedelta(
            months=data.method.requalification_months)
        professional_method, = LabProfessionalMethod.search([
            ('professional', '=', data.professional),
            ('method', '=', data.method),
            ('type', '=', 'preparation'),
            ])
        last_execution = date.min
        for requalification in professional_method.requalification_history:
            if (requalification.last_execution_date and
                    last_execution < requalification.last_execution_date):
                last_execution = requalification.last_execution_date

        if last_execution < deadline:
            self.sit3.professional = data.professional.id
            self.sit3.method = data.method.id
            return 'sit3'
        else:
            QualificationAction.create([{
                'session_id': self._session_id,
                'professional': data.professional.id,
                'method': data.method.id,
                'action': 5,
                }])
        return self._continue()

    def default_sit3(self, fields):
        professional = self.sit3.professional.id
        method = self.sit3.method.id
        self.sit3.professional = None
        self.sit3.method = None
        return {
            'professional': professional,
            'method': method,
            }

    def transition_sit3_op1(self):
        # Requalify the technician
        QualificationAction = Pool().get(
            'lims.planification.qualification.action')

        QualificationAction.create([{
            'session_id': self._session_id,
            'professional': self.sit3.professional.id,
            'method': self.sit3.method.id,
            'action': 4,
            }])
        return self._continue()

    def situation_4(self, data):
        # The method has no qualified technician
        self.sit4.methods = data['methods']
        return 'sit4'

    def default_sit4(self, fields):
        methods = self.sit4.methods
        self.sit4.methods = None
        return {
            'methods': methods,
            }

    def _continue(self):
        self.situations.index += 1
        if self.situations.index < self.situations.total:
            return 'next_'
        return self._confirm()

    def _confirm(self):
        pool = Pool()
        Planification = pool.get('lims.planification')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        LabProfessionalMethod = pool.get('lims.lab.professional.method')
        QualificationAction = pool.get(
            'lims.planification.qualification.action')

        planification = Planification(Transaction().context['active_id'])

        planification_details = PlanificationServiceDetail.search([
            ('planification', '=', planification.id),
            ])
        methods = []
        for detail in planification_details:
            method = (detail.notebook_line.method if detail.notebook_line
                else None)
            if method:
                qualified = False
                for technician in detail.staff_responsible:
                    qualifications = LabProfessionalMethod.search([
                        ('professional', '=', technician.id),
                        ('method', '=', method.id),
                        ('type', '=', 'preparation'),
                        ])
                    if (qualifications and qualifications[0].state in
                            ('qualified', 'requalified')):
                        if not method.supervised_requalification:
                            qualified = True
                            break
                        actions = QualificationAction.search([
                            ('session_id', '=', self._session_id),
                            ('professional', '=', technician.id),
                            ('method', '=', method.id),
                            ('action', '=', 4),
                            ])
                        if not actions:
                            qualified = True
                            break
                if not qualified:
                    methods.append('[%s] %s' % (method.code, method.name))

        if methods:
            return self.situation_4({'methods': '\n'.join(list(set(methods)))})

        actions = QualificationAction.search([
            ('session_id', '=', self._session_id),
            ])
        if actions:
            controls = self._get_controls()
            start_date = planification.start_date
            for data in actions:
                if data.action == 1:
                    self.action_1(data, controls, start_date)
                elif data.action == 2:
                    self.action_2(data, controls, start_date)
                elif data.action == 3:
                    self.action_3(data, controls, start_date)
                elif data.action == 4:
                    self.action_4(data, controls, start_date)
                elif data.action == 5:
                    self.action_5(data, controls, start_date)
        return 'confirm'

    def action_1(self, data, controls, start_date):
        # The technician has not the method. Write a new training directly.
        pool = Pool()
        LabProfessionalMethod = pool.get('lims.lab.professional.method')
        LabProfessionalMethodRequalification = pool.get(
            'lims.lab.professional.method.requalification')
        Date = pool.get('ir.date')

        supervisors = self._get_supervisors(data)
        professional_method, = LabProfessionalMethod.create([{
            'professional': data.professional.id,
            'method': data.method.id,
            'type': 'preparation',
            'state': 'training',
            }])
        LabProfessionalMethodRequalification.create([{
            'professional_method': professional_method.id,
            'type': 'training',
            'date': Date.today(),
            'last_execution_date': start_date,
            'supervisors': [('create', supervisors)],
            'controls': [('create', controls)],
            }])

    def action_2(self, data, controls, start_date):
        # Qualify the technician
        pool = Pool()
        LabProfessionalMethod = pool.get('lims.lab.professional.method')
        LabProfessionalMethodRequalification = pool.get(
            'lims.lab.professional.method.requalification')
        Date = pool.get('ir.date')

        supervisors = self._get_supervisors(data)
        professional_method, = LabProfessionalMethod.search([
            ('professional', '=', data.professional),
            ('method', '=', data.method),
            ('type', '=', 'preparation'),
            ])
        LabProfessionalMethod.write([professional_method], {
            'state': 'qualified',
            })
        LabProfessionalMethodRequalification.create([{
            'professional_method': professional_method.id,
            'type': 'qualification',
            'date': Date.today(),
            'last_execution_date': start_date,
            'supervisors': [('create', supervisors)],
            'controls': [('create', controls)],
            }])

    def action_3(self, data, controls, start_date):
        # Write a new training
        pool = Pool()
        LabProfessionalMethod = pool.get('lims.lab.professional.method')
        LabProfessionalMethodRequalification = pool.get(
            'lims.lab.professional.method.requalification')
        Date = pool.get('ir.date')

        supervisors = self._get_supervisors(data)
        professional_method, = LabProfessionalMethod.search([
            ('professional', '=', data.professional),
            ('method', '=', data.method),
            ('type', '=', 'preparation'),
            ])
        LabProfessionalMethodRequalification.create([{
            'professional_method': professional_method.id,
            'type': 'training',
            'date': Date.today(),
            'last_execution_date': start_date,
            'supervisors': [('create', supervisors)],
            'controls': [('create', controls)],
            }])

    def action_4(self, data, controls, start_date):
        # Requalify the technician
        pool = Pool()
        LabProfessionalMethod = pool.get('lims.lab.professional.method')
        LabProfessionalMethodRequalification = pool.get(
            'lims.lab.professional.method.requalification')
        Date = pool.get('ir.date')

        supervisors = self._get_supervisors(data)
        professional_method, = LabProfessionalMethod.search([
            ('professional', '=', data.professional),
            ('method', '=', data.method),
            ('type', '=', 'preparation'),
            ])
        LabProfessionalMethod.write([professional_method], {
            'state': 'requalified',
            })
        LabProfessionalMethodRequalification.create([{
            'professional_method': professional_method.id,
            'type': 'requalification',
            'date': Date.today(),
            'last_execution_date': start_date,
            'supervisors': [('create', supervisors)],
            'controls': [('create', controls)],
            }])

    def action_5(self, data, controls, start_date):
        # Write a new execution
        pool = Pool()
        LabProfessionalMethod = pool.get('lims.lab.professional.method')
        LabProfessionalMethodRequalification = pool.get(
            'lims.lab.professional.method.requalification')
        Date = pool.get('ir.date')

        supervisors = self._get_supervisors(data)
        professional_method, = LabProfessionalMethod.search([
            ('professional', '=', data.professional),
            ('method', '=', data.method),
            ('type', '=', 'preparation'),
            ])
        LabProfessionalMethodRequalification.create([{
            'professional_method': professional_method.id,
            'type': ('qualification'
                if professional_method.state == 'qualified'
                else 'requalification'),
            'date': Date.today(),
            'last_execution_date': start_date,
            'supervisors': [('create', supervisors)],
            'controls': [('create', controls)],
            }])

    def transition_confirm(self):
        Planification = Pool().get('lims.planification')
        planification = Planification(Transaction().context['active_id'])
        planification.state = 'confirmed'
        planification.save()
        planification.pre_update_laboratory_notebook()
        planification.update_analysis_detail()
        Planification.__queue__.do_confirm([planification])
        return 'end'

    def _get_supervisors(self, data):
        pool = Pool()
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        LabProfessionalMethod = pool.get('lims.lab.professional.method')
        QualificationAction = pool.get(
            'lims.planification.qualification.action')

        planification_id = Transaction().context['active_id']
        supervisors = []

        planification_details = PlanificationServiceDetail.search([
            ('planification', '=', planification_id),
            ('notebook_line.method', '=', data.method.id),
            ])
        for detail in planification_details:
            for technician in detail.staff_responsible:
                if technician.id == data.professional.id:
                    continue
                qualifications = LabProfessionalMethod.search([
                    ('professional', '=', technician.id),
                    ('method', '=', data.method.id),
                    ('type', '=', 'preparation'),
                    ])
                if (qualifications and qualifications[0].state in
                        ('qualified', 'requalified')):
                    if not data.method.supervised_requalification:
                        supervisors.append(technician.id)
                    else:
                        actions = QualificationAction.search([
                            ('session_id', '=', self._session_id),
                            ('professional', '=', technician.id),
                            ('method', '=', data.method.id),
                            ('action', '=', 4),
                            ])
                        if not actions:
                            supervisors.append(technician.id)

        return [{'supervisor': t_id} for t_id in list(set(supervisors))]

    def _get_controls(self):
        PlanificationFraction = Pool().get('lims.planification-fraction')

        planification_id = Transaction().context['active_id']
        controls = []

        planification_controls = PlanificationFraction.search([
            ('planification', '=', planification_id),
            ('fraction.type.requalify', '=', True),
            ])
        for p_control in planification_controls:
            controls.append(p_control.fraction.id)

        return [{'control': f_id} for f_id in list(set(controls))]


class ReplaceTechnicianStart(ModelView):
    'Replace Technician'
    __name__ = 'lims.planification.replace_technician.start'

    planification = fields.Many2One('lims.planification', 'Planification')
    technician_replaced = fields.Many2One('lims.laboratory.professional',
        'Technician replaced', required=True,
        domain=[('id', 'in', Eval('replaced_domain'))],
        depends=['replaced_domain'])
    replaced_domain = fields.One2Many('lims.laboratory.professional',
        None, 'Replaced domain')
    technician_substitute = fields.Many2One('lims.laboratory.professional',
        'Technician substitute', required=True,
        domain=[('id', 'in', Eval('substitute_domain'))],
        depends=['substitute_domain'])
    substitute_domain = fields.Function(fields.One2Many(
        'lims.laboratory.professional', None, 'Substitute domain'),
        'on_change_with_substitute_domain')

    @fields.depends('technician_replaced', 'planification',
        '_parent_planification.laboratory')
    def on_change_with_substitute_domain(self, name=None):
        pool = Pool()
        UserLaboratory = pool.get('lims.user-laboratory')
        LaboratoryProfessional = pool.get('lims.laboratory.professional')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        LabProfessionalMethod = pool.get('lims.lab.professional.method')

        if not self.technician_replaced:
            return []

        substitute_domain = []
        users = UserLaboratory.search([
            ('laboratory', '=', self.planification.laboratory.id),
            ])
        if users:
            professionals = LaboratoryProfessional.search([
                ('party.lims_user', 'in', [u.user.id for u in users]),
                ])
            if professionals:
                substitute_domain = [p.id for p in professionals]

        service_details = PlanificationServiceDetail.search([
            ('planification', '=', self.planification.id),
            ('notebook_line', '!=', None),
            ('staff_responsible', '=', self.technician_replaced.id),
            ])
        methods_ids = []
        for service_detail in service_details:
            method = service_detail.notebook_line.method
            if method:
                methods_ids.append(method.id)

        methods_ids = list(set(methods_ids))
        for technician_id in substitute_domain:
            for method_id in methods_ids:
                qualifications = LabProfessionalMethod.search([
                    ('professional', '=', technician_id),
                    ('method', '=', method_id),
                    ('type', '=', 'preparation'),
                    ])
                if not (qualifications and qualifications[0].state in
                        ('qualified', 'requalified')):
                    substitute_domain.remove(technician_id)
                    break
        return substitute_domain


class ReplaceTechnician(Wizard):
    'Replace Technician'
    __name__ = 'lims.planification.replace_technician'

    start_state = 'check'
    check = StateTransition()
    start = StateView('lims.planification.replace_technician.start',
        'lims.lims_replace_technician_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Replace', 'replace', 'tryton-ok', default=True),
            ])
    replace = StateTransition()

    def transition_check(self):
        pool = Pool()
        Planification = pool.get('lims.planification')
        NotebookLine = pool.get('lims.notebook.line')

        planification = Planification(Transaction().context['active_id'])
        if planification.state != 'confirmed':
            return 'end'

        notebook_lines = NotebookLine.search([
            ('planification', '=', planification.id),
            ('end_date', '!=', None),
            ])
        if notebook_lines:
            return 'end'

        return 'start'

    def default_start(self, fields):
        Planification = Pool().get('lims.planification')

        planification = Planification(Transaction().context['active_id'])

        return {
            'planification': planification.id,
            'replaced_domain': [t.laboratory_professional.id
                for t in planification.technicians],
            'substitute_domain': [],
            }

    def transition_replace(self):
        planification_id = Transaction().context['active_id']
        technician_replaced_id = self.start.technician_replaced.id
        technician_substitute_id = self.start.technician_substitute.id

        self.update_planification_detail(planification_id,
            technician_replaced_id, technician_substitute_id)
        self.update_planification_technician(planification_id,
            technician_replaced_id, technician_substitute_id)
        self.update_laboratory_notebook(planification_id,
            technician_replaced_id, technician_substitute_id)
        return 'end'

    def update_planification_detail(self, planification_id,
            technician_replaced_id, technician_substitute_id):
        PlanificationServiceDetailProfessional = Pool().get(
            'lims.planification.service_detail-laboratory.professional')

        details_professional = PlanificationServiceDetailProfessional.search([
            ('detail.detail.planification', '=', planification_id),
            ('professional', '=', technician_replaced_id),
            ])
        PlanificationServiceDetailProfessional.write(details_professional, {
            'professional': technician_substitute_id,
            })

    def update_planification_technician(self, planification_id,
            technician_replaced_id, technician_substitute_id):
        PlanificationTechnician = Pool().get('lims.planification.technician')

        planification_professional = PlanificationTechnician.search([
            ('planification', '=', planification_id),
            ('laboratory_professional', '=', technician_replaced_id),
            ])
        PlanificationTechnician.delete(planification_professional)

        planification_professional = PlanificationTechnician.search([
            ('planification', '=', planification_id),
            ('laboratory_professional', '=', technician_substitute_id),
            ])
        if not planification_professional:
            PlanificationTechnician.create([{
                'planification': planification_id,
                'laboratory_professional': technician_substitute_id,
                }])

    def update_laboratory_notebook(self, planification_id,
            technician_replaced_id, technician_substitute_id):
        NotebookLineProfessional = Pool().get(
            'lims.notebook.line-laboratory.professional')

        notebook_line_professional = NotebookLineProfessional.search([
            ('notebook_line.planification', '=', planification_id),
            ('professional', '=', technician_replaced_id),
            ])
        NotebookLineProfessional.write(notebook_line_professional, {
            'professional': technician_substitute_id,
            })


class PlanificationSequenceReport(Report):
    'Sequence'
    __name__ = 'lims.planification.sequence.report'

    @classmethod
    def get_context(cls, records, header, data):
        report_context = super().get_context(records, header, data)

        report_context['company'] = report_context['user'].company

        objects = {}
        for planification in records:
            if planification.state != 'confirmed':
                continue
            date = str(planification.start_date)
            if date not in objects:
                objects[date] = {
                    'date': planification.start_date,
                    'methods': {},
                    }
            for detail in planification.details:
                fraction = detail.fraction
                for service_detail in detail.details:
                    if (service_detail.notebook_line.analysis.behavior ==
                            'internal_relation'):
                        continue
                    notebook_line = service_detail.notebook_line
                    method_id = notebook_line.method.id
                    if method_id not in objects[date]['methods']:
                        objects[date]['methods'][method_id] = {
                            'method': notebook_line.method.code,
                            'lines': {},
                            }

                    number = fraction.get_formated_number('sn-sy-fn')
                    number = (number + '-' + str(notebook_line.repetition))
                    number_parts = number.split('-')
                    order = (number_parts[1] + '-' + number_parts[0] + '-' +
                        number_parts[2] + '-' + number_parts[3])

                    product_type = fraction.product_type.code
                    matrix = fraction.matrix.code
                    fraction_type = fraction.type.code
                    comments = fraction.comments
                    analysis_origin = notebook_line.analysis_origin
                    priority = notebook_line.priority
                    urgent = notebook_line.urgent
                    report_date = (notebook_line.report_date or
                        notebook_line.results_estimated_date)
                    trace_report = fraction.sample.trace_report
                    sample_client_description = (
                        fraction.sample.sample_client_description)
                    party = notebook_line.party.code
                    key = (number, product_type, matrix, fraction_type,
                        analysis_origin, priority, trace_report)
                    if key not in objects[date]['methods'][method_id]['lines']:
                        objects[date]['methods'][method_id]['lines'][key] = {
                            'order': order,
                            'number': number,
                            'product_type': product_type,
                            'matrix': matrix,
                            'fraction_type': fraction_type,
                            'analysis_origin': analysis_origin,
                            'urgent': urgent,
                            'priority': priority,
                            'report_date': report_date,
                            'trace_report': trace_report,
                            'comments': comments,
                            'sample_client_description': (
                                sample_client_description),
                            'party': party,
                            }

        for k1 in objects.keys():
            for k2, lines in objects[k1]['methods'].items():
                sorted_lines = sorted(list(lines['lines'].values()),
                    key=lambda x: x['order'])
                objects[k1]['methods'][k2]['lines'] = sorted_lines

        report_context['records'] = objects

        return report_context


class PlanificationWorksheetAnalysisReport(Report):
    'Worksheet by Analysis'
    __name__ = 'lims.planification.worksheet_analysis.report'

    @classmethod
    def get_context(cls, records, header, data):
        report_context = super().get_context(records, header, data)

        report_context['company'] = report_context['user'].company

        objects = {}
        for planification in records:
            if planification.state != 'confirmed':
                continue
            date = str(planification.start_date)
            if date not in objects:
                objects[date] = {
                    'date': planification.start_date,
                    'professionals': {},
                    }
            for detail in planification.details:
                fraction = detail.fraction
                for service_detail in detail.details:
                    if not service_detail.notebook_line:
                        continue
                    if (service_detail.notebook_line.analysis.behavior ==
                            'internal_relation'):
                        continue
                    p_key = ()
                    p_names = ''
                    for professional in service_detail.staff_responsible:
                        p_key += (professional.id,)
                        if p_names:
                            p_names += ', '
                        p_names += professional.rec_name
                    if not p_key:
                        continue
                    p_key = tuple(sorted(p_key))
                    if (p_key not in objects[date]['professionals']):
                        objects[date]['professionals'][p_key] = {
                            'professional': p_names,
                            'analysis': {},
                            'total': 0,
                            }
                    notebook_line = service_detail.notebook_line
                    key = (notebook_line.analysis.id, notebook_line.method.id)
                    if (key not in
                            objects[date]['professionals'][p_key]['analysis']):
                        objects[date]['professionals'][p_key]['analysis'][
                            key] = {
                                'order': notebook_line.analysis.order or 9999,
                                'analysis': notebook_line.analysis.rec_name,
                                'method': notebook_line.method.rec_name,
                                'lines': {},
                                }

                    number = fraction.get_formated_number('pt-m-sy-sn-fn')
                    number = (number + '-' + str(notebook_line.repetition))
                    concentration_level = (
                        notebook_line.concentration_level.description if
                        notebook_line.concentration_level else '')
                    if (number in objects[date]['professionals'][p_key][
                            'analysis'][key]['lines']):
                        continue

                    number_parts = number.split('-')
                    order = (number_parts[3] + '-' + number_parts[2] + '-' +
                        number_parts[4] + '-' + number_parts[5])
                    comments = '%s - %s - %s' % (planification.comments or '',
                        notebook_line.service.comments or '',
                        notebook_line.service.fraction.comments or '')
                    record = {
                        'order': order,
                        'number': number,
                        'label': fraction.label,
                        'sample_client_description': (
                            fraction.sample.sample_client_description),
                        'party': fraction.party.code,
                        'storage_location': fraction.storage_location.code,
                        'fraction_type': fraction.type.code,
                        'concentration_level': concentration_level,
                        'device': (notebook_line.device.code if
                            notebook_line.device else None),
                        'urgent': 'SI' if notebook_line.urgent else '',
                        'comments': comments,
                        'planification_code': planification.code,
                        'sample_obj_description': (
                            fraction.sample.obj_description.description
                            if fraction.sample.obj_description else
                            fraction.sample.obj_description_manual
                            if fraction.sample.obj_description_manual else
                            ''),
                        }
                    objects[date]['professionals'][p_key]['analysis'][
                        key]['lines'][number] = record
                    objects[date]['professionals'][p_key]['total'] += 1

        for k1 in objects.keys():
            for k2 in objects[k1]['professionals'].keys():
                sorted_analysis = sorted(list(objects[k1]['professionals'][k2][
                    'analysis'].items()), key=lambda x: x[1]['order'])
                objects[k1]['professionals'][k2]['analysis'] = []
                for item in sorted_analysis:
                    sorted_lines = sorted(list(item[1]['lines'].items()),
                            key=lambda x: x[1]['order'])
                    item[1]['lines'] = [l[1] for l in sorted_lines]
                    objects[k1]['professionals'][k2]['analysis'].append(
                        item[1])

        report_context['records'] = objects

        return report_context


class PlanificationWorksheetMethodReport(Report):
    'Worksheet by Method'
    __name__ = 'lims.planification.worksheet_method.report'

    @classmethod
    def get_context(cls, records, header, data):
        report_context = super().get_context(records, header, data)

        report_context['company'] = report_context['user'].company

        objects = {}
        for planification in records:
            if planification.state != 'confirmed':
                continue
            date = str(planification.start_date)
            if date not in objects:
                objects[date] = {
                    'date': planification.start_date,
                    'professionals': {},
                    }
            for detail in planification.details:
                fraction = detail.fraction
                for service_detail in detail.details:
                    if not service_detail.notebook_line:
                        continue
                    if (service_detail.notebook_line.analysis.behavior ==
                            'internal_relation'):
                        continue
                    p_key = ()
                    p_names = ''
                    for professional in service_detail.staff_responsible:
                        p_key += (professional.id,)
                        if p_names:
                            p_names += ', '
                        p_names += professional.rec_name
                    if not p_key:
                        continue
                    p_key = tuple(sorted(p_key))
                    if (p_key not in objects[date]['professionals']):
                        objects[date]['professionals'][p_key] = {
                            'professional': p_names,
                            'total': 0,
                            'lines': {},
                            }
                    notebook_line = service_detail.notebook_line

                    number = fraction.get_formated_number('pt-m-sy-sn-fn')
                    number = (number + '-' + str(notebook_line.repetition))
                    concentration_level = (
                        notebook_line.concentration_level.description if
                        notebook_line.concentration_level else '')
                    if (number not in objects[date]['professionals'][p_key][
                            'lines']):
                        number_parts = number.split('-')
                        order = (number_parts[3] + '-' + number_parts[2] +
                            '-' + number_parts[4] + '-' + number_parts[5])

                        comments_planif = '. '.join(cls.get_planning_legend(
                            fraction, planification))
                        if comments_planif:
                            comments = '%s - %s - %s - %s' % (
                                planification.comments or '',
                                notebook_line.service.comments or '',
                                notebook_line.service.fraction.comments or '',
                                comments_planif or '')
                        else:
                            comments = '%s - %s - %s' % (
                                planification.comments or
                                '', notebook_line.service.comments or '',
                                notebook_line.service.fraction.comments or '')

                        record = {
                            'order': order,
                            'number': number,
                            'label': fraction.label,
                            'sample_client_description': (
                                fraction.sample.sample_client_description),
                            'party': fraction.party.code,
                            'storage_location': fraction.storage_location.code,
                            'fraction_type': fraction.type.code,
                            'concentration_level': concentration_level,
                            'device': (notebook_line.device.code if
                                notebook_line.device else None),
                            'urgent': 'SI' if notebook_line.urgent else '',
                            'comments': comments,
                            'planification_code': planification.code,
                            'package_type': fraction.packages_string,
                            'methods': {},
                            'sample_obj_description': (
                                fraction.sample.obj_description.description
                                if fraction.sample.obj_description else
                                fraction.sample.obj_description_manual
                                if fraction.sample.obj_description_manual else
                                ''),
                            }
                        objects[date]['professionals'][p_key]['lines'][
                            number] = record
                        objects[date]['professionals'][p_key]['total'] += 1
                    if (notebook_line.method.id not in
                            objects[date]['professionals'][p_key]['lines'][
                            number]['methods']):
                        objects[date]['professionals'][p_key]['lines'][
                            number]['methods'][notebook_line.method.id] = (
                                notebook_line.method.rec_name)

        for k1 in objects.keys():
            for k2 in objects[k1]['professionals'].keys():
                objects[k1]['professionals'][k2]['methods'] = {}
                fractions = list(
                    objects[k1]['professionals'][k2]['lines'].values())
                for fraction in fractions:
                    m_key = ()
                    m_names = []
                    for m_id, m_name in fraction['methods'].items():
                        m_key += (m_id,)
                        m_names.append(m_name)
                    m_key = tuple(sorted(m_key))
                    if (m_key not in objects[k1]['professionals'][k2][
                            'methods']):
                        objects[k1]['professionals'][k2]['methods'][m_key] = {
                            'methods': m_names,
                            'lines': [],
                            }
                    objects[k1]['professionals'][k2]['methods'][m_key][
                        'lines'].append(fraction)

                del objects[k1]['professionals'][k2]['lines']
                for m_key in objects[k1]['professionals'][k2][
                        'methods'].keys():
                    sorted_lines = sorted(objects[k1]['professionals'][k2][
                        'methods'][m_key]['lines'], key=lambda x: x['order'])
                    objects[k1]['professionals'][k2]['methods'][m_key][
                        'lines'] = sorted_lines

        report_context['records'] = objects

        return report_context

    @classmethod
    def get_planning_legend(cls, fraction, planification):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        PlanificationDetail = pool.get('lims.planification.detail')
        NotebookLine = pool.get('lims.notebook.line')

        cursor.execute('SELECT DISTINCT(a.planning_legend) '
            'FROM "' + PlanificationDetail._table + '" pd '
                'INNER JOIN "' + PlanificationServiceDetail._table +
                    '" psd '
                'ON pd.id = psd.detail '
                'INNER JOIN "' + NotebookLine._table + '" nl '
                'ON nl.id = psd.notebook_line '
                'INNER JOIN "' + Analysis._table + '" a '
                'ON a.id = nl.analysis '
            'WHERE pd.planification = %s '
                'AND pd.fraction = %s '
                'AND a.planning_legend IS NOT NULL ',
                (planification.id, fraction.id))

        planned_ids = [s[0] for s in cursor.fetchall()]
        return planned_ids


class PlanificationWorksheetReport(Report):
    'Worksheet'
    __name__ = 'lims.planification.worksheet.report'

    @classmethod
    def get_context(cls, records, header, data):
        report_context = super().get_context(records, header, data)

        report_context['company'] = report_context['user'].company

        objects = {}
        for planification in records:
            if planification.state != 'confirmed':
                continue
            date = str(planification.start_date)
            if date not in objects:
                objects[date] = {
                    'date': planification.start_date,
                    'professionals': {},
                    }
            for detail in planification.details:
                fraction = detail.fraction
                for service_detail in detail.details:
                    if not service_detail.notebook_line:
                        continue
                    if (service_detail.notebook_line.analysis.behavior ==
                            'internal_relation'):
                        continue
                    p_key = ()
                    p_names = ''
                    for professional in service_detail.staff_responsible:
                        p_key += (professional.id,)
                        if p_names:
                            p_names += ', '
                        p_names += professional.rec_name
                    if not p_key:
                        continue
                    p_key = tuple(sorted(p_key))
                    if (p_key not in objects[date]['professionals']):
                        objects[date]['professionals'][p_key] = {
                            'professional': p_names,
                            'analysis': {},
                            'total': 0,
                            }
                    notebook_line = service_detail.notebook_line
                    key = service_detail.planned_service.id
                    if (key not in
                            objects[date]['professionals'][p_key]['analysis']):
                        objects[date]['professionals'][p_key]['analysis'][
                            key] = {
                                'analysis': (
                                    service_detail.planned_service.rec_name),
                                'lines': {},
                                }

                    number = fraction.get_formated_number('pt-m-sy-sn-fn')
                    number = (number + '-' + str(notebook_line.repetition))
                    concentration_level = (
                        notebook_line.concentration_level.description if
                        notebook_line.concentration_level else '')
                    if (number not in objects[date]['professionals'][p_key][
                            'analysis'][key]['lines']):
                        number_parts = number.split('-')
                        order = (number_parts[3] + '-' + number_parts[2] +
                            '-' + number_parts[4] + '-' + number_parts[5])

                        comments_planif = '. '.join(cls.get_planning_legend(
                            fraction, planification))
                        if comments_planif:
                            comments = '%s - %s - %s - %s' % (
                                planification.comments or '',
                                notebook_line.service.comments or '',
                                notebook_line.service.fraction.comments or '',
                                comments_planif or '')
                        else:
                            comments = '%s - %s - %s' % (
                                planification.comments or
                                '', notebook_line.service.comments or '',
                                notebook_line.service.fraction.comments or '')

                        record = {
                            'order': order,
                            'number': number,
                            'label': fraction.label,
                            'sample_client_description': (
                                fraction.sample.sample_client_description if
                                fraction.sample.sample_client_description else
                                ''),
                            'party': fraction.party.code,
                            'storage_location': fraction.storage_location.code,
                            'fraction_type': fraction.type.code,
                            'concentration_level': concentration_level,
                            'device': (notebook_line.device.code if
                                notebook_line.device else None),
                            'urgent': 'SI' if notebook_line.urgent else '',
                            'comments': comments,
                            'planification_code': planification.code,
                            'package_type': fraction.packages_string,
                            'methods': {},
                            'sample_obj_description': (
                                fraction.sample.obj_description.description
                                if fraction.sample.obj_description else
                                fraction.sample.obj_description_manual
                                if fraction.sample.obj_description_manual else
                                ''),
                            }
                        objects[date]['professionals'][p_key]['analysis'][key][
                            'lines'][number] = record
                        objects[date]['professionals'][p_key]['total'] += 1
                    if (notebook_line.method.id not in
                            objects[date]['professionals'][p_key]['analysis'][
                            key]['lines'][number]['methods']):
                        objects[date]['professionals'][p_key]['analysis'][
                            key]['lines'][number]['methods'][
                            notebook_line.method.id] = (
                                notebook_line.method.rec_name)

        for k1 in objects.keys():
            for k2 in objects[k1]['professionals'].keys():
                for k3 in objects[k1]['professionals'][k2][
                        'analysis'].keys():
                    objects[k1]['professionals'][k2]['analysis'][k3][
                        'methods'] = {}
                    fractions = list(
                        objects[k1]['professionals'][k2]['analysis'][
                            k3]['lines'].values())
                    for fraction in fractions:
                        m_key = ()
                        m_names = []
                        for m_id, m_name in fraction['methods'].items():
                            m_key += (m_id,)
                            m_names.append(m_name)
                        m_key = tuple(sorted(m_key))
                        if (m_key not in objects[k1]['professionals'][k2][
                                'analysis'][k3]['methods']):
                            objects[k1]['professionals'][k2]['analysis'][k3][
                                'methods'][m_key] = {
                                    'methods': m_names,
                                    'lines': [],
                                    }
                        objects[k1]['professionals'][k2]['analysis'][k3][
                            'methods'][m_key]['lines'].append(fraction)

                    del objects[k1]['professionals'][k2]['analysis'][k3][
                        'lines']
                    for m_key in objects[k1]['professionals'][k2][
                            'analysis'][k3]['methods'].keys():
                        sorted_lines = sorted(objects[k1]['professionals'][k2][
                            'analysis'][k3]['methods'][m_key]['lines'],
                            key=lambda x: x['order'])
                        objects[k1]['professionals'][k2]['analysis'][k3][
                            'methods'][m_key]['lines'] = sorted_lines

        report_context['records'] = objects
        return report_context

    @classmethod
    def get_planning_legend(cls, fraction, planification):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        PlanificationDetail = pool.get('lims.planification.detail')
        NotebookLine = pool.get('lims.notebook.line')

        cursor.execute('SELECT DISTINCT(a.planning_legend) '
            'FROM "' + PlanificationDetail._table + '" pd '
                'INNER JOIN "' + PlanificationServiceDetail._table +
                    '" psd '
                'ON pd.id = psd.detail '
                'INNER JOIN "' + NotebookLine._table + '" nl '
                'ON nl.id = psd.notebook_line '
                'INNER JOIN "' + Analysis._table + '" a '
                'ON a.id = nl.analysis '
            'WHERE pd.planification = %s '
                'AND pd.fraction = %s '
                'AND a.planning_legend IS NOT NULL ',
                (planification.id, fraction.id))

        planned_ids = [s[0] for s in cursor.fetchall()]
        return planned_ids


class PrintPendingServicesUnplannedReportStart(ModelView):
    'Print Pending Services Unplanned Report Start'
    __name__ = 'lims.pending_services_unplanned.start'

    start_date = fields.Date('Start Date', required=True)
    end_date = fields.Date('End Date', required=True)
    party = fields.Many2One('party.party', 'Party')
    include_method = fields.Boolean('Include method')


class PrintPendingServicesUnplannedReport(Wizard):
    'Print Pending Services Unplanned Report'
    __name__ = 'lims.pending_services_unplanned'

    start = StateView('lims.pending_services_unplanned.start',
        'lims.print_pending_services_unplanned_report_start'
        '_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Print', 'print_', 'tryton-print', default=True),
            Button('Save', 'save', 'tryton-save'),
        ])
    print_ = StateAction(
        'lims.report_pending_services_unplanned')
    save = StateAction(
        'lims.report_pending_services_unplanned_spreadsheet')

    @classmethod
    def check_access(cls):
        pass

    def do_print_(self, action):
        data = {
            'start_date': self.start.start_date,
            'end_date': self.start.end_date or None,
            'party': self.start.party and self.start.party.id or None,
            'include_method': self.start.include_method,
            }
        return action, data

    def do_save(self, action):
        data = {
            'start_date': self.start.start_date,
            'end_date': self.start.end_date or None,
            'party': self.start.party and self.start.party.id or None,
            'include_method': self.start.include_method,
            }
        return action, data


class PendingServicesUnplannedReport(Report):
    'Pending Services Unplanned'
    __name__ = 'lims.pending_services_unplanned.report'

    @classmethod
    def get_context(cls, records, header, data):
        pool = Pool()
        Laboratory = pool.get('lims.laboratory')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        NotebookLine = pool.get('lims.notebook.line')

        report_context = super().get_context(records, header, data)

        report_context['company'] = report_context['user'].company
        report_context['start_date'] = (data['start_date']
            if data['start_date'] else '')
        report_context['end_date'] = (data['end_date']
            if data['end_date'] else '')
        report_context['include_method'] = data['include_method']

        if report_context['user'].laboratory:
            labs = [report_context['user'].laboratory.id]
        elif report_context['user'].laboratories:
            labs = [l.id for l in report_context['user'].laboratories]
        else:
            labs = [l.id for l in Laboratory.search([])]

        clause = cls._get_details_clause(data)
        objects = {}
        with Transaction().set_user(0):
            details = EntryDetailAnalysis.search(clause)
        for detail in details:
            # Check for available lines
            if NotebookLine.search_count([
                    ('analysis_detail', '=', detail),
                    ('start_date', '=', None),
                    ('annulment_date', '=', None),
                    ]) == 0:
                continue

            # Laboratory
            laboratory_id = detail.laboratory.id
            if laboratory_id not in labs:
                continue

            if laboratory_id not in objects:
                objects[laboratory_id] = {
                    'laboratory': detail.laboratory.rec_name,
                    'services': {},
                    'total': 0,
                    }

            # Service
            analysis_id = detail.service.analysis.id
            if analysis_id not in objects[laboratory_id]['services']:
                objects[laboratory_id]['services'][analysis_id] = {
                    'service': detail.service.analysis.rec_name,
                    'parties': {},
                    'total': 0,
                    }

            # Party
            party_id = detail.party.id
            if (party_id not in objects[laboratory_id]['services'][
                    analysis_id]['parties']):
                objects[laboratory_id]['services'][analysis_id]['parties'][
                    party_id] = {
                        'party': detail.party.code,
                        'lines': {},
                        'total': 0,
                        }

            # Key
            if data['include_method']:
                key = (detail.fraction.id, detail.method.id)
            else:
                key = detail.fraction.id

            if (key in objects[laboratory_id]['services'][analysis_id][
                    'parties'][party_id]['lines']):
                continue

            number = detail.service.fraction.get_formated_number(
                'pt-m-sn-sy-fn')
            number = (number + '-' + str(detail.service.sample.label))
            number_parts = number.split('-')
            order = (number_parts[3] + '-' + number_parts[2] + '-' +
                number_parts[4])

            results_estimated_date = None
            results_estimated_waiting = cls._get_estimated_waiting(detail.id)
            if results_estimated_waiting:
                results_estimated_date = (detail.service.confirmation_date +
                    relativedelta(days=results_estimated_waiting))

            record = {
                'order': order,
                'number': number,
                'date': detail.service.sample.date2,
                'sample_client_description': (
                    detail.service.sample.sample_client_description),
                'current_location': (
                    detail.service.fraction.current_location.code
                    if detail.service.fraction.current_location else ''),
                'fraction_type': detail.service.fraction.type.code,
                'priority': detail.service.priority,
                'urgent': 'Yes' if detail.service.urgent else 'No',
                'method': detail.method.code if data['include_method'] else '',
                'comments': ('%s - %s' % (
                    detail.service.fraction.comments or '',
                    detail.service.sample.comments or '')),
                'report_date': detail.service.report_date,
                'confirmation_date': (detail.service.confirmation_date
                    if detail.service.confirmation_date else ''),
                'results_estimated_date': results_estimated_date or '',
                }
            objects[laboratory_id]['services'][analysis_id]['parties'][
                party_id]['lines'][key] = record
            objects[laboratory_id]['services'][analysis_id]['parties'][
                party_id]['total'] += 1
            objects[laboratory_id]['services'][analysis_id][
                'total'] += 1
            objects[laboratory_id]['total'] += 1

        for k1 in objects.keys():
            for k2 in objects[k1]['services'].keys():
                for k3, lines in objects[k1]['services'][k2][
                        'parties'].items():
                    sorted_lines = sorted(lines['lines'].values(),
                        key=lambda x: x['order'])
                    objects[k1]['services'][k2]['parties'][k3]['lines'] = (
                        sorted_lines)

        report_context['records'] = objects
        return report_context

    @classmethod
    def _get_details_clause(cls, data):
        clause = [
            ('plannable', '=', True),
            ('state', '=', 'unplanned'),
            ('analysis.behavior', '!=', 'internal_relation'),
            ('service.fraction.confirmed', '=', True),
            ]
        if data['start_date']:
            clause.append(
                ('service.confirmation_date', '>=', data['start_date']))
        if data['end_date']:
            clause.append(
                ('service.confirmation_date', '<=', data['end_date']))
        if data['party']:
            clause.append(('party', '=', data['party']))
        return clause

    @classmethod
    def _get_estimated_waiting(cls, detail_id):
        cursor = Transaction().connection.cursor()
        NotebookLine = Pool().get('lims.notebook.line')

        cursor.execute('SELECT MIN(results_estimated_waiting) '
            'FROM "' + NotebookLine._table + '" '
            'WHERE analysis_detail = %s '
                'AND results_estimated_waiting IS NOT NULL',
            (detail_id,))
        res = cursor.fetchone()
        return res and res[0] or None


class PendingServicesUnplannedSpreadsheet(Report):
    'Pending Services Unplanned'
    __name__ = 'lims.pending_services_unplanned.spreadsheet'

    @classmethod
    def get_context(cls, records, header, data):
        pool = Pool()
        Laboratory = pool.get('lims.laboratory')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')

        report_context = super().get_context(records, header, data)

        report_context['company'] = report_context['user'].company
        report_context['start_date'] = (data['start_date']
            if data['start_date'] else '')
        report_context['end_date'] = (data['end_date']
            if data['end_date'] else '')
        report_context['include_method'] = data['include_method']

        if report_context['user'].laboratory:
            labs = [report_context['user'].laboratory.id]
        elif report_context['user'].laboratories:
            labs = [l.id for l in report_context['user'].laboratories]
        else:
            labs = [l.id for l in Laboratory.search([])]

        clause = cls._get_details_clause(data)
        objects = {}
        with Transaction().set_user(0):
            details = EntryDetailAnalysis.search(clause)
        for detail in details:
            # Laboratory
            laboratory_id = detail.laboratory.id
            if laboratory_id not in labs:
                continue

            # Service
            analysis_id = detail.service.analysis.id

            # Party
            party_id = detail.party.id

            # Key
            if data['include_method']:
                key = (laboratory_id, analysis_id, party_id,
                    detail.fraction.id, detail.method.id)
            else:
                key = (laboratory_id, analysis_id, party_id,
                    detail.fraction.id)
            if key in objects:
                continue

            number = detail.service.fraction.get_formated_number(
                'pt-m-sn-sy-fn')
            number = (number + '-' + str(detail.service.sample.label))
            number_parts = number.split('-')
            order = (number_parts[3] + '-' + number_parts[2] + '-' +
                number_parts[4])

            results_estimated_date = None
            results_estimated_waiting = cls._get_estimated_waiting(detail.id)
            if results_estimated_waiting:
                results_estimated_date = (detail.service.confirmation_date +
                    relativedelta(days=results_estimated_waiting))

            notice = None
            report_date = detail.service.report_date
            today = get_print_date().date()

            if report_date:
                if report_date < today:
                    notice = 'Timed out'
                else:
                    d = (report_date - today).days
                    if d >= 0 and d < 3:
                        notice = 'To expire'
            elif results_estimated_date:
                if results_estimated_date < today:
                    notice = 'Timed out'
                else:
                    d = (results_estimated_date - today).days
                    if d >= 0 and d < 3:
                        notice = 'To expire'

            record = {
                'laboratory': detail.laboratory.rec_name,
                'service': detail.service.analysis.rec_name,
                'party': detail.party.code,
                'order': order,
                'number': number,
                'date': detail.service.sample.date2,
                'sample_client_description': (
                    detail.service.sample.sample_client_description),
                'current_location': (
                    detail.service.fraction.current_location.code
                    if detail.service.fraction.current_location else ''),
                'fraction_type': detail.service.fraction.type.code,
                'priority': detail.service.priority,
                'urgent': 'Yes' if detail.service.urgent else 'No',
                'method': detail.method.code if data['include_method'] else '',
                'comments': ('%s - %s' % (
                    detail.service.fraction.comments or '',
                    detail.service.sample.comments or '')),
                'report_date': detail.service.report_date,
                'confirmation_date': (detail.service.confirmation_date
                    if detail.service.confirmation_date else ''),
                'results_estimated_date': results_estimated_date or '',
                'results_estimated_waiting': results_estimated_waiting or '',
                'notice': notice or '',
                }
            objects[key] = record

        objects = sorted(objects.values(), key=lambda x: (
                x['laboratory'], x['service'], x['party'], x['order']))

        report_context['records'] = objects
        return report_context

    @classmethod
    def _get_details_clause(cls, data):
        clause = [
            ('plannable', '=', True),
            ('state', '=', 'unplanned'),
            ('analysis.behavior', '!=', 'internal_relation'),
            ('service.fraction.confirmed', '=', True),
            ]
        if data['start_date']:
            clause.append(
                ('service.confirmation_date', '>=', data['start_date']))
        if data['end_date']:
            clause.append(
                ('service.confirmation_date', '<=', data['end_date']))
        if data['party']:
            clause.append(('party', '=', data['party']))
        return clause

    @classmethod
    def _get_estimated_waiting(cls, detail_id):
        cursor = Transaction().connection.cursor()
        NotebookLine = Pool().get('lims.notebook.line')

        cursor.execute('SELECT MIN(results_estimated_waiting) '
            'FROM "' + NotebookLine._table + '" '
            'WHERE analysis_detail = %s '
                'AND results_estimated_waiting IS NOT NULL',
            (detail_id,))
        res = cursor.fetchone()
        return res and res[0] or None


class PrintBlindSampleReportStart(ModelView):
    'Blind Samples Report'
    __name__ = 'lims.print_blind_sample_report.start'

    date_from = fields.Date('Date from', required=True)
    date_to = fields.Date('Date to', required=True)


class PrintBlindSampleReport(Wizard):
    'Blind Samples Report'
    __name__ = 'lims.print_blind_sample_report'

    start = StateView('lims.print_blind_sample_report.start',
        'lims.lims_print_blind_sample_report_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Print', 'print_', 'tryton-print', default=True),
            ])
    print_ = StateAction('lims.report_blind_sample')

    def do_print_(self, action):
        BlindSample = Pool().get('lims.blind_sample')

        blind_samples = BlindSample.search_count([
            ('date', '>=', self.start.date_from),
            ('date', '<=', self.start.date_to),
            ('line.result', '!=', None),
            ])
        if blind_samples > 0:
            data = {
                'date_from': self.start.date_from,
                'date_to': self.start.date_to,
                }
            return action, data

    def transition_print_(self):
        return 'end'


class BlindSampleReport(Report):
    'Blind Samples Report'
    __name__ = 'lims.blind_sample_report'

    @classmethod
    def get_context(cls, records, header, data):
        BlindSample = Pool().get('lims.blind_sample')

        report_context = super().get_context(records, header, data)

        report_context['company'] = report_context['user'].company

        objects = []
        blind_samples = BlindSample.search([
            ('date', '>=', data['date_from']),
            ('date', '<=', data['date_to']),
            ('line.result', '!=', None),
            ], order=[('date', 'ASC')])
        for bs in blind_samples:

            if bs.line.converted_result:
                result = float(bs.line.converted_result)
                unit = bs.line.final_unit
                concentration = bs.line.final_concentration
                result = round(result, bs.line.decimals)
            elif bs.line.result:
                result = float(bs.line.result)
                unit = bs.line.initial_unit
                concentration = bs.line.initial_concentration
                result = round(result, bs.line.decimals)
            else:
                result = None
                unit = None
                concentration = ''

            record = {
                'date': bs.date,
                'fraction': bs.fraction.rec_name,
                'report': (bs.line.results_report.number if
                    bs.line.results_report else ''),
                'repetition': bs.line.repetition,
                'analysis_origin': bs.line.analysis_origin,
                'analysis': bs.analysis.rec_name,
                'result': result,
                'unit': unit.symbol if unit else '',
                'concentration': concentration,
                'original_fraction': '',
                'original_report': '',
                'original_result': '',
                'original_unit': '',
                'original_concentration': '',
                'error': '',
                'max_sd': '',
                'two_max_sd': '',
                'accepted': '',
                }
            if bs.original_fraction:
                record['original_fraction'] = bs.original_fraction.rec_name
                if bs.original_line:
                    if bs.original_line.results_report:
                        record['original_report'] = (
                            bs.original_line.results_report.number)
                    if bs.original_line.converted_result:
                        original_result = float(
                            bs.original_line.converted_result)
                        original_unit = bs.original_line.final_unit
                        original_concentration = (
                            bs.original_line.final_concentration)
                    elif bs.original_line.result:
                        original_result = float(bs.original_line.result)
                        original_unit = bs.original_line.initial_unit
                        original_concentration = (
                            bs.original_line.initial_concentration)
                    else:
                        original_result = None
                    if original_result:
                        original_result = round(original_result,
                            bs.original_line.decimals)
                        record['original_result'] = original_result
                        record['original_unit'] = (original_unit.symbol
                            if original_unit else '')
                        record['original_concentration'] = (
                            original_concentration)
                        if unit == original_unit:
                            average = (result + original_result) / 2
                            difference = result - original_result
                            record['error'] = round(difference * 100 / average,
                                2)
                            try:
                                maximum_concentration = float(
                                    unit.maximum_concentration)
                                rsd_horwitz = float(unit.rsd_horwitz)
                            except (TypeError, ValueError):
                                maximum_concentration = 0
                                rsd_horwitz = 0

                            if result <= maximum_concentration:
                                record['max_sd'] = round(average * rsd_horwitz,
                                    2)
                                record['two_max_sd'] = round(record['max_sd'] *
                                    2, 2)
                                if abs(difference) <= record['two_max_sd']:
                                    record['accepted'] = 'a'
                                else:
                                    record['accepted'] = 'r'
            else:
                if bs.min_value and bs.max_value:
                    min_value = float(bs.min_value)
                    max_value = float(bs.max_value)
                    if min_value <= result and result <= max_value:
                        record['error'] = 'OK'
                    elif result < min_value:
                        record['error'] = round(result - min_value, 2)
                    elif result > max_value:
                        record['error'] = round(result - max_value, 2)
            objects.append(record)

        report_context['records'] = objects

        report_context['date_from'] = data['date_from']
        report_context['date_to'] = data['date_to']

        return report_context

    def _get_variables(self, formula, notebook_line):
        pool = Pool()
        VolumeConversion = pool.get('lims.volume.conversion')

        variables = {}
        for var in ('DI',):
            while True:
                idx = formula.find(var)
                if idx >= 0:
                    variables[var] = 0
                    formula = formula.replace(var, '_')
                else:
                    break
        for var in variables.keys():
            if var == 'DI':
                ic = float(notebook_line.final_concentration)
                result = VolumeConversion.brixToDensity(ic)
                if result:
                    variables[var] = result
        return variables


class PlanificationSequenceAnalysisReport(Report):
    'Sequence Analysis'
    __name__ = 'lims.planification.sequence.analysis.report'

    @classmethod
    def get_context(cls, records, header, data):
        report_context = super().get_context(records, header, data)

        report_context['company'] = report_context['user'].company

        objects = {}
        for planification in records:
            if planification.state != 'confirmed':
                continue
            date = str(planification.start_date)
            if date not in objects:
                objects[date] = {
                    'date': planification.start_date,
                    'methods': {},
                    }
            for detail in planification.details:
                fraction = detail.fraction
                for service_detail in detail.details:
                    if (service_detail.notebook_line.analysis.behavior ==
                            'internal_relation'):
                        continue
                    notebook_line = service_detail.notebook_line
                    method_id = notebook_line.method.id
                    if method_id not in objects[date]['methods']:
                        objects[date]['methods'][method_id] = {
                            'method': notebook_line.method.code,
                            'lines': {},
                            }

                    number = fraction.get_formated_number('sn-sy-fn')
                    number = (number + '-' + str(notebook_line.repetition))
                    number_parts = number.split('-')
                    order = (number_parts[1] + '-' + number_parts[0] + '-' +
                        number_parts[2] + '-' + number_parts[3])

                    product_type = fraction.product_type.code
                    matrix = fraction.matrix.code
                    fraction_type = fraction.type.code
                    analysis = notebook_line.analysis.rec_name
                    initial_unit = (notebook_line.initial_unit.symbol
                        if notebook_line.initial_unit else '')
                    priority = notebook_line.priority
                    urgent = notebook_line.urgent
                    report_date = (notebook_line.report_date or
                        notebook_line.results_estimated_date)
                    trace_report = fraction.sample.trace_report
                    sample_client_description = (
                        fraction.sample.sample_client_description)
                    key = (number, product_type, matrix, fraction_type,
                        analysis, priority, trace_report)
                    if key not in objects[date]['methods'][method_id]['lines']:
                        objects[date]['methods'][method_id]['lines'][key] = {
                            'order': order,
                            'number': number,
                            'product_type': product_type,
                            'matrix': matrix,
                            'fraction_type': fraction_type,
                            'analysis': analysis,
                            'priority': priority,
                            'urgent': urgent,
                            'report_date': report_date,
                            'trace_report': trace_report,
                            'sample_client_description': (
                                sample_client_description),
                            'initial_unit': initial_unit,
                            }

        for k1 in objects.keys():
            for k2, lines in objects[k1]['methods'].items():
                sorted_lines = sorted(list(lines['lines'].values()),
                    key=lambda x: x['order'])
                objects[k1]['methods'][k2]['lines'] = sorted_lines

        report_context['records'] = objects

        return report_context
