# This file is part of lims_analysis_sheet module for Tryton.
# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.
import operator
from datetime import datetime, time
from collections import defaultdict
from sql import Column, Literal, Cast

from trytond.model import ModelView, ModelSQL, fields
from trytond.wizard import Wizard, StateTransition, StateView, StateAction, \
    Button
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, Equal, Bool, If, PYSONEncoder
from trytond.transaction import Transaction


class Planification(metaclass=PoolMeta):
    __name__ = 'lims.planification'

    planification_update_draft_sheet = fields.Boolean(
        'Update draft sheets')
    analysis_sheets = fields.One2Many('lims.planification.analysis_sheet',
        'planification', 'Analysis sheets to update', depends=['state'],
        states={'readonly': Eval('state') != 'preplanned'})

    @staticmethod
    def default_planification_update_draft_sheet():
        Config = Pool().get('lims.configuration')
        return Config(1).planification_update_draft_sheet

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._buttons.update({
            'search_analysis_sheet': {
                'readonly': (Eval('state') != 'draft'),
                },
            })

    @classmethod
    @ModelView.button_action('lims_analysis_sheet.wiz_search_analysis_sheet')
    def search_analysis_sheet(cls, planifications):
        pass

    @classmethod
    @ModelView.button_action('lims.wiz_lims_technicians_qualification')
    def confirm(cls, planifications):
        super().confirm(planifications)
        for planification in planifications:
            planification.load_analysis_sheets()

    def load_analysis_sheets(self):
        pool = Pool()
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        AnalysisSheet = pool.get('lims.analysis_sheet')
        PlanificationAnalysisSheet = pool.get(
            'lims.planification.analysis_sheet')

        if not self.planification_update_draft_sheet:
            return

        analysis_sheets = {}
        service_details = PlanificationServiceDetail.search([
            ('detail.planification', '=', self.id),
            ('notebook_line', '!=', None),
            ])
        for service_detail in service_details:
            nl = service_detail.notebook_line
            template_id = nl.get_analysis_sheet_template()
            if not template_id:
                continue
            key = (template_id, service_detail.staff_responsible[0])
            if key not in analysis_sheets:
                analysis_sheets[key] = []
            analysis_sheets[key].append(nl)

        for key, values in analysis_sheets.items():
            if PlanificationAnalysisSheet.search([
                    ('planification', '=', self.id),
                    ('professional', '=', key[1]),
                    ('analysis_sheet.template', '=', key[0]),
                    ]):
                continue
            draft_sheet = AnalysisSheet.search([
                ('template', '=', key[0]),
                ('professional', '=', key[1]),
                ('state', '=', 'draft'),
                ])
            if not draft_sheet:
                continue
            PlanificationAnalysisSheet.create([{
                'planification': self.id,
                'professional': key[1],
                'analysis_sheet': draft_sheet[0].id,
                }])

    @classmethod
    def do_confirm(cls, planifications):
        super().do_confirm(planifications)
        for planification in planifications:
            planification.create_analysis_sheets()

    def create_analysis_sheets(self):
        pool = Pool()
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        AnalysisSheet = pool.get('lims.analysis_sheet')
        PlanificationAnalysisSheet = pool.get(
            'lims.planification.analysis_sheet')
        analysis_sheets = {}
        sheets = []
        service_details = PlanificationServiceDetail.search([
            ('detail.planification', '=', self.id),
            ('notebook_line', '!=', None),
            ])
        for service_detail in service_details:
            nl = service_detail.notebook_line
            template_id = nl.get_analysis_sheet_template()
            if not template_id:
                continue
            key = (template_id, service_detail.staff_responsible[0])
            if key not in analysis_sheets:
                analysis_sheets[key] = []
            analysis_sheets[key].append(nl)

        date_time = datetime.combine(self.start_date, self.create_date.time())

        for key, values in analysis_sheets.items():
            planification_sheet = PlanificationAnalysisSheet.search([
                ('planification', '=', self.id),
                ('professional', '=', key[1]),
                ('analysis_sheet.template', '=', key[0]),
                ('analysis_sheet.state', 'in', ['draft', 'active']),
                ])
            if planification_sheet:
                sheet = AnalysisSheet(planification_sheet[0].analysis_sheet.id)
            else:
                sheet = AnalysisSheet()
                sheet.template = key[0]
                sheet.compilation = sheet.get_new_compilation(
                    {'date_time': date_time})
                sheet.professional = key[1]
                sheet.laboratory = self.laboratory.id
                sheet.planification = self.id
                sheet.save()
            sheets.append(sheet)
            sheet.create_lines(values)
        return sheets


class PlanificationAnalysisSheet(ModelSQL, ModelView):
    'Planification - Analysis Sheet'
    __name__ = 'lims.planification.analysis_sheet'

    planification = fields.Many2One('lims.planification', 'Planification',
        ondelete='CASCADE', select=True, required=True)
    professional = fields.Many2One('lims.laboratory.professional',
        'Laboratory professional', required=True)
    analysis_sheet = fields.Many2One('lims.analysis_sheet',
        'Analysis Sheet', required=True,
        domain=['OR', ('id', '=', Eval('analysis_sheet')),
            [('state', 'in', ['draft', 'active']),
                ('professional', '=', Eval('professional'))]],
        depends=['professional'])
    template = fields.Function(fields.Many2One('lims.template.analysis_sheet',
        'Template'), 'on_change_with_template')

    @fields.depends('analysis_sheet')
    def on_change_with_template(self, name=None):
        if self.analysis_sheet:
            return self.analysis_sheet.template.id
        return None


class SearchAnalysisSheetStart(ModelView):
    'Search Analysis Sheets'
    __name__ = 'lims.planification.search_analysis_sheet.start'

    date_from = fields.Date('Date from', readonly=True)
    date_to = fields.Date('Date to', readonly=True)
    templates = fields.Many2Many('lims.template.analysis_sheet',
        None, None, 'Templates', required=True,
        domain=[('id', 'in', Eval('templates_domain'))],
        context={'date_from': Eval('date_from'), 'date_to': Eval('date_to')},
        depends=['templates_domain', 'date_from', 'date_to'])
    templates_domain = fields.One2Many('lims.template.analysis_sheet',
        None, 'Templates domain')


class SearchAnalysisSheetNext(ModelView):
    'Search Analysis Sheets'
    __name__ = 'lims.planification.search_analysis_sheet.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 SearchAnalysisSheet(Wizard):
    'Search Analysis Sheets'
    __name__ = 'lims.planification.search_analysis_sheet'

    start = StateView('lims.planification.search_analysis_sheet.start',
        'lims_analysis_sheet.planification_search_analysis_sheet_start'
        '_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Search', 'search', 'tryton-forward', default=True),
            ])
    search = StateTransition()
    next = StateView('lims.planification.search_analysis_sheet.next',
        'lims_analysis_sheet.planification_search_analysis_sheet_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'])
        templates_domain = self._get_templates(planification)
        return {
            'templates': [],
            'templates_domain': templates_domain,
            'date_from': planification.date_from,
            'date_to': planification.date_to,
            }

    def _get_templates(self, planification):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        PlanificationDetail = pool.get('lims.planification.detail')
        Planification = pool.get('lims.planification')
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')
        Fraction = pool.get('lims.fraction')
        Sample = pool.get('lims.sample')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        Analysis = pool.get('lims.analysis')
        Template = pool.get('lims.template.analysis_sheet')
        TemplateAnalysis = pool.get('lims.template.analysis_sheet.analysis')

        cursor.execute('SELECT nl.id '
            'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + PlanificationServiceDetail._table +
                '" psd ON psd.notebook_line = nl.id '
                'INNER JOIN "' + PlanificationDetail._table + '" pd '
                'ON psd.detail = pd.id '
                'INNER JOIN "' + Planification._table + '" p '
                'ON pd.planification = p.id '
            'WHERE p.state = \'preplanned\' '
                'OR p.id = %s',
            (str(planification.id),))
        planned_lines = [x[0] for x in cursor.fetchall()]
        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)

        sql_select = 'SELECT nl.analysis, nl.method, s.product_type, s.matrix '
        sql_from = (
            'FROM "' + NotebookLine._table + '" nl '
            'INNER JOIN "' + Analysis._table + '" nla '
            'ON nla.id = nl.analysis '
            'INNER JOIN "' + Notebook._table + '" n '
            'ON n.id = nl.notebook '
            'INNER JOIN "' + Fraction._table + '" f '
            'ON f.id = n.fraction '
            'INNER JOIN "' + Sample._table + '" s '
            'ON s.id = f.sample '
            'INNER JOIN "' + EntryDetailAnalysis._table + '" ad '
            'ON ad.id = nl.analysis_detail ')
        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)

        with Transaction().set_user(0):
            cursor.execute(sql_select + sql_from + sql_where,
                (planification.laboratory.id,))
        notebook_lines = cursor.fetchall()
        if not notebook_lines:
            return []

        result = []
        for nl in notebook_lines:
            cursor.execute('SELECT t.id '
                'FROM "' + Template._table + '" t '
                    'INNER JOIN "' + TemplateAnalysis._table + '" ta '
                    'ON t.id = ta.template '
                'WHERE t.active IS TRUE '
                    'AND ta.analysis = %s '
                    'AND (ta.method = %s OR ta.method IS NULL) '
                    'AND (ta.product_type = %s OR ta.product_type IS NULL) '
                    'AND (ta.matrix = %s OR ta.matrix IS NULL)',
                (nl[0], nl[1], nl[2], nl[3]))
            template = cursor.fetchone()
            if template:
                result.append(template[0])
        return list(set(result))

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

        planification = Planification(Transaction().context['active_id'])
        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')
        PlanificationAnalysis = pool.get('lims.planification-analysis')

        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)

        details_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:
                details_to_create.append({
                    'planification': planification.id,
                    'fraction': k[0],
                    'service_analysis': k[1],
                    'details': [('create', v)],
                    })
        if details_to_create:
            PlanificationDetail.create(details_to_create)

        analysis_to_create = []
        for a_id in list(set(k[1] for k in list(data.keys()))):
            if not PlanificationAnalysis.search([
                    ('planification', '=', planification.id),
                    ('analysis', '=', a_id),
                    ]):
                analysis_to_create.append({
                    'planification': planification.id,
                    'analysis': a_id,
                    })
        if analysis_to_create:
            PlanificationAnalysis.create(analysis_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')
        TemplateAnalysis = pool.get('lims.template.analysis_sheet.analysis')

        template_analysis = {}
        for template in self.start.templates:
            cursor.execute('SELECT analysis, method '
                'FROM "' + TemplateAnalysis._table + '" '
                'WHERE template = %s',
                (template.id,))
            for res in cursor.fetchall():
                if res[0] not in template_analysis:
                    template_analysis[res[0]] = []
                if res[1]:
                    template_analysis[res[0]].append(res[1])
        if not template_analysis:
            return {}
        all_included_analysis_ids = ', '.join(str(x)
            for x in list(template_analysis.keys()))
        service_where = 'AND ad.analysis IN (%s) ' % all_included_analysis_ids

        planification_details = PlanificationServiceDetail.search([['OR',
            ('planification.state', '=', 'preplanned'),
            ('planification', '=', planification.id)],
            ('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)

        sql_select = ('SELECT nl.id, nb.fraction, srv.analysis' +
            ', nl.analysis, nl.method, 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:
            return {}

        result = {}
        nlines_added = []
        if extra_where:
            for nl in notebook_lines:
                if (template_analysis[nl[3]] and
                        nl[4] not in template_analysis[nl[3]]):
                    continue
                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': nl[2],
                        })
        else:
            for nl in notebook_lines:
                if (template_analysis[nl[3]] and
                        nl[4] not in template_analysis[nl[3]]):
                    continue
                f_ = nl[1]
                s_ = nl[2]
                result[(f_, s_)] = {
                    'repetition': nl[5],
                    }

        return result


class RelateTechniciansStart(metaclass=PoolMeta):
    __name__ = 'lims.planification.relate_technicians.start'

    @classmethod
    def __setup__(cls):
        super().__setup__()
        grouping = ('analysis_sheet', 'Analysis sheet')
        if grouping not in cls.grouping.selection:
            cls.grouping.selection.append(grouping)


class RelateTechniciansResult(metaclass=PoolMeta):
    __name__ = 'lims.planification.relate_technicians.result'

    details4 = fields.Many2Many(
        'lims.planification.relate_technicians.detail4', None, None,
        'Fractions to plan', domain=[('id', 'in', Eval('details4_domain'))],
        states={'invisible': ~Bool(Equal(Eval('grouping'), 'analysis_sheet'))},
        depends=['details4_domain', 'grouping'])
    details4_domain = fields.One2Many(
        'lims.planification.relate_technicians.detail4', None,
        'Fractions domain')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        grouping = ('analysis_sheet', 'Analysis sheet')
        if grouping not in cls.grouping.selection:
            cls.grouping.selection.append(grouping)

    @fields.depends('grouping')
    def on_change_grouping(self):
        super().on_change_grouping()
        self.details4 = []


class RelateTechniciansDetail4(ModelSQL, ModelView):
    'Fraction Detail'
    __name__ = 'lims.planification.relate_technicians.detail4'
    _table = 'lims_planification_relate_technicians_d4'

    fraction = fields.Many2One('lims.fraction', 'Fraction')
    template = fields.Many2One('lims.template.analysis_sheet',
        'Analysis sheet')
    services_list = fields.Char('Services')
    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, ('template', 'ASC'))


class RelateTechnicians(metaclass=PoolMeta):
    __name__ = 'lims.planification.relate_technicians'

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

        self.result.details4_domain = []

        if self.start.grouping == 'analysis_sheet':
            self.result.details4_domain = self._view_details4(planification_id,
                self.start.exclude_relateds)
        return super().transition_search()

    def default_result(self, fields):
        res = super().default_result(fields)
        details4_domain = []
        if self.result.details4_domain:
            details4_domain = [d.id for d in self.result.details4_domain]
        res['details4_domain'] = details4_domain
        return res

    def _view_details4(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')
        RelateTechniciansDetail4 = pool.get(
            'lims.planification.relate_technicians.detail4')
        Template = pool.get('lims.template.analysis_sheet')
        TemplateAnalysis = pool.get('lims.template.analysis_sheet.analysis')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        Service = pool.get('lims.service')

        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)

        details4 = {}
        cursor.execute('SELECT d.fraction, nl.analysis, nl.method, '
                'ad.service '
            '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 '
                'INNER JOIN "' + EntryDetailAnalysis._table + '" ad '
                    'ON nl.analysis_detail = ad.id '
            'WHERE d.planification = %s' +
            exclude_relateds_clause,
            (planification_id,))
        for x in cursor.fetchall():
            cursor.execute('SELECT t.id '
                'FROM "' + Template._table + '" t '
                    'INNER JOIN "' + TemplateAnalysis._table + '" ta '
                    'ON t.id = ta.template '
                'WHERE t.active IS TRUE '
                    'AND ta.analysis = %s '
                    'AND (ta.method = %s OR ta.method IS NULL)',
                (x[1], x[2]))
            template = cursor.fetchone()
            t = template and template[0] or None
            f = x[0]
            if (f, t) not in details4:
                details4[(f, t)] = {
                    'fraction': f,
                    'template': t,
                    'service_ids': [],
                    }
            details4[(f, t)]['service_ids'].append(x[3])

        to_create = []
        for d in details4.values():
            services = Service.browse(list(set(d['service_ids'])))
            to_create.append({
                'session_id': self._session_id,
                'fraction': d['fraction'],
                'template': d['template'],
                'services_list': ' // '.join(
                    s.analysis.rec_name for s in services),
                })
        return RelateTechniciansDetail4.create(to_create)

    def _get_details(self, planification_id):
        details = super()._get_details(planification_id)
        if self.start.grouping == 'analysis_sheet':
            details = self._get_details4(planification_id,
                self.start.exclude_relateds)
        return details

    def _get_details4(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')
        TemplateAnalysis = pool.get('lims.template.analysis_sheet.analysis')

        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.details4:

            template_analysis = {}
            cursor.execute('SELECT analysis, method '
                'FROM "' + TemplateAnalysis._table + '" '
                'WHERE template = %s',
                (detail.template and detail.template.id or None,))
            for res in cursor.fetchall():
                if res[0] not in template_analysis:
                    template_analysis[res[0]] = []
                if res[1]:
                    template_analysis[res[0]].append(res[1])
            if not template_analysis:
                continue
            all_included_analysis_ids = ', '.join(str(x)
                for x in list(template_analysis.keys()))
            analysis_where = ('AND nl.analysis IN (' +
                all_included_analysis_ids + ') ')

            cursor.execute('SELECT sd.id, nl.analysis, nl.method '
                '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 ' +
                    analysis_where + exclude_relateds_clause,
                (planification_id, detail.fraction.id))
            for x in cursor.fetchall():
                if (template_analysis[x[1]] and
                        x[2] not in template_analysis[x[1]]):
                    continue
                details.append(x[0])

        return PlanificationServiceDetail.browse(details)


class LaboratoryProfessional(metaclass=PoolMeta):
    __name__ = 'lims.laboratory.professional'

    sheets_queue = fields.Function(fields.Integer('# Sheets in queue'),
        'get_professional')
    sheets_process = fields.Function(fields.Integer('# Sheets in process'),
        'get_professional')
    samples_qty = fields.Function(fields.Integer('# Samples'),
        'get_professional')

    @classmethod
    def get_professional(cls, records, name):
        pool = Pool()
        Sheet = pool.get('lims.analysis_sheet')

        res = defaultdict(int)
        clause = [('state', 'in', ['draft', 'active'])]

        context = Transaction().context
        if context.get('laboratory'):
            clause.append(('laboratory', '=', context.get('laboratory')))
        if context.get('from_date'):
            clause.append(('date', '>=', datetime.combine(
                context.get('from_date'), time(0, 0))))
        if context.get('to_date'):
            clause.append(('date', '<=', datetime.combine(
                context.get('to_date'), time(23, 59))))

        sheets = Sheet.search(clause)
        for sheet in sheets:
            if name == 'sheets_queue' and sheet.state == 'draft':
                res[sheet.professional.id] += 1
            if name == 'sheets_process' and sheet.state == 'active':
                res[sheet.professional.id] += 1
            if name == 'samples_qty':
                res[sheet.professional.id] += sheet.samples_qty
        return res


class PlanificationProfessional(ModelSQL, ModelView):
    'Planification Professional'
    __name__ = 'lims.planification.professional'

    name = fields.Char('Name')
    sheets_queue = fields.Function(fields.Integer('# Sheets in queue'),
        'get_professional', searcher='search_professional')
    sheets_process = fields.Function(fields.Integer('# Sheets in process'),
        'get_professional', searcher='search_professional')
    samples_qty = fields.Function(fields.Integer('# Samples'),
        'get_professional', searcher='search_professional')

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

    @classmethod
    def table_query(cls):
        pool = Pool()
        Professional = pool.get('lims.laboratory.professional')
        Party = pool.get('party.party')
        professional = Professional.__table__()
        party = Party.__table__()
        columns = []
        for fname, field in cls._fields.items():
            if hasattr(field, 'set'):
                continue
            if fname == 'name':
                column = Column(party, 'name').as_(fname)
            else:
                column = Column(professional, fname).as_(fname)
            columns.append(column)
        return professional.join(
            party, condition=professional.party == party.id).select(*columns)

    @classmethod
    def get_professional(cls, records, name):
        pool = Pool()
        Professional = pool.get('lims.laboratory.professional')

        professionals = Professional.browse(records)
        return {p.id: getattr(p, name) for p in professionals}

    @classmethod
    def search_professional(cls, name, domain):
        pool = Pool()
        Professional = pool.get('lims.laboratory.professional')

        professionals = Professional.search([], order=[])

        _, operator_, operand = domain
        operator_ = {
            '=': operator.eq,
            '>=': operator.ge,
            '>': operator.gt,
            '<=': operator.le,
            '<': operator.lt,
            '!=': operator.ne,
            'in': lambda v, l: v in l,
            'not in': lambda v, l: v not in l,
            }.get(operator_, lambda v, l: False)

        ids = [p.id for p in professionals
            if operator_(getattr(p, name), operand)]
        return [('id', 'in', ids)]

    def get_rec_name(self, name):
        return self.name


class PlanificationProfessionalContext(ModelView):
    'Planification Professional Context'
    __name__ = 'lims.planification.professional.context'
    laboratory = fields.Many2One('lims.laboratory', 'Laboratory')
    from_date = fields.Date("From Date",
        domain=[
            If(Eval('to_date') & Eval('from_date'),
                ('from_date', '<=', Eval('to_date')),
                ()),
            ],
        depends=['to_date'])
    to_date = fields.Date("To Date",
        domain=[
            If(Eval('from_date') & Eval('to_date'),
                ('to_date', '>=', Eval('from_date')),
                ()),
            ],
        depends=['from_date'])

    @classmethod
    def default_laboratory(cls):
        return Transaction().context.get('laboratory')

    @classmethod
    def default_from_date(cls):
        return Transaction().context.get('from_date')

    @classmethod
    def default_to_date(cls):
        return Transaction().context.get('to_date')


class PlanificationProfessionalLine(ModelSQL, ModelView):
    'Planification Professional Line'
    __name__ = 'lims.planification.professional.line'

    template = fields.Many2One('lims.template.analysis_sheet', 'Template')
    laboratory = fields.Many2One('lims.laboratory', 'Laboratory')
    professional = fields.Many2One('lims.laboratory.professional',
        'Professional')
    date = fields.Date('Date')
    state = fields.Selection([
        ('draft', 'Draft'),
        ('active', 'Active'),
        ('validated', 'Validated'),
        ('done', 'Done'),
        ], 'State')
    samples_qty = fields.Function(fields.Integer('# Samples'),
        'get_fields')
    completion_percentage = fields.Function(fields.Numeric('Complete',
        digits=(1, 4), domain=[
            ('completion_percentage', '>=', 0),
            ('completion_percentage', '<=', 1),
            ]),
        'get_fields')
    color = fields.Function(fields.Char('Color'), 'get_color')

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

    @classmethod
    def table_query(cls):
        pool = Pool()
        Sheet = pool.get('lims.analysis_sheet')
        Compilation = pool.get('lims.interface.compilation')
        sheet = Sheet.__table__()
        compilation = Compilation.__table__()

        context = Transaction().context
        where = Literal(True)
        if context.get('laboratory'):
            where &= sheet.laboratory == context.get('laboratory')
        if context.get('from_date'):
            where &= compilation.date_time >= datetime.combine(
                context.get('from_date'), time(0, 0))
        if context.get('to_date'):
            where &= compilation.date_time <= datetime.combine(
                context.get('to_date'), time(23, 59))

        columns = []
        for fname, field in cls._fields.items():
            if hasattr(field, 'set'):
                continue
            if fname == 'date':
                column = Cast(compilation.date_time, 'date').as_(fname)
            else:
                column = Column(sheet, fname).as_(fname)
            columns.append(column)
        return sheet.join(compilation,
            condition=sheet.compilation == compilation.id).select(*columns,
            where=where)

    @classmethod
    def get_fields(cls, records, name):
        pool = Pool()
        Sheet = pool.get('lims.analysis_sheet')

        sheets = Sheet.browse(records)
        return {s.id: getattr(s, name) for s in sheets}

    def get_color(self, name):
        return 'lightblue'


class OpenSheetSample(Wizard):
    'Open Sheet Sample'
    __name__ = 'lims.planification.professional.open_sheet_sample'
    start_state = 'open_'
    open_ = StateAction('lims.act_lims_sample_list')

    def do_open_(self, action):
        pool = Pool()
        Sheet = pool.get('lims.analysis_sheet')
        Data = pool.get('lims.interface.data')

        sheet = Sheet(Transaction().context['active_id'])

        with Transaction().set_context(
                lims_interface_table=sheet.compilation.table.id):
            lines = Data.search([('compilation', '=', sheet.compilation.id)])
            samples = []
            for line in lines:
                nl = line.notebook_line
                if not nl:
                    continue
                if nl.sample.id not in samples:
                    samples.append(nl.sample.id)

        action['pyson_domain'] = [
            ('id', 'in', samples),
            ]
        action['pyson_domain'] = PYSONEncoder().encode(action['pyson_domain'])
        return action, {}


class SamplesPendingPlanning(ModelSQL, ModelView):
    'Samples Pending Planning'
    __name__ = 'lims.sample_pending_planning'

    name = fields.Char('Name')
    samples_qty = fields.Function(fields.Integer('# Samples'),
        'get_samples_qty')

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

    @classmethod
    def table_query(cls):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        PlanificationDetail = pool.get('lims.planification.detail')
        Planification = pool.get('lims.planification')
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')
        Fraction = pool.get('lims.fraction')
        Sample = pool.get('lims.sample')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        Analysis = pool.get('lims.analysis')
        TemplateAnalysis = pool.get('lims.template.analysis_sheet.analysis')
        Template = pool.get('lims.template.analysis_sheet')
        template = Template.__table__()

        context = Transaction().context

        cursor.execute('SELECT nl.id '
            'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + PlanificationServiceDetail._table +
                '" psd ON psd.notebook_line = nl.id '
                'INNER JOIN "' + PlanificationDetail._table + '" pd '
                'ON psd.detail = pd.id '
                'INNER JOIN "' + Planification._table + '" p '
                'ON pd.planification = p.id '
            'WHERE p.state = \'preplanned\' ')
        planned_lines = [x[0] for x in cursor.fetchall()]
        planned_lines_ids = ', '.join(str(x)
            for x in [0] + planned_lines)
        preplanned_where = 'AND nl.id NOT IN (%s) ' % planned_lines_ids

        sql_select = 'SELECT nl.analysis, nl.method, s.product_type, s.matrix '
        sql_from = (
            'FROM "' + NotebookLine._table + '" nl '
            'INNER JOIN "' + Analysis._table + '" nla '
            'ON nla.id = nl.analysis '
            'INNER JOIN "' + Notebook._table + '" n '
            'ON n.id = nl.notebook '
            'INNER JOIN "' + Fraction._table + '" f '
            'ON f.id = n.fraction '
            'INNER JOIN "' + Sample._table + '" s '
            'ON s.id = f.sample '
            'INNER JOIN "' + EntryDetailAnalysis._table + '" ad '
            'ON ad.id = nl.analysis_detail ')
        sql_where = (
            'WHERE ad.plannable = TRUE '
            'AND nl.start_date IS NULL '
            'AND nl.annulled = FALSE '
            'AND nla.behavior != \'internal_relation\' ' +
            preplanned_where)
        params = []

        if context.get('laboratory'):
            sql_where += 'AND nl.laboratory = %s '
            params.append(context.get('laboratory'))
        if context.get('department'):
            sql_where += 'AND nl.department = %s '
            params.append(context.get('department'))
        if context.get('date_from'):
            sql_where += 'AND ad.confirmation_date::date >= %s::date '
            params.append(context.get('date_from'))
        if context.get('date_to'):
            sql_where += 'AND ad.confirmation_date::date <= %s::date '
            params.append(context.get('date_to'))

        with Transaction().set_user(0):
            cursor.execute(sql_select + sql_from + sql_where, tuple(params))
        notebook_lines = cursor.fetchall()

        result = []
        for nl in notebook_lines:
            cursor.execute('SELECT t.id '
                'FROM "' + Template._table + '" t '
                    'INNER JOIN "' + TemplateAnalysis._table + '" ta '
                    'ON t.id = ta.template '
                'WHERE t.active IS TRUE '
                    'AND ta.analysis = %s '
                    'AND (ta.method = %s OR ta.method IS NULL) '
                    'AND (ta.product_type = %s OR ta.product_type IS NULL) '
                    'AND (ta.matrix = %s OR ta.matrix IS NULL)',
                (nl[0], nl[1], nl[2], nl[3]))
            template_id = cursor.fetchone()
            if template_id:
                result.append(template_id[0])
        template_ids = list(set(result))

        where = (template.id.in_(template_ids))
        if not template_ids:
            where = (template.id == -1)
        columns = []
        for fname, field in cls._fields.items():
            if hasattr(field, 'set'):
                continue
            columns.append(Column(template, fname).as_(fname))
        return template.select(*columns, where=where)

    @classmethod
    def get_samples_qty(cls, records, name):
        pool = Pool()
        Template = pool.get('lims.template.analysis_sheet')

        context = Transaction().context
        with Transaction().set_context(
                date_from=context.get('date_from') or datetime.min,
                date_to=context.get('date_to') or datetime.max,
                laboratory=context.get('laboratory'),
                department=context.get('department')):
            templates = Template.browse(records)
        return {t.id: getattr(t, 'pending_fractions') for t in templates}

    def get_rec_name(self, name):
        return self.name


class SamplesPendingPlanningContext(ModelView):
    'Samples Pending Planning Context'
    __name__ = 'lims.sample_pending_planning.context'

    laboratory = fields.Many2One('lims.laboratory', 'Laboratory')
    department = fields.Many2One('company.department', 'Department')
    date_from = fields.Date("From Date",
        domain=[
            If(Eval('date_to') & Eval('date_from'),
                ('date_from', '<=', Eval('date_to')),
                ()),
            ],
        depends=['date_to'])
    date_to = fields.Date("To Date",
        domain=[
            If(Eval('date_from') & Eval('date_to'),
                ('date_to', '>=', Eval('date_from')),
                ()),
            ],
        depends=['date_from'])

    @classmethod
    def default_laboratory(cls):
        return Transaction().context.get('laboratory')

    @classmethod
    def default_date_from(cls):
        return Transaction().context.get('date_from')

    @classmethod
    def default_date_to(cls):
        return Transaction().context.get('date_to')


class OpenPendingPlanningSample(Wizard):
    'Open Pending Planning Sample'
    __name__ = 'lims.sample_pending_planning.open_sample'

    start_state = 'open_'
    open_ = StateAction('lims.act_lims_sample_list')

    def do_open_(self, action):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        Template = pool.get('lims.template.analysis_sheet')
        PlanificationServiceDetail = pool.get(
            'lims.planification.service_detail')
        PlanificationDetail = pool.get('lims.planification.detail')
        Planification = pool.get('lims.planification')
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')
        Fraction = pool.get('lims.fraction')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        Analysis = pool.get('lims.analysis')

        context = Transaction().context
        template = Template(context['active_id'])
        template_analysis = {}
        for analysis in template.analysis:
            if analysis.analysis.id not in template_analysis:
                template_analysis[analysis.analysis.id] = []
            if analysis.method:
                template_analysis[analysis.analysis.id].append(
                    analysis.method.id)

        cursor.execute('SELECT nl.id '
            'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + PlanificationServiceDetail._table +
                '" psd ON psd.notebook_line = nl.id '
                'INNER JOIN "' + PlanificationDetail._table + '" pd '
                'ON psd.detail = pd.id '
                'INNER JOIN "' + Planification._table + '" p '
                'ON pd.planification = p.id '
            'WHERE p.state = \'preplanned\' ')
        planned_lines = [x[0] for x in cursor.fetchall()]
        planned_lines_ids = ', '.join(str(x)
            for x in [0] + planned_lines)
        preplanned_where = 'AND nl.id NOT IN (%s) ' % planned_lines_ids

        analysis_ids = ', '.join(str(x) for x in template_analysis.keys())
        sql_select = 'SELECT nl.analysis, nl.method, frc.sample '
        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 ')
        sql_where = (
            'WHERE ad.plannable = TRUE '
            'AND nl.start_date IS NULL '
            'AND nl.analysis IN (' + analysis_ids + ') '
            'AND nl.annulled = FALSE '
            'AND nla.behavior != \'internal_relation\' ' +
            preplanned_where)
        params = []

        if context.get('laboratory'):
            sql_where += 'AND nl.laboratory = %s '
            params.append(context.get('laboratory'))
        if context.get('department'):
            sql_where += 'AND nl.department = %s '
            params.append(context.get('department'))
        if context.get('date_from'):
            sql_where += 'AND ad.confirmation_date::date >= %s::date '
            params.append(context.get('date_from'))
        if context.get('date_to'):
            sql_where += 'AND ad.confirmation_date::date <= %s::date '
            params.append(context.get('date_to'))

        with Transaction().set_user(0):
            cursor.execute(sql_select + sql_from + sql_where, tuple(params))
        notebook_lines = cursor.fetchall()

        result = []
        for nl in notebook_lines:
            if (template_analysis[nl[0]] and
                    nl[1] not in template_analysis[nl[0]]):
                continue
            result.append(nl[2])
        samples = list(set(result))

        action['pyson_domain'] = [
            ('id', 'in', samples),
            ]
        action['pyson_domain'] = PYSONEncoder().encode(action['pyson_domain'])
        action['name'] += ' (%s)' % template.rec_name
        return action, {}
