# -*- 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 operator
import re
from collections import defaultdict
from datetime import datetime
from dateutil.relativedelta import relativedelta
from sql import Literal, Join

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


class Notebook(ModelSQL, ModelView):
    'Laboratory Notebook'
    __name__ = 'lims.notebook'

    fraction = fields.Many2One('lims.fraction', 'Fraction', required=True,
        readonly=True, ondelete='CASCADE', select=True)
    lines = fields.One2Many('lims.notebook.line', 'notebook', 'Lines')
    product_type = fields.Function(fields.Many2One('lims.product.type',
        'Product type'), 'get_sample_field', searcher='search_sample_field')
    matrix = fields.Function(fields.Many2One('lims.matrix', 'Matrix'),
        'get_sample_field', searcher='search_sample_field')
    party = fields.Function(fields.Many2One('party.party', 'Party'),
        'get_sample_field', searcher='search_sample_field')
    party_code = fields.Function(fields.Char('Party'), 'get_party_code',
        searcher='search_party_code')
    invoice_party = fields.Function(fields.Many2One('party.party',
        'Invoice Party'),
        'get_sample_field', searcher='search_sample_field')
    label = fields.Function(fields.Char('Label'), 'get_sample_field',
        searcher='search_sample_field')
    date = fields.Function(fields.DateTime('Date'), 'get_sample_field',
        searcher='search_sample_field')
    date2 = fields.Function(fields.Date('Date'), 'get_sample_field',
        searcher='search_sample_field')
    sample_comments = fields.Function(fields.Text('Sample Comments'),
        'get_sample_field')
    fraction_type = fields.Function(fields.Many2One('lims.fraction.type',
        'Fraction type'), 'get_fraction_field',
        searcher='search_fraction_field')
    fraction_comments = fields.Function(fields.Text('Fraction Comments'),
        'get_fraction_field')
    shared = fields.Function(fields.Boolean('Shared'), 'get_fraction_field',
        searcher='search_fraction_field')
    current_location = fields.Function(fields.Many2One('stock.location',
        'Current Location'), 'get_current_location',
        searcher='search_current_location')
    divided_report = fields.Function(fields.Boolean('Divided report'),
        'get_divided_report')
    icon = fields.Function(fields.Char("Icon"), 'get_icon')
    obj_description = fields.Function(fields.Char('Objective description',
        translate=True), 'get_obj_description')
    completion_percentage = fields.Function(fields.Numeric('Complete',
        digits=(1, 4)), 'get_sample_field')
    department = fields.Function(fields.Many2One('company.department',
        'Department'), 'get_department', searcher='search_department')
    state = fields.Function(fields.Char('State'), 'get_state',
        searcher='search_state')
    lines_pending_reporting = fields.Function(fields.One2Many(
        'lims.notebook.line', None, 'Lines'),
        'get_lines_pending_reporting')
    acceptance_pending = fields.Function(fields.Boolean('Pending acceptance'),
        'get_acceptance_pending', searcher='search_acceptance_pending')
    urgent = fields.Function(fields.Boolean('Urgent'), 'get_urgent',
        searcher='search_urgent')
    entry_summary = fields.Function(fields.Char('Entry / Qty. Samples'),
        'get_entry_summary', searcher='search_entry_summary')

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

    def get_rec_name(self, name):
        if self.fraction:
            return self.fraction.rec_name

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

    @classmethod
    def get_sample_field(cls, notebooks, names):
        result = {}
        for name in names:
            result[name] = {}
            if cls._fields[name]._type == 'many2one':
                for n in notebooks:
                    field = getattr(n.fraction.sample, name, None)
                    result[name][n.id] = field.id if field else None
            elif name == 'sample_comments':
                for n in notebooks:
                    result[name][n.id] = getattr(
                        n.fraction.sample, 'comments', None)
            else:
                for n in notebooks:
                    result[name][n.id] = getattr(n.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_party = _order_sample_field('party')
    order_product_type = _order_sample_field('product_type')
    order_matrix = _order_sample_field('matrix')
    order_label = _order_sample_field('label')
    order_date = _order_sample_field('date')

    @classmethod
    def get_obj_description(cls, notebooks, name):
        result = {}
        for n in notebooks:
            field = getattr(n.fraction.sample, 'obj_description', None)
            if field:
                result[n.id] = field.description
            else:
                result[n.id] = getattr(n.fraction.sample,
                    'obj_description_manual', None)
        return result

    @classmethod
    def get_party_code(cls, notebooks, name):
        result = {}
        for n in notebooks:
            result[n.id] = n.party.code
        return result

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

    @classmethod
    def get_fraction_field(cls, notebooks, names):
        result = {}
        for name in names:
            result[name] = {}
            if name == 'fraction_type':
                for n in notebooks:
                    field = getattr(n.fraction, 'type', None)
                    result[name][n.id] = field.id if field else None
            elif name == 'fraction_comments':
                for n in notebooks:
                    result[name][n.id] = getattr(n.fraction, 'comments', None)
            else:
                for n in notebooks:
                    result[name][n.id] = getattr(n.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_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)
    order_fraction_type = _order_fraction_field('type')
    order_shared = _order_fraction_field('shared')

    def get_divided_report(self, name):
        if not self.fraction or not self.fraction.services:
            return False
        for s in self.fraction.services:
            if s.divide:
                return True
        return False

    @classmethod
    def get_current_location(cls, notebooks, name=None):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        Move = pool.get('stock.move')

        result = {}
        for n in notebooks:
            cursor.execute('SELECT to_location '
                'FROM "' + Move._table + '" '
                'WHERE fraction = %s '
                    'AND state IN (\'assigned\', \'done\') '
                'ORDER BY effective_date DESC, id DESC '
                'LIMIT 1', (n.fraction.id,))
            location = cursor.fetchone()
            result[n.id] = location[0] if location else None
        return result

    @classmethod
    def search_current_location(cls, name, domain=None):

        def _search_current_location_eval_domain(line, domain):
            operator_funcs = {
                '=': 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,
                'ilike': lambda v, l: False,
                }
            field, op, operand = domain
            value = line.get(field)
            return operator_funcs[op](value, operand)

        if domain and domain[1] == 'ilike':
            Location = Pool().get('stock.location')
            locations = Location.search([
                ('code', '=', domain[2]),
                ], order=[])
            if not locations:
                locations = Location.search([
                    ('name',) + tuple(domain[1:]),
                    ], order=[])
                if not locations:
                    return []
            domain = ('current_location', 'in', [l.id for l in locations])

        all_notebooks = cls.search([])
        current_locations = iter(
            cls.get_current_location(all_notebooks).items())

        processed_lines = [{
            'fraction': fraction,
            'current_location': location,
            } for fraction, location in current_locations]

        record_ids = [line['fraction'] for line in processed_lines
            if _search_current_location_eval_domain(line, domain)]
        return [('id', 'in', record_ids)]

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

    @classmethod
    def get_department(cls, notebooks, name):
        result = {}
        for n in notebooks:
            field = getattr(n.fraction.sample.product_type, name, None)
            result[n.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:])]

    @classmethod
    def get_state(cls, notebooks, name=None):
        result = {}
        laboratory_id = Transaction().context.get(
            'samples_pending_reporting_laboratory', None)
        if not laboratory_id:
            for n in notebooks:
                result[n.id] = None
            return result
        for n in notebooks:
            result[n.id] = cls._get_notebook_state(n.id, laboratory_id)
        return result

    @classmethod
    def _get_notebook_state(cls, notebook_id, laboratory_id):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        ResultsLine = pool.get('lims.results_report.version.detail.line')
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')
        Fraction = pool.get('lims.fraction')
        FractionType = pool.get('lims.fraction.type')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        ResultModifier = pool.get('lims.result_modifier')

        draft_lines_ids = ResultsLine.get_draft_lines_ids(
            laboratory_id, notebook_id)
        draft_lines_ids = ', '.join(str(l) for l in [0] + draft_lines_ids)

        sql_query = ('SELECT COUNT(*) '
            'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + Notebook._table + '" n '
                'ON n.id = nl.notebook '
                'INNER JOIN "' + Fraction._table + '" f '
                'ON f.id = n.fraction '
                'INNER JOIN "' + FractionType._table + '" ft '
                'ON ft.id = f.type '
                'LEFT JOIN "' + ResultModifier._table + '" rm '
                'ON rm.id = nl.result_modifier '
            'WHERE nl.notebook = %s '
                'AND nl.laboratory = %s '
                'AND ft.report = TRUE '
                'AND nl.report = TRUE '
                'AND nl.annulled = FALSE '
                'AND nl.results_report IS NULL '
                'AND nl.accepted = TRUE '
                'AND nl.id NOT IN (' + draft_lines_ids + ') ')

        excluded_notebooks = cls._get_excluded_notebooks([notebook_id],
            laboratory_id)
        if excluded_notebooks:
            for n_id, grouper in excluded_notebooks:
                cursor.execute('SELECT nl.id '
                    'FROM "' + NotebookLine._table + '" nl '
                        'INNER JOIN "' + EntryDetailAnalysis._table + '" d '
                        'ON d.id = nl.analysis_detail '
                    'WHERE nl.notebook = %s AND d.report_grouper = %s',
                    (n_id, grouper))
                excluded_lines = [x[0] for x in cursor.fetchall()]
                excluded_lines_ids = ', '.join(str(l)
                    for l in [0] + excluded_lines)
                sql_query += 'AND nl.id NOT IN (' + excluded_lines_ids + ') '
        cursor.execute(sql_query, (notebook_id, laboratory_id))
        if cursor.fetchone()[0] > 0:
            return 'complete'

        sql_query = ('SELECT COUNT(*) '
            'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + Notebook._table + '" n '
                'ON n.id = nl.notebook '
                'INNER JOIN "' + Fraction._table + '" f '
                'ON f.id = n.fraction '
                'INNER JOIN "' + FractionType._table + '" ft '
                'ON ft.id = f.type '
                'LEFT JOIN "' + ResultModifier._table + '" rm '
                'ON rm.id = nl.result_modifier '
            'WHERE nl.notebook = %s '
                'AND nl.laboratory = %s '
                'AND ft.report = TRUE '
                'AND nl.report = TRUE '
                'AND nl.annulled = FALSE '
                'AND nl.results_report IS NULL '
                'AND nl.id NOT IN (' + draft_lines_ids + ') ')
        sql_query += cls._get_samples_in_progress_sql_clause()

        cursor.execute(sql_query, (notebook_id, laboratory_id))
        if cursor.fetchone()[0] > 0:
            return 'in_progress'

        return None

    @classmethod
    def search_state(cls, name, domain=None):
        if domain[2] == 'complete':
            notebooks_ids = cls._get_notebooks_complete()
            return [('id', 'in', notebooks_ids)]
        if domain[2] == 'in_progress':
            notebooks_ids = cls._get_notebooks_in_progress()
            return [('id', 'in', notebooks_ids)]
        return [('id', '=', -1)]

    @classmethod
    def _get_notebooks_complete(cls):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        ResultsLine = pool.get('lims.results_report.version.detail.line')
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')
        Fraction = pool.get('lims.fraction')
        FractionType = pool.get('lims.fraction.type')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')

        laboratory_id = Transaction().context.get(
            'samples_pending_reporting_laboratory', None)
        if not laboratory_id:
            return []

        draft_lines_ids = ResultsLine.get_draft_lines_ids(laboratory_id)
        draft_lines_ids = ', '.join(str(l) for l in [0] + draft_lines_ids)

        sql_query = ('SELECT nl.notebook '
            'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + Notebook._table + '" n '
                'ON n.id = nl.notebook '
                'INNER JOIN "' + Fraction._table + '" f '
                'ON f.id = n.fraction '
                'INNER JOIN "' + FractionType._table + '" ft '
                'ON ft.id = f.type '
            'WHERE nl.laboratory = %s '
                'AND ft.report = TRUE '
                'AND nl.report = TRUE '
                'AND nl.annulled = FALSE '
                'AND nl.results_report IS NULL '
                'AND nl.accepted = TRUE '
                'AND nl.id NOT IN (' + draft_lines_ids + ') ')
        cursor.execute(sql_query, (laboratory_id,))
        notebooks_ids = [x[0] for x in cursor.fetchall()]

        excluded_notebooks = cls._get_excluded_notebooks(notebooks_ids,
            laboratory_id)
        if excluded_notebooks:
            for n_id, grouper in excluded_notebooks:
                cursor.execute('SELECT nl.id '
                    'FROM "' + NotebookLine._table + '" nl '
                        'INNER JOIN "' + EntryDetailAnalysis._table + '" d '
                        'ON d.id = nl.analysis_detail '
                    'WHERE nl.notebook = %s AND d.report_grouper = %s',
                    (n_id, grouper))
                excluded_lines = [x[0] for x in cursor.fetchall()]
                excluded_lines_ids = ', '.join(str(l)
                    for l in [0] + excluded_lines)
                sql_query += 'AND nl.id NOT IN (' + excluded_lines_ids + ') '
            cursor.execute(sql_query, (laboratory_id,))
            notebooks_ids = [x[0] for x in cursor.fetchall()]
        return notebooks_ids

    @classmethod
    def _get_excluded_notebooks(cls, notebooks_ids, laboratory_id):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        Notebook = pool.get('lims.notebook')
        Fraction = pool.get('lims.fraction')
        Sample = pool.get('lims.sample')
        Entry = pool.get('lims.entry')
        FractionType = pool.get('lims.fraction.type')

        notebooks_ids = '\', \''.join(str(n) for n in notebooks_ids + [0])

        cursor.execute('SELECT nl.notebook, nl.analysis, nl.method, '
                'd.report_grouper, nl.accepted '
            'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + EntryDetailAnalysis._table + '" d '
                'ON d.id = nl.analysis_detail '
                '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 "' + Entry._table + '" e '
                'ON e.id = s.entry '
                'INNER JOIN "' + FractionType._table + '" ft '
                'ON ft.id = f.type '
            'WHERE nl.laboratory = %s '
                'AND ft.report = TRUE '
                'AND nl.report = TRUE '
                'AND nl.annulled = FALSE '
                'AND n.id IN (\'' + notebooks_ids + '\')',
            (laboratory_id,))
        notebook_lines = cursor.fetchall()

        # Check repetitions
        oks, to_check = [], []
        accepted_notebooks = []
        for line in notebook_lines:
            key = (line[0], line[1], line[2], line[3])
            if not line[4]:
                to_check.append(key)
            else:
                oks.append(key)
                accepted_notebooks.append(line[0])

        to_check = list(set(to_check) - set(oks))
        accepted_notebooks = list(set(accepted_notebooks))

        excluded_notebooks = set()
        for n_id, a_id, m_id, grouper in to_check:
            if n_id not in accepted_notebooks:
                continue
            key = (n_id, grouper)
            excluded_notebooks.add(key)
        return excluded_notebooks

    @classmethod
    def _get_notebooks_in_progress(cls):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        ResultsLine = pool.get('lims.results_report.version.detail.line')
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')
        Fraction = pool.get('lims.fraction')
        FractionType = pool.get('lims.fraction.type')
        ResultModifier = pool.get('lims.result_modifier')

        laboratory_id = Transaction().context.get(
            'samples_pending_reporting_laboratory', None)
        if not laboratory_id:
            return []

        draft_lines_ids = ResultsLine.get_draft_lines_ids(laboratory_id)
        draft_lines_ids = ', '.join(str(l) for l in [0] + draft_lines_ids)

        sql_query = ('SELECT nl.notebook '
            'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + Notebook._table + '" n '
                'ON n.id = nl.notebook '
                'INNER JOIN "' + Fraction._table + '" f '
                'ON f.id = n.fraction '
                'INNER JOIN "' + FractionType._table + '" ft '
                'ON ft.id = f.type '
                'LEFT JOIN "' + ResultModifier._table + '" rm '
                'ON rm.id = nl.result_modifier '
            'WHERE nl.laboratory = %s '
                'AND ft.report = TRUE '
                'AND nl.report = TRUE '
                'AND nl.annulled = FALSE '
                'AND nl.results_report IS NULL '
                'AND nl.id NOT IN (' + draft_lines_ids + ') ')
        sql_query += cls._get_samples_in_progress_sql_clause()
        cursor.execute(sql_query, (laboratory_id,))
        notebooks_ids = [x[0] for x in cursor.fetchall()]

        complete_notebooks_ids = cls._get_notebooks_complete()
        if complete_notebooks_ids:
            notebooks_ids = list(
                set(notebooks_ids) - set(complete_notebooks_ids))
        return notebooks_ids

    @classmethod
    def _get_samples_in_progress_clause(cls):
        Config = Pool().get('lims.configuration')
        samples_in_progress = Config(1).samples_in_progress
        clause = []
        if samples_in_progress == 'accepted':
            clause = [('accepted', '=', True)]
        elif samples_in_progress == 'result':
            clause = [['OR',
                ('result', 'not in', [None, '']),
                ('literal_result', 'not in', [None, '']),
                ('result_modifier.code', 'in', [
                    'd', 'nd', 'pos', 'neg', 'ni', 'abs', 'pre', 'na']),
                ]]
        return clause

    @classmethod
    def _get_samples_in_progress_sql_clause(cls):
        Config = Pool().get('lims.configuration')
        samples_in_progress = Config(1).samples_in_progress
        sql_clause = ''
        if samples_in_progress == 'accepted':
            sql_clause = 'AND nl.accepted = TRUE '
        elif samples_in_progress == 'result':
            sql_clause = ('AND ((nl.result IS NOT NULL '
                'AND nl.result != \'\') '
                'OR (nl.literal_result IS NOT NULL '
                'AND nl.literal_result != \'\') '
                'OR rm.code IN '
                '(\'d\', \'nd\', \'pos\', \'neg\', '
                '\'ni\', \'abs\', \'pre\', \'na\')) ')
        return sql_clause

    @classmethod
    def view_toolbar_get(cls):
        if not Transaction().context.get('samples_pending_reporting', False):
            return super().view_toolbar_get()

        # Samples Pending Reporting uses specific keywords
        prints = cls.get_samples_pending_reporting_keyword('form_print')
        actions = cls.get_samples_pending_reporting_keyword('form_action')
        relates = cls.get_samples_pending_reporting_keyword('form_relate')
        result = {
            'print': prints,
            'action': actions,
            'relate': relates,
            'exports': [],
            }
        return result

    @classmethod
    def get_samples_pending_reporting_keyword(cls, keyword):
        """
        Method copied from ActionKeyword. It search for specific keywords
        for Samples Pending Reporting: lims.notebook,-2
        """
        pool = Pool()
        ActionKeyword = pool.get('ir.action.keyword')
        Action = pool.get('ir.action')

        key = (keyword, ('lims.notebook', -2))
        keywords = ActionKeyword._get_keyword_cache.get(key)
        if keywords is not None:
            return keywords

        clause = [
            ('keyword', '=', keyword),
            ('model', '=', 'lims.notebook,-2'),
            ('action.active', '=', True),
            ]
        action_keywords = ActionKeyword.search(clause, order=[])
        types = defaultdict(list)
        for action_keyword in action_keywords:
            type_ = action_keyword.action.type
            types[type_].append(action_keyword.action.id)
        keywords = []
        for type_, action_ids in types.items():
            for value in Action.get_action_values(type_, action_ids):
                value['keyword'] = keyword
                keywords.append(value)
        keywords.sort(key=operator.itemgetter('name'))
        ActionKeyword._get_keyword_cache.set(key, keywords)
        return keywords

    def get_lines_pending_reporting(self, name=None):
        laboratory_id = Transaction().context.get(
            'samples_pending_reporting_laboratory', None)
        if not laboratory_id:
            return []
        lines = self._get_lines_for_reporting(laboratory_id, 'complete')
        return [l.id for l in lines]

    def _get_lines_for_reporting(self, laboratory_id, state):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        ResultsLine = pool.get('lims.results_report.version.detail.line')
        NotebookLine = pool.get('lims.notebook.line')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')

        draft_lines_ids = ResultsLine.get_draft_lines_ids(
            laboratory_id, self.id)

        clause = [
            ('notebook', '=', self.id),
            ('laboratory', '=', laboratory_id),
            ('notebook.fraction.type.report', '=', True),
            ('report', '=', True),
            ('annulled', '=', False),
            ('results_report', '=', None),
            ('id', 'not in', draft_lines_ids),
            ]
        if state == 'in_progress':
            clause.extend(self._get_samples_in_progress_clause())
        else:
            clause.append(('accepted', '=', True))
            excluded_notebooks = self._get_excluded_notebooks(
                [self.id], laboratory_id)
            if excluded_notebooks:
                for n_id, grouper in excluded_notebooks:
                    cursor.execute('SELECT nl.id '
                        'FROM "' + NotebookLine._table + '" nl '
                        'INNER JOIN "' + EntryDetailAnalysis._table + '" d '
                        'ON d.id = nl.analysis_detail '
                        'WHERE nl.notebook = %s AND d.report_grouper = %s',
                        (n_id, grouper))
                    excluded_notebook_lines = [x[0] for x in cursor.fetchall()]
                    clause.append(('id', 'not in', excluded_notebook_lines))

        return NotebookLine.search(clause)

    @classmethod
    def get_acceptance_pending(cls, notebooks, name=None):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        Fraction = pool.get('lims.fraction')
        FractionType = pool.get('lims.fraction.type')
        ResultModifier = pool.get('lims.result_modifier')

        result = {}
        for n in notebooks:
            result[n.id] = False
            cursor.execute('SELECT COUNT(*) '
                'FROM "' + NotebookLine._table + '" nl '
                    'INNER JOIN "' + cls._table + '" n '
                    'ON n.id = nl.notebook '
                    'INNER JOIN "' + Fraction._table + '" f '
                    'ON f.id = n.fraction '
                    'INNER JOIN "' + FractionType._table + '" ft '
                    'ON ft.id = f.type '
                    'LEFT JOIN "' + ResultModifier._table + '" rm '
                    'ON rm.id = nl.result_modifier '
                'WHERE ft.report = TRUE '
                    'AND nl.report = TRUE '
                    'AND nl.annulled = FALSE '
                    'AND nl.accepted = FALSE '
                    'AND (nl.result IS NOT NULL '
                        'OR nl.literal_result IS NOT NULL '
                        'OR rm.code IN '
                        '(\'d\', \'nd\', \'pos\', \'neg\', '
                        '\'ni\', \'abs\', \'pre\', \'na\')) '
                    'AND nl.notebook = %s',
                (n.id,))
            if cursor.fetchone()[0] > 0:
                result[n.id] = True
        return result

    @classmethod
    def search_acceptance_pending(cls, name, domain=None):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        Fraction = pool.get('lims.fraction')
        FractionType = pool.get('lims.fraction.type')
        ResultModifier = pool.get('lims.result_modifier')

        cursor.execute('SELECT nl.notebook '
            'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + cls._table + '" n '
                'ON n.id = nl.notebook '
                'INNER JOIN "' + Fraction._table + '" f '
                'ON f.id = n.fraction '
                'INNER JOIN "' + FractionType._table + '" ft '
                'ON ft.id = f.type '
                'LEFT JOIN "' + ResultModifier._table + '" rm '
                'ON rm.id = nl.result_modifier '
            'WHERE ft.report = TRUE '
                'AND nl.report = TRUE '
                'AND nl.annulled = FALSE '
                'AND nl.accepted = FALSE '
                'AND (nl.result IS NOT NULL '
                    'OR nl.literal_result IS NOT NULL '
                    'OR rm.code IN '
                    '(\'d\', \'nd\', \'pos\', \'neg\', '
                    '\'ni\', \'abs\', \'pre\', \'na\'))')
        notebooks_ids = [x[0] for x in cursor.fetchall()]
        field, op, operand = domain
        if (op, operand) in (('=', True), ('!=', False)):
            return [('id', 'in', notebooks_ids)]
        elif (op, operand) in (('=', False), ('!=', True)):
            return [('id', 'not in', notebooks_ids)]
        return []

    @classmethod
    def get_urgent(cls, notebooks, name):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')

        result = {}
        for n in notebooks:
            lines = NotebookLine.search_count([
                ('notebook', '=', n.id),
                ('urgent', '=', True),
                ])
            result[n.id] = True if lines > 0 else False
        return result

    @classmethod
    def search_urgent(cls, name, clause):
        field, op, operand = clause
        if (op, operand) in (('=', True), ('!=', False)):
            return [('lines.urgent', '=', True)]
        elif (op, operand) in (('=', False), ('!=', True)):
            urgents = cls.search([('lines.urgent', '=', True)])
            return [('id', 'not in', [u.id for u in urgents])]
        return []

    @classmethod
    def get_entry_summary(cls, notebooks, name):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        Entry = pool.get('lims.entry')
        Sample = pool.get('lims.sample')
        Fraction = pool.get('lims.fraction')

        result = {}
        for n in notebooks:
            result[n.id] = ''

            cursor.execute('SELECT s.entry '
                'FROM "' + Sample._table + '" s '
                    'INNER JOIN "' + Fraction._table + '" f '
                    'ON s.id = f.sample '
                'WHERE f.id = %s',
                (str(n.fraction.id),))
            entry_id = cursor.fetchone()[0]

            cursor.execute('SELECT e.number, count(s.id) '
                'FROM "' + Entry._table + '" e '
                    'INNER JOIN "' + Sample._table + '" s '
                    'ON e.id = s.entry '
                'WHERE e.id = %s '
                'GROUP BY e.number',
                (str(entry_id),))
            res = cursor.fetchone()
            if not res:
                continue
            result[n.id] = '%s/%s' % (res[0], res[1])
        return result

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


class NotebookLine(ModelSQL, ModelView):
    'Laboratory Notebook Line'
    __name__ = 'lims.notebook.line'

    _states = {'readonly': Bool(Eval('accepted'))}
    _depends = ['accepted']

    notebook = fields.Many2One('lims.notebook', 'Laboratory notebook',
        ondelete='CASCADE', select=True, required=True)
    analysis_detail = fields.Many2One('lims.entry.detail.analysis',
        'Analysis detail', ondelete='CASCADE', select=True)
    service = fields.Many2One('lims.service', 'Service', readonly=True,
        ondelete='CASCADE', select=True)
    analysis = fields.Many2One('lims.analysis', 'Analysis', required=True,
        readonly=True, select=True)
    repetition = fields.Integer('Repetition', readonly=True)
    start_date = fields.Date('Start date', states={'readonly': True},
        select=True)
    end_date = fields.Date('End date', states={
        'readonly': Or(~Bool(Eval('start_date')), Bool(Eval('accepted'))),
        }, depends=['start_date', 'accepted'], select=True)
    laboratory = fields.Many2One('lims.laboratory', 'Laboratory',
        readonly=True, select=True)
    method = fields.Many2One('lims.lab.method', 'Method',
        required=True, states=_states,
        domain=['OR', ('id', '=', Eval('method')),
            ('id', 'in', Eval('method_domain'))],
        depends=['accepted', 'method_domain'])
    method_view = fields.Function(fields.Many2One('lims.lab.method',
        'Method'), 'get_views_field')
    method_domain = fields.Function(fields.Many2Many('lims.lab.method',
        None, None, 'Method domain'),
        'on_change_with_method_domain')
    method_version = fields.Many2One('lims.lab.method.version',
        'Method version', readonly=True)
    device = fields.Many2One('lims.lab.device', 'Device',
        states=_states,
        domain=['OR', ('id', '=', Eval('device')),
            ('id', 'in', Eval('device_domain'))],
        depends=['accepted', 'device_domain'])
    device_view = fields.Function(fields.Many2One('lims.lab.device',
        'Device'), 'get_views_field')
    device_domain = fields.Function(fields.Many2Many('lims.lab.device',
        None, None, 'Device domain'), 'on_change_with_device_domain')
    analysis_origin = fields.Char('Analysis origin', readonly=True)
    initial_concentration = fields.Char('Initial concentration',
        translate=True, states=_states, depends=_depends)
    final_concentration = fields.Char('Final concentration',
        translate=True, states=_states, depends=_depends)
    literal_final_concentration = fields.Char('Literal Final concentration',
        translate=True, states=_states, depends=_depends)
    laboratory_professionals = fields.Many2Many(
        'lims.notebook.line-laboratory.professional', 'notebook_line',
        'professional', 'Preparation professionals',
        states=_states, depends=_depends)
    initial_unit = fields.Many2One('product.uom', 'Initial unit',
        domain=[('category.lims_only_available', '=', True)],
        states=_states, depends=_depends)
    final_unit = fields.Many2One('product.uom', 'Final unit',
        domain=[('category.lims_only_available', '=', True)],
        states=_states, depends=_depends)
    result_modifier = fields.Many2One('lims.result_modifier',
        'Result modifier', select=True,
        states=_states, depends=_depends)
    converted_result_modifier = fields.Many2One('lims.result_modifier',
        'Converted result modifier', select=True,
        states=_states, depends=_depends)
    result = fields.Char('Result',
        states=_states, depends=_depends)
    converted_result = fields.Char('Converted result',
        states=_states, depends=_depends)
    formated_result = fields.Function(fields.Char('Result to report'),
        'get_formated_result')
    formated_converted_result = fields.Function(fields.Char(
        'Converted result to report'), 'get_formated_converted_result')
    detection_limit = fields.Char('Detection limit',
        states=_states, depends=_depends)
    quantification_limit = fields.Char('Quantification limit',
        states=_states, depends=_depends)
    lower_limit = fields.Char('Lower limit allowed',
        states=_states, depends=_depends)
    upper_limit = fields.Char('Upper limit allowed',
        states=_states, depends=_depends)
    check_result_limits = fields.Function(fields.Boolean(
        'Validate limits directly on the result'), 'get_typification_field')
    chromatogram = fields.Char('Chromatogram',
        states=_states, depends=_depends)
    professionals = fields.One2Many('lims.notebook.line.professional',
        'notebook_line', 'Analytic professionals',
        states=_states, depends=_depends)
    comments = fields.Text('Entry comments',
        states=_states, depends=_depends)
    theoretical_concentration = fields.Char('Theoretical concentration',
        states=_states, depends=_depends)
    concentration_level = fields.Many2One('lims.concentration.level',
        'Concentration level',
        states=_states, depends=_depends, select=True)
    decimals = fields.Integer('Decimals',
        states=_states, depends=_depends)
    significant_digits = fields.Integer('Significant digits',
        states=_states, depends=_depends)
    scientific_notation = fields.Boolean('Scientific notation',
        states=_states, depends=_depends)
    backup = fields.Char('Backup',
        states=_states, depends=_depends)
    reference = fields.Char('Reference',
        states=_states, depends=_depends)
    literal_result = fields.Char('Literal result', translate=True,
        states=_states, depends=_depends)
    rm_correction_formula = fields.Char('RM Correction Formula',
        states=_states, depends=_depends)
    report = fields.Boolean('Report',
        states=_states, depends=_depends, select=True)
    uncertainty = fields.Char('Uncertainty',
        states=_states, depends=_depends)
    verification = fields.Char('Verification',
        states=_states, depends=_depends)
    analysis_order = fields.Function(fields.Integer('Order'),
        'get_analysis_order')
    dilution_factor = fields.Float('Dilution factor',
        states=_states, depends=_depends)
    accepted = fields.Boolean('Accepted', select=True)
    acceptance_date = fields.DateTime('Acceptance date',
        states={'readonly': True})
    not_accepted_message = fields.Text('Message', readonly=True,
        states={'invisible': Not(Bool(Eval('not_accepted_message')))})
    annulled = fields.Boolean('Annulled', states={'readonly': True},
        select=True)
    annulment_date = fields.DateTime('Annulment date',
        states={'readonly': True})
    annulment_reason = fields.Text('Annulment reason',
        states={'readonly': True, 'invisible': ~Eval('annulled')},
        depends=['annulled'])
    results_report = fields.Many2One('lims.results_report', 'Results Report',
        readonly=True, select=True)
    planification = fields.Many2One('lims.planification', 'Planification',
        readonly=True)
    urgent = fields.Boolean('Urgent',
        states=_states, depends=_depends)
    priority = fields.Function(fields.Integer('Priority'),
        'get_service_field', searcher='search_service_field')
    fraction = fields.Function(fields.Many2One('lims.fraction', 'Fraction'),
        'get_service_field', searcher='search_service_field')
    fraction_type = fields.Function(fields.Many2One('lims.fraction.type',
        'Fraction type'), 'get_fraction_field',
        searcher='search_fraction_field')
    sample = fields.Function(fields.Many2One('lims.sample', 'Sample'),
        'get_fraction_field', searcher='search_fraction_field')
    party = fields.Function(fields.Many2One('party.party', 'Party'),
        'get_fraction_field', searcher='search_fraction_field')
    product_type = fields.Function(fields.Many2One('lims.product.type',
        'Product type'), 'get_sample_field', searcher='search_sample_field')
    matrix = fields.Function(fields.Many2One('lims.matrix', 'Matrix'),
        'get_sample_field', searcher='search_sample_field')
    label = fields.Function(fields.Char('Label'), 'get_sample_field',
        searcher='search_sample_field')
    date = fields.Function(fields.DateTime('Date'), 'get_sample_field',
        searcher='search_sample_field')
    date2 = fields.Function(fields.Date('Date'), 'get_sample_field',
        searcher='search_sample_field')
    report_type = fields.Function(fields.Char('Report type'),
        'get_typification_field', searcher='search_typification_field')
    report_result_type = fields.Function(fields.Char('Result type'),
        'get_typification_field', searcher='search_typification_field')
    results_estimated_waiting = fields.Integer(
        'Estimated number of days for results', states={'readonly': True})
    results_estimated_date = fields.Function(fields.Date(
        'Estimated date of result'), 'get_results_estimated_date')
    laboratory_date = fields.Function(fields.Date('Laboratory deadline'),
        'get_service_field', searcher='search_service_field')
    report_date = fields.Function(fields.Date('Date agreed for result'),
        'get_service_field', searcher='search_service_field')
    department = fields.Many2One('company.department', 'Department',
        domain=['OR', ('id', '=', Eval('department')),
            ('id', 'in', Eval('department_domain'))],
        states=_states, depends=_depends + ['department_domain'])
    department_domain = fields.Function(fields.Many2Many('company.department',
        None, None, 'Department domain'), 'on_change_with_department_domain')
    icon = fields.Function(fields.Char("Icon"), 'get_icon')
    planning_comments = fields.Function(fields.Text('Planification comments'),
        'get_planning_comments')
    controls = fields.Many2Many('lims.notebook.line-fraction',
        'notebook_line', 'fraction', 'Controls',
        states=_states, depends=_depends)
    referral = fields.Function(fields.Many2One('lims.referral', 'Referral'),
        'get_detail_field', searcher='search_detail_field')
    repetition_reason = fields.Char('Repetition reason',
        states={'readonly': True, 'invisible': Eval('repetition', 0) == 0},
        depends=['repetition'])
    exceptional_load = fields.Boolean('Exceptionally loaded result',
        readonly=True)
    exceptional_load_uid = fields.Many2One('res.user',
        'Exceptional loading of results User', readonly=True)
    rule = fields.Many2One('lims.rule', 'Notebook Rule', readonly=True,
        ondelete='SET NULL')
    trace_report = fields.Boolean('Trace report')
    sample_client_description = fields.Function(fields.Char(
        'Product described by the client', translate=True),
        'get_sample_field')

    del _states, _depends

    @classmethod
    def __register__(cls, module_name):
        cursor = Transaction().connection.cursor()
        Service = Pool().get('lims.service')
        table_h = cls.__table_handler__(module_name)

        urgent_exist = table_h.column_exist('urgent')

        migrate_result_modifier = (
            table_h.column_exist('result_modifier') and
            table_h.column_is_type('result_modifier', 'VARCHAR'))
        if migrate_result_modifier:
            table_h.column_rename('result_modifier',
                'temp_result_modifier')
            table_h.column_rename('converted_result_modifier',
                'temp_converted_result_modifier')

        super().__register__(module_name)

        if not urgent_exist:
            cursor.execute('UPDATE "' + cls._table + '" nl '
                'SET urgent = srv.urgent FROM '
                '"' + Service._table + '" srv '
                'WHERE srv.id = nl.service')
        #if migrate_result_modifier:
            #cls._migrate_result_modifier()
            #table_h.drop_column('temp_result_modifier')
            #table_h.drop_column('temp_converted_result_modifier')

    @classmethod
    def _migrate_result_modifier(cls):
        cursor = Transaction().connection.cursor()
        ResultModifier = Pool().get('lims.result_modifier')
        cursor.execute('UPDATE "' + cls._table + '" '
            'SET result_modifier = NULL '
            'WHERE temp_result_modifier = \'eq\'')
        cursor.execute('UPDATE "' + cls._table + '" nl '
            'SET result_modifier = rm.id FROM '
            '"' + ResultModifier._table + '" rm '
            'WHERE rm.code = nl.temp_result_modifier')
        cursor.execute('UPDATE "' + cls._table + '" '
            'SET converted_result_modifier = NULL '
            'WHERE temp_converted_result_modifier = \'eq\'')
        cursor.execute('UPDATE "' + cls._table + '" nl '
            'SET converted_result_modifier = rm.id FROM '
            '"' + ResultModifier._table + '" rm '
            'WHERE rm.code = nl.temp_converted_result_modifier')

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

    @staticmethod
    def default_repetition():
        return 0

    @staticmethod
    def default_decimals():
        return 2

    @staticmethod
    def default_report():
        return True

    @staticmethod
    def default_dilution_factor():
        return 1.0

    @staticmethod
    def default_accepted():
        return False

    @staticmethod
    def default_annulled():
        return False

    @classmethod
    def create(cls, vlist):
        pool = Pool()
        LabMethod = pool.get('lims.lab.method')
        Sample = pool.get('lims.sample')

        vlist = [x.copy() for x in vlist]
        for values in vlist:
            # set method version
            if 'method' in values and values['method'] is not None:
                values['method_version'] = LabMethod(
                    values['method']).get_current_version()

        lines = super().create(vlist)

        cls.update_detail_report(lines)
        sample_ids = list(set(nl.sample.id for nl in lines))
        Sample.update_samples_state(sample_ids)
        return lines

    @classmethod
    def write(cls, *args):
        pool = Pool()
        LabMethod = pool.get('lims.lab.method')
        Sample = pool.get('lims.sample')

        actions = iter(args)
        args = []
        for lines, values in zip(actions, actions):
            # set method version
            if 'method' in values and values['method'] is not None:
                values['method_version'] = LabMethod(
                    values['method']).get_current_version()
            args.extend((lines, values))

        super().write(*args)

        actions = iter(args)
        for lines, vals in zip(actions, actions):
            if vals.get('not_accepted_message'):
                cls.write(lines, {'not_accepted_message': None})
            if 'accepted' in vals:
                cls.update_detail_analysis(lines, vals['accepted'])
            if 'report' in vals:
                cls.update_detail_report(lines)
            update_samples_state = False
            for field in ('start_date', 'end_date', 'acceptance_date',
                    'report', 'annulled'):
                if field in vals:
                    update_samples_state = True
                    break
            if update_samples_state:
                sample_ids = list(set(nl.sample.id for nl in lines))
                Sample.update_samples_state(sample_ids)
            update_referrals_state = False
            for field in ('accepted', 'annulled', 'result', 'literal_result',
                    'result_modifier'):
                if field in vals:
                    update_referrals_state = True
                    break
            if update_referrals_state:
                cls.update_referrals_state(lines)

    @staticmethod
    def update_detail_analysis(lines, accepted):
        EntryDetailAnalysis = Pool().get('lims.entry.detail.analysis')

        details = [nl.analysis_detail.id for nl in lines]
        if accepted:
            analysis_details = EntryDetailAnalysis.search([
                ('id', 'in', details),
                ])
            if analysis_details:
                EntryDetailAnalysis.write(analysis_details, {
                    'state': 'done',
                    })
        else:
            analysis_details = EntryDetailAnalysis.search([
                ('id', 'in', details),
                ('analysis.behavior', '!=', 'internal_relation'),
                ])
            if analysis_details:
                EntryDetailAnalysis.write(analysis_details, {
                    'state': 'planned',
                    })
            analysis_details = EntryDetailAnalysis.search([
                ('id', 'in', details),
                ('analysis.behavior', '=', 'internal_relation'),
                ])
            if analysis_details:
                EntryDetailAnalysis.write(analysis_details, {
                    'state': 'unplanned',
                    })

    @staticmethod
    def update_detail_report(lines):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        NotebookLine = pool.get('lims.notebook.line')

        to_save = []
        details_ids = list(set(nl.analysis_detail.id for nl in lines))
        with Transaction().set_context(_check_access=False):
            analysis_details = EntryDetailAnalysis.browse(details_ids)
        for d in analysis_details:
            cursor.execute('SELECT report '
                'FROM "' + NotebookLine._table + '" '
                'WHERE analysis_detail = %s '
                'ORDER BY id DESC LIMIT 1',
                (d.id,))
            value = cursor.fetchone()
            d.report = value[0] if value else False
            to_save.append(d)
        EntryDetailAnalysis.save(to_save)

    @classmethod
    def update_referrals_state(cls, lines):
        Referral = Pool().get('lims.referral')

        referral_ids = [l.analysis_detail.referral.id
            for l in lines if l.analysis_detail.referral]
        if not referral_ids:
            return

        referrals = Referral.search([
            ('state', '=', 'sent'),
            ('id', 'in', referral_ids),
            ])
        for referral in referrals:
            if cls.search_count([
                    ('analysis_detail.referral', '=', referral.id),
                    ('annulled', '=', False),
                    ('result', 'in', [None, '']),
                    ('literal_result', 'in', [None, '']),
                    ['OR', ('result_modifier', '=', None),
                        ('result_modifier.code', 'not in',
                        ['d', 'nd', 'pos', 'neg', 'ni', 'abs', 'pre', 'na'])],
                    ]) == 0:
                Referral.write([referral], {'state': 'done'})

    @classmethod
    def validate(cls, notebook_lines):
        super().validate(notebook_lines)
        for line in notebook_lines:
            line.check_end_date()
            line.check_accepted()

    def check_end_date(self):
        if self.end_date:
            if not self.start_date or self.end_date < self.start_date:
                raise UserError(gettext('lims.msg_line_end_date',
                    analysis=self.analysis.rec_name))
            if not self.start_date or self.end_date > datetime.now().date():
                raise UserError(gettext('lims.msg_end_date_wrong',
                    analysis=self.analysis.rec_name))

    def check_accepted(self):
        Config = Pool().get('lims.configuration')
        if self.accepted:
            clause = [
                ('notebook', '=', self.notebook.id),
                ('analysis', '=', self.analysis.id),
                ('accepted', '=', True),
                ('id', '!=', self.id),
                ]
            if Config(1).notebook_lines_acceptance_method:
                clause.append(('method', '=', self.method.id))
            accepted_lines = self.search(clause)
            if accepted_lines:
                raise UserError(gettext('lims.msg_accepted',
                    analysis=self.analysis.rec_name))

    @classmethod
    def get_analysis_order(cls, notebook_lines, name):
        result = {}
        for nl in notebook_lines:
            analysis = getattr(nl, 'analysis', None)
            result[nl.id] = analysis.order if analysis else None
        return result

    @staticmethod
    def order_analysis_order(tables):
        Analysis = Pool().get('lims.analysis')
        field = Analysis._fields['order']
        table, _ = tables[None]
        analysis_tables = tables.get('analysis')
        if analysis_tables is None:
            analysis = Analysis.__table__()
            analysis_tables = {
                None: (analysis, analysis.id == table.analysis),
                }
            tables['analysis'] = analysis_tables
        return field.convert_order('order', analysis_tables, Analysis)

    @classmethod
    def get_views_field(cls, notebook_lines, names):
        result = {}
        for name in names:
            field_name = name[:-5]
            result[name] = {}
            for nl in notebook_lines:
                field = getattr(nl, field_name, None)
                result[name][nl.id] = field.id if field else None
        return result

    @classmethod
    def get_detail_field(cls, notebook_lines, names):
        result = {}
        for name in names:
            result[name] = {}
            if cls._fields[name]._type == 'many2one':
                for nl in notebook_lines:
                    field = getattr(nl.analysis_detail, name, None)
                    result[name][nl.id] = field.id if field else None
            else:
                for nl in notebook_lines:
                    result[name][nl.id] = getattr(nl.analysis_detail,
                        name, None)
        return result

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

    @classmethod
    def get_service_field(cls, notebook_lines, names):
        result = {}
        for name in names:
            result[name] = {}
            if name == 'fraction':
                for nl in notebook_lines:
                    field = getattr(nl.service, name, None)
                    result[name][nl.id] = field.id if field else None
            else:
                for nl in notebook_lines:
                    result[name][nl.id] = getattr(nl.service, name, None)
        return result

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

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

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

    @classmethod
    def get_sample_field(cls, notebook_lines, names):
        result = {}
        for name in names:
            result[name] = {}
            for nl in notebook_lines:
                result[name][nl.id] = None
                fraction = getattr(nl.service, 'fraction', None)
                if not fraction:
                    continue
                sample = getattr(fraction, 'sample', None)
                if not sample:
                    continue
                field = getattr(sample, name, None)
                if isinstance(field, ModelSQL):
                    result[name][nl.id] = field.id if field else None
                elif isinstance(field, tuple):
                    result[name][nl.id] = [f.id for f in field]
                else:
                    result[name][nl.id] = field
        return result

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

    def get_rec_name(self, name):
        rec_name = '%s / %s' % (self.notebook.rec_name, self.analysis.rec_name)
        if self.repetition != 0:
            rec_name += ' (%s)' % self.repetition
        return rec_name

    @classmethod
    def search_rec_name(cls, name, clause):
        return [('analysis',) + tuple(clause[1:])]

    @classmethod
    def _get_custom_views(cls):
        # All Notebook Lines view
        return ['notebook_line_all_list']

    @classmethod
    def fields_view_get(cls, view_id=None, view_type='form', level=None):
        pool = Pool()
        User = pool.get('res.user')
        Config = pool.get('lims.configuration')
        UiView = pool.get('ir.ui.view')

        result = super().fields_view_get(view_id, view_type, level)

        if view_id and UiView(view_id).name in cls._get_custom_views():
            return result

        notebook_view = User(Transaction().user).notebook_view
        if not notebook_view:
            notebook_view = Config(1).default_notebook_view
            if not notebook_view:
                return result

        if view_type == 'tree':
            xml = '<?xml version="1.0"?>\n' \
                '<tree editable="1">\n'
            fields = set()
            for column in notebook_view.columns:
                fields.add(column.field.name)
                attrs = []
                if column.field.name == 'analysis':
                    attrs.append('icon="icon"')
                if column.field.name in ('acceptance_date', 'annulment_date'):
                    attrs.append('widget="date"')
                xml += ('<field name="%s" %s/>\n'
                    % (column.field.name, ' '.join(attrs)))
                for depend in getattr(cls, column.field.name).depends:
                    fields.add(depend)
            for field in ('report_date', 'result', 'converted_result',
                    'result_modifier', 'converted_result_modifier',
                    'literal_result', 'backup', 'verification', 'uncertainty',
                    'accepted', 'acceptance_date', 'end_date', 'report',
                    'annulled', 'annulment_date', 'icon'):
                fields.add(field)
            xml += '</tree>'
            result['arch'] = xml
            result['fields'] = cls.fields_get(fields_names=list(fields))
        return result

    @fields.depends('result', 'converted_result', 'converted_result_modifier',
        'backup', 'verification', 'uncertainty', 'end_date', 'start_date',
        'analysis')
    def on_change_result(self):
        self.converted_result = None
        self.converted_result_modifier = None
        self.backup = None
        self.verification = None
        self.uncertainty = None
        self.end_date = None
        if self.analysis and self.analysis.behavior == 'internal_relation':
            self.start_date = None

    @fields.depends('accepted', 'report', 'annulled', 'result',
        'converted_result', 'literal_result', 'result_modifier',
        'converted_result_modifier', 'end_date', 'acceptance_date')
    def on_change_accepted(self):
        self.not_accepted_message = ''
        if self.accepted:
            if not self.report:
                self.accepted = False
                self.not_accepted_message = gettext('lims.msg_not_accepted_1')
            elif self.annulled:
                self.accepted = False
                self.not_accepted_message = gettext('lims.msg_not_accepted_2')
            elif not self.end_date:
                self.accepted = False
                self.not_accepted_message = gettext('lims.msg_not_accepted_3')
            elif not (self.result or self.converted_result or
                    self.literal_result or
                    (self.result_modifier and
                        self.result_modifier.code in
                        ('d', 'nd', 'pos', 'neg', 'ni', 'abs', 'pre')) or
                    (self.converted_result_modifier and
                        self.converted_result_modifier.code in
                        ('d', 'nd', 'pos', 'neg', 'ni', 'abs', 'pre'))):
                self.accepted = False
                self.not_accepted_message = gettext('lims.msg_not_accepted_4')
            else:
                if (self.converted_result and (
                        self.converted_result_modifier and
                        self.converted_result_modifier.code not in
                        ('ni', 'low'))):
                    self.accepted = False
                    self.not_accepted_message = gettext(
                        'lims.msg_not_accepted_5')
                elif (self.result and (
                        self.result_modifier and
                        self.result_modifier.code not in
                        ('ni', 'low'))):
                    self.accepted = False
                    self.not_accepted_message = gettext(
                        'lims.msg_not_accepted_6')
                elif (self.result_modifier and
                        self.result_modifier.code == 'ni' and
                        not self.literal_result and
                        (not self.converted_result_modifier or
                            not self.converted_result) and
                        (self.converted_result_modifier and
                            self.converted_result_modifier.code not in
                            ('nd', 'pos', 'neg'))):
                    self.accepted = False
                    self.not_accepted_message = gettext(
                        'lims.msg_not_accepted_7')
                else:
                    self.acceptance_date = datetime.now()
        else:
            ResultsLine = Pool().get('lims.results_report.version.detail.line')
            report_lines = ResultsLine.search([
                ('notebook_line', '=', self.id),
                ('detail_sample.version_detail.state', '!=', 'annulled'),
                ])
            if report_lines:
                self.accepted = True
                report_detail = report_lines[0].detail_sample.version_detail
                self.not_accepted_message = gettext('lims.msg_accepted_1',
                    report=report_detail.report_version.results_report.number)
            else:
                self.acceptance_date = None

    @fields.depends('result_modifier', 'annulled', 'annulment_date', 'report')
    def on_change_result_modifier(self):
        if (self.result_modifier and self.result_modifier.code == 'na'
                and not self.annulled):
            self.annulled = True
            self.annulment_date = datetime.now()
            self.report = False
        elif ((not self.result_modifier or self.result_modifier.code != 'na')
                and self.annulled):
            self.annulled = False
            self.annulment_date = None
            self.report = True

    @classmethod
    def get_typification_field(cls, notebook_lines, names):
        pool = Pool()
        Typification = pool.get('lims.typification')

        result = dict((name, {}) for name in names)
        for nl in notebook_lines:
            typification = Typification.get_valid_typification(
                nl.notebook.product_type.id, nl.notebook.matrix.id,
                nl.analysis.id, nl.method.id)
            for name in names:
                if typification:
                    result[name][nl.id] = getattr(typification, name, None)
                else:
                    if name == 'report_type':
                        result[name][nl.id] = 'normal'
                    elif name == 'report_result_type':
                        result[name][nl.id] = 'result'
                    elif name == 'check_result_limits':
                        result[name][nl.id] = False
                    else:
                        result[name][nl.id] = None
        return result

    @classmethod
    def search_typification_field(cls, name, clause):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        Notebook = pool.get('lims.notebook')
        Fraction = pool.get('lims.fraction')
        Sample = pool.get('lims.sample')
        Typification = pool.get('lims.typification')

        operator_ = clause[1:2][0]
        cursor.execute('SELECT nl.id '
            'FROM "' + cls._table + '" nl '
                'INNER JOIN "' + Notebook._table + '" n '
                'ON nl.notebook = n.id '
                'INNER JOIN "' + Fraction._table + '" f '
                'ON n.fraction = f.id '
                'INNER JOIN "' + Sample._table + '" s '
                'ON f.sample = s.id '
                'INNER JOIN "' + Typification._table + '" t '
                'ON (nl.analysis = t.analysis AND nl.method = t.method '
                'AND s.product_type = t.product_type AND t.matrix = t.matrix) '
            'WHERE t.valid = TRUE '
                'AND t.' + name + ' ' + operator_ + ' %s',
            clause[2:3])
        return [('id', 'in', [x[0] for x in cursor.fetchall()])]

    @fields.depends('method', 'party',
        '_parent_method.results_estimated_waiting')
    def on_change_with_results_estimated_waiting(self, name=None):
        LabMethodWaitingTime = Pool().get('lims.lab.method.results_waiting')
        if self.method:
            waiting_times = LabMethodWaitingTime.search([
                ('method', '=', self.method.id),
                ('party', '=', self.party.id),
                ])
            if waiting_times:
                return waiting_times[0].results_estimated_waiting
            return self.method.results_estimated_waiting
        return None

    @classmethod
    def get_results_estimated_date(cls, notebook_lines, name):
        result = {}
        for nl in notebook_lines:
            result[nl.id] = None
            detail = getattr(nl, 'analysis_detail', None)
            if not detail:
                continue
            confirmation_date = getattr(detail, 'confirmation_date', None)
            if not confirmation_date:
                continue
            estimated_waiting = getattr(nl, 'results_estimated_waiting', None)
            if not estimated_waiting:
                continue
            result[nl.id] = cls._get_results_estimated_date(confirmation_date,
                estimated_waiting)
        return result

    @staticmethod
    def _get_results_estimated_date(confirmation_date, estimated_waiting):
        date = (confirmation_date +
            relativedelta(days=estimated_waiting))
        return date

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

    @fields.depends('analysis', 'laboratory')
    def on_change_with_device_domain(self, name=None):
        cursor = Transaction().connection.cursor()
        AnalysisDevice = Pool().get('lims.analysis.device')

        if not self.analysis or not self.laboratory:
            return []

        cursor.execute('SELECT DISTINCT(device) '
            'FROM "' + AnalysisDevice._table + '" '
            'WHERE active IS TRUE '
                'AND analysis = %s  '
                'AND laboratory = %s',
            (self.analysis.id, self.laboratory.id))
        res = cursor.fetchall()
        if not res:
            return []
        return [x[0] for x in res]

    @fields.depends('analysis', 'laboratory')
    def on_change_with_department_domain(self, name=None):
        cursor = Transaction().connection.cursor()
        AnalysisLaboratory = Pool().get('lims.analysis-laboratory')

        if not self.analysis or not self.laboratory:
            return []

        cursor.execute('SELECT DISTINCT(department) '
            'FROM "' + AnalysisLaboratory._table + '" '
            'WHERE analysis = %s  '
                'AND laboratory = %s',
            (self.analysis.id, self.laboratory.id))
        res = cursor.fetchall()
        if not res:
            return []
        return [x[0] for x in res]

    def get_icon(self, name):
        if self.report_date:
            return 'lims-red'
        return 'lims-white'

    def get_planning_comments(self, name=None):
        if self.planification:
            return self.planification.comments
        return ''

    def get_formated_result(self, name=None):
        res = ''
        result_modifier = self.result_modifier and self.result_modifier.code
        if self.literal_result:
            res = self.literal_result
        else:
            res = self._format_result(self.result,
                self.decimals, self.significant_digits,
                self.scientific_notation)
            if not result_modifier:
                res = res
            elif result_modifier == 'low':
                res = gettext('lims.msg_quantification_limit', loq=res)
            elif result_modifier == 'd':
                res = gettext('lims.msg_d')
            elif result_modifier == 'nd':
                res = gettext('lims.msg_nd')
            elif result_modifier == 'ni':
                res = ''
            elif result_modifier == 'pos':
                res = gettext('lims.msg_pos')
            elif result_modifier == 'neg':
                res = gettext('lims.msg_neg')
            elif result_modifier == 'pre':
                res = gettext('lims.msg_pre')
            elif result_modifier == 'abs':
                res = gettext('lims.msg_abs')
            else:
                res = self.result_modifier.name
        return res

    def get_formated_converted_result(self, name=None):
        res = ''
        result_modifier = (self.converted_result_modifier and
            self.converted_result_modifier.code)
        if not self.literal_result:
            res = self._format_result(self.converted_result,
                self.decimals, self.significant_digits,
                self.scientific_notation)
            if not result_modifier:
                res = res
            elif result_modifier == 'low':
                res = gettext('lims.msg_quantification_limit', loq=res)
            elif result_modifier == 'd':
                res = gettext('lims.msg_d')
            elif result_modifier == 'nd':
                res = gettext('lims.msg_nd')
            elif result_modifier == 'ni':
                res = ''
            elif result_modifier == 'pos':
                res = gettext('lims.msg_pos')
            elif result_modifier == 'neg':
                res = gettext('lims.msg_neg')
            elif result_modifier == 'pre':
                res = gettext('lims.msg_pre')
            elif result_modifier == 'abs':
                res = gettext('lims.msg_abs')
            else:
                res = self.converted_result_modifier.name
        return res

    def _format_result(self, result, decimals, significant_digits=None,
            scientific_notation=False):
        res = ''
        if not result:
            return res
        try:
            if significant_digits:
                res = ("{0:.%ie}" % (significant_digits - 1)).format(
                    float(result))
                if not scientific_notation:
                    res = str(float(res))
                    if float(res) < 1 and len(res) < (significant_digits + 2):
                        res = res.ljust(significant_digits + 2, '0')
            else:
                res = round(float(result), decimals)
                res = format(res, '.{}f'.format(decimals))
        except (TypeError, ValueError):
            pass
        return res


class NotebookLineAllFields(ModelSQL, ModelView):
    'Laboratory Notebook Line'
    __name__ = 'lims.notebook.line.all_fields'

    line = fields.Many2One('lims.notebook.line', 'Notebook Line')
    fraction = fields.Many2One('lims.fraction', 'Fraction', readonly=True)
    fraction_type = fields.Many2One('lims.fraction.type', 'Fraction type',
        readonly=True)
    party = fields.Many2One('party.party', 'Party', readonly=True)
    party_code = fields.Char('Party', readonly=True)
    product_type = fields.Many2One('lims.product.type', 'Product type',
        readonly=True)
    matrix = fields.Many2One('lims.matrix', 'Matrix', readonly=True)
    label = fields.Char('Label', readonly=True)
    date = fields.DateTime('Date', readonly=True)
    analysis = fields.Many2One('lims.analysis', 'Analysis', readonly=True)
    repetition = fields.Integer('Repetition', readonly=True)
    start_date = fields.Date('Start date', readonly=True)
    end_date = fields.Date('End date', readonly=True)
    laboratory = fields.Many2One('lims.laboratory', 'Laboratory',
        readonly=True)
    method = fields.Many2One('lims.lab.method', 'Method', readonly=True)
    device = fields.Many2One('lims.lab.device', 'Device', readonly=True)
    service = fields.Many2One('lims.service', 'Service', readonly=True)
    analysis_origin = fields.Char('Analysis origin', readonly=True)
    urgent = fields.Boolean('Urgent', readonly=True)
    priority = fields.Integer('Priority', readonly=True)
    initial_concentration = fields.Char('Initial concentration', readonly=True)
    final_concentration = fields.Char('Final concentration', readonly=True)
    literal_final_concentration = fields.Char('Literal Final concentration',
        readonly=True)
    laboratory_professionals = fields.Function(fields.Many2Many(
        'lims.laboratory.professional', None, None,
        'Preparation professionals'), 'get_line_field',
        searcher='search_line_field')
    initial_unit = fields.Many2One('product.uom', 'Initial unit',
        readonly=True)
    final_unit = fields.Many2One('product.uom', 'Final unit', readonly=True)
    result_modifier = fields.Many2One('lims.result_modifier',
        'Result modifier', readonly=True)
    converted_result_modifier = fields.Many2One('lims.result_modifier',
        'Converted result modifier', readonly=True)
    result = fields.Char('Result', readonly=True)
    converted_result = fields.Char('Converted result', readonly=True)
    formated_result = fields.Function(fields.Char('Result to report'),
        'get_line_field')
    formated_converted_result = fields.Function(fields.Char(
        'Converted result to report'), 'get_line_field')
    detection_limit = fields.Char('Detection limit', readonly=True)
    quantification_limit = fields.Char('Quantification limit', readonly=True)
    lower_limit = fields.Char('Lower limit allowed', readonly=True)
    upper_limit = fields.Char('Upper limit allowed', readonly=True)
    chromatogram = fields.Char('Chromatogram', readonly=True)
    professionals = fields.Function(fields.One2Many(
        'lims.notebook.line.professional', None,
        'Analytic professionals'), 'get_line_field',
        searcher='search_line_field')
    comments = fields.Text('Entry comments', readonly=True)
    theoretical_concentration = fields.Char('Theoretical concentration',
        readonly=True)
    concentration_level = fields.Many2One('lims.concentration.level',
        'Concentration level', readonly=True)
    decimals = fields.Integer('Decimals', readonly=True)
    significant_digits = fields.Integer('Significant digits', readonly=True)
    scientific_notation = fields.Boolean('Scientific notation', readonly=True)
    backup = fields.Char('Backup', readonly=True)
    reference = fields.Char('Reference', readonly=True)
    literal_result = fields.Char('Literal result', readonly=True)
    rm_correction_formula = fields.Char('RM Correction Formula', readonly=True)
    report = fields.Boolean('Report', readonly=True)
    uncertainty = fields.Char('Uncertainty', readonly=True)
    verification = fields.Char('Verification', readonly=True)
    dilution_factor = fields.Float('Dilution factor', readonly=True)
    accepted = fields.Boolean('Accepted', readonly=True)
    acceptance_date = fields.DateTime('Acceptance date', readonly=True)
    annulled = fields.Boolean('Annulled', readonly=True)
    annulment_date = fields.DateTime('Annulment date', readonly=True)
    annulment_reason = fields.Text('Annulment reason', readonly=True,
        states={'invisible': ~Eval('annulled')},
        depends=['annulled'])
    results_report = fields.Many2One('lims.results_report', 'Results Report',
        readonly=True)
    planification = fields.Many2One('lims.planification', 'Planification',
        readonly=True)
    confirmation_date = fields.Date('Confirmation date', readonly=True)
    results_estimated_waiting = fields.Integer(
        'Estimated number of days for results', readonly=True)
    results_estimated_date = fields.Function(fields.Date(
        'Estimated date of result'), 'get_line_field')
    laboratory_date = fields.Date('Laboratory deadline', readonly=True)
    report_date = fields.Date('Date agreed for result', readonly=True)
    department = fields.Many2One('company.department', 'Department',
        readonly=True)
    referral = fields.Function(fields.Many2One('lims.referral', 'Referral'),
        'get_line_field', searcher='search_line_field')
    repetition_reason = fields.Char('Repetition reason', readonly=True,
        states={'invisible': Eval('repetition', 0) == 0},
        depends=['repetition'])
    exceptional_load = fields.Boolean('Exceptionally loaded result',
        readonly=True)
    exceptional_load_uid = fields.Many2One('res.user',
        'Exceptional loading of results User', readonly=True)
    trace_report = fields.Boolean('Trace report', readonly=True)
    sample_client_description = fields.Function(fields.Char(
        'Product described by the client', translate=True),
        'get_sample_field')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('fraction', 'DESC'))
        cls._order.insert(1, ('analysis', 'ASC'))
        cls._order.insert(2, ('repetition', 'ASC'))

    @staticmethod
    def table_query():
        pool = Pool()
        line = pool.get('lims.notebook.line').__table__()
        detail = pool.get('lims.entry.detail.analysis').__table__()
        service = pool.get('lims.service').__table__()
        fraction = pool.get('lims.fraction').__table__()
        sample = pool.get('lims.sample').__table__()
        entry = pool.get('lims.entry').__table__()
        party = pool.get('party.party').__table__()

        join1 = Join(line, service)
        join1.condition = join1.right.id == line.service
        join2 = Join(join1, fraction)
        join2.condition = join2.right.id == join1.right.fraction
        join3 = Join(join2, sample)
        join3.condition = join3.right.id == join2.right.sample
        join4 = Join(join3, entry)
        join4.condition = join4.right.id == join3.right.entry
        join5 = Join(join4, party)
        join5.condition = join5.right.id == join4.right.party
        join6 = Join(join5, detail)
        join6.condition = join6.right.id == join1.left.analysis_detail

        columns = [
            line.id,
            line.create_uid,
            line.create_date,
            line.write_uid,
            line.write_date,
            line.id.as_('line'),
            service.fraction,
            sample.party,
            party.code.as_('party_code'),
            sample.product_type,
            sample.matrix,
            sample.label,
            fraction.type.as_('fraction_type'),
            sample.date,
            line.analysis,
            line.repetition,
            line.start_date,
            line.end_date,
            line.laboratory,
            line.method,
            line.device,
            line.service,
            line.analysis_origin,
            line.urgent,
            service.priority,
            line.initial_concentration,
            line.final_concentration,
            line.literal_final_concentration,
            line.initial_unit,
            line.final_unit,
            line.result_modifier,
            line.converted_result_modifier,
            line.result,
            line.converted_result,
            line.detection_limit,
            line.quantification_limit,
            line.lower_limit,
            line.upper_limit,
            line.dilution_factor,
            line.chromatogram,
            line.comments,
            line.theoretical_concentration,
            line.concentration_level,
            line.decimals,
            line.significant_digits,
            line.scientific_notation,
            line.backup,
            line.reference,
            line.literal_result,
            line.rm_correction_formula,
            line.report,
            line.uncertainty,
            line.verification,
            line.accepted,
            line.acceptance_date,
            line.annulled,
            line.annulment_date,
            line.annulment_reason,
            line.results_report,
            line.planification,
            detail.confirmation_date,
            line.results_estimated_waiting,
            service.laboratory_date,
            service.report_date,
            line.department,
            line.repetition_reason,
            line.exceptional_load,
            line.exceptional_load_uid,
            line.trace_report,
            ]
        where = Literal(True)
        return join6.select(*columns, where=where)

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

    @classmethod
    def get_line_field(cls, notebook_lines, names):
        result = dict((name, {}) for name in names)
        for nl in notebook_lines:
            for name in names:
                field = getattr(nl.line, name, None)
                if isinstance(field, ModelSQL):
                    result[name][nl.id] = field.id if field else None
                elif isinstance(field, tuple):
                    result[name][nl.id] = [f.id for f in field]
                else:
                    result[name][nl.id] = field
        return result

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

    @classmethod
    def get_sample_field(cls, notebook_lines, names):
        result = dict((name, {}) for name in names)
        for nl in notebook_lines:
            for name in names:
                result[name][nl.id] = None
                fraction = getattr(nl.line.service, 'fraction', None)
                if not fraction:
                    continue
                sample = getattr(fraction, 'sample', None)
                if not sample:
                    continue
                field = getattr(sample, name, None)
                if isinstance(field, ModelSQL):
                    result[name][nl.id] = field.id if field else None
                elif isinstance(field, tuple):
                    result[name][nl.id] = [f.id for f in field]
                else:
                    result[name][nl.id] = field
        return result


class NotebookLineLaboratoryProfessional(ModelSQL):
    'Laboratory Notebook Line - Laboratory Professional'
    __name__ = 'lims.notebook.line-laboratory.professional'

    notebook_line = fields.Many2One('lims.notebook.line', 'Notebook Line',
        ondelete='CASCADE', select=True, required=True)
    professional = fields.Many2One('lims.laboratory.professional',
        'Laboratory professional', ondelete='CASCADE', select=True,
        required=True)


class NotebookLineProfessional(ModelSQL, ModelView):
    'Laboratory Notebook Line Professional'
    __name__ = 'lims.notebook.line.professional'

    notebook_line = fields.Many2One('lims.notebook.line', 'Notebook Line',
        ondelete='CASCADE', select=True, required=True)
    professional = fields.Many2One('lims.laboratory.professional',
        'Laboratory professional', required=True)


class NotebookInitialConcentrationCalcStart(ModelView):
    'Initial Concentration Calculation'
    __name__ = 'lims.notebook.initial_concentration_calc.start'


class NotebookInitialConcentrationCalc(Wizard):
    'Initial Concentration Calculation'
    __name__ = 'lims.notebook.initial_concentration_calc'

    start_state = 'ok'
    start = StateView('lims.notebook.initial_concentration_calc.start',
        'lims.lims_notebook_initial_concentration_calc_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Ok', 'ok', 'tryton-ok', default=True),
            ])
    ok = StateTransition()

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        for notebook_id in Transaction().context['active_ids']:
            with Transaction().set_context(_check_access=True):
                notebook_lines = NotebookLine.search([
                    ('notebook', '=', notebook_id),
                    ])
            if not notebook_lines:
                continue
            self.lines_initial_concentration_calc(notebook_lines)
        return 'end'

    def lines_initial_concentration_calc(self, notebook_lines):
        NotebookLine = Pool().get('lims.notebook.line')

        lines_to_save = []
        for notebook_line in notebook_lines:
            if notebook_line.accepted:
                continue
            ic = notebook_line.initial_concentration
            if not ic:
                continue
            if ic[0] == 'A':
                analysis_code = ic[1:]
                result = self._get_analysis_result(analysis_code,
                    notebook_line.notebook)
                if result is not None:
                    notebook_line.initial_concentration = str(result)
                    lines_to_save.append(notebook_line)
            elif ic[0] == 'R':
                analysis_code = ic[1:]
                result = self._get_relation_result(analysis_code,
                    notebook_line.notebook)
                if result is not None:
                    notebook_line.initial_concentration = str(result)
                    lines_to_save.append(notebook_line)
            else:
                continue
        if lines_to_save:
            NotebookLine.save(lines_to_save)

    def _get_analysis_result(self, analysis_code, notebook, round_=False):
        NotebookLine = Pool().get('lims.notebook.line')

        with Transaction().set_user(0):
            notebook_lines = NotebookLine.search([
                ('notebook', '=', notebook.id),
                ('analysis.code', '=', analysis_code),
                ('annulment_date', '=', None),
                ])
        if not notebook_lines:
            return None

        try:
            res = float(notebook_lines[0].result)
        except (TypeError, ValueError):
            return None
        if not round_:
            return res
        return round(res, notebook_lines[0].decimals)

    def _get_relation_result(self, analysis_code, notebook, round_=False):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')

        internal_relations = Analysis.search([
            ('code', '=', analysis_code),
            ])
        if not internal_relations:
            return None
        formula = internal_relations[0].result_formula
        if not formula:
            return None
        for i in (' ', '\t', '\n', '\r'):
            formula = formula.replace(i, '')
        variables = self._get_variables(formula, notebook)
        if not variables:
            return None

        parser = FormulaParser(formula, variables)
        value = parser.getValue()

        if int(value) == value:
            res = int(value)
        else:
            epsilon = 0.0000000001
            if int(value + epsilon) != int(value):
                res = int(value + epsilon)
            elif int(value - epsilon) != int(value):
                res = int(value)
            else:
                res = float(value)
        if not round_:
            return res

        with Transaction().set_user(0):
            notebook_lines = NotebookLine.search([
                ('notebook', '=', notebook.id),
                ('analysis.code', '=', analysis_code),
                ('repetition', '=', 0),
                ('annulment_date', '=', None),
                ])
        if not notebook_lines:
            return None
        return round(res, notebook_lines[0].decimals)

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

        variables = {}
        for variable in re.findall(r'\{.*?\}', formula):
            var = variable.replace('{', '').replace('}', '')
            for prefix in ('A', 'D', 'T', 'Y', 'R'):
                if var.startswith(prefix):
                    variables[var] = None

        for var in variables.keys():
            if var[0] == 'A':
                analysis_code = var[1:]
                result = self._get_analysis_result(analysis_code, notebook,
                    round_=True)
                if result is not None:
                    variables[var] = result
            elif var[0] == 'D':
                analysis_code = var[1:]
                result = self._get_analysis_result(analysis_code, notebook,
                    round_=True)
                if result is not None:
                    result = VolumeConversion.brixToDensity(result)
                    if result is not None:
                        variables[var] = result
            elif var[0] == 'T':
                analysis_code = var[1:]
                result = self._get_analysis_result(analysis_code, notebook,
                    round_=True)
                if result is not None:
                    result = VolumeConversion.brixToSolubleSolids(result)
                    if result is not None:
                        variables[var] = result
            elif var[0] == 'R':
                analysis_code = var[1:]
                result = self._get_relation_result(analysis_code, notebook,
                    round_=True)
                if result is not None:
                    result = VolumeConversion.brixToSolubleSolids(result)
                    if result is not None:
                        variables[var] = result
            elif var[0] == 'Y':
                analysis_code = var[1:]
                result = self._get_relation_result(analysis_code, notebook,
                    round_=True)
                if result is not None:
                    result = VolumeConversion.brixToDensity(result)
                    if result is not None:
                        variables[var] = result
        for var in variables.values():
            if var is None:
                return None
        return variables

    def end(self):
        return 'reload'


class NotebookLineInitialConcentrationCalc(NotebookInitialConcentrationCalc):
    'Initial Concentration Calculation'
    __name__ = 'lims.notebook_line.initial_concentration_calc'

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        notebook_lines = NotebookLine.browse(
            Transaction().context['active_ids'])
        if not notebook_lines:
            return 'end'

        self.lines_initial_concentration_calc(notebook_lines)
        return 'end'


class NotebookInitialConcentrationCalc2Start(ModelView):
    'Initial Concentration Calculation'
    __name__ = 'lims.notebook.initial_concentration_calc_2.start'

    variables = fields.One2Many(
        'lims.notebook.initial_concentration_calc_2.variable', None,
        'Variables')


class NotebookInitialConcentrationCalc2Variable(ModelView):
    'Formula Variable'
    __name__ = 'lims.notebook.initial_concentration_calc_2.variable'

    line = fields.Many2One('lims.notebook.line', 'Line')
    analysis = fields.Many2One('lims.analysis', 'Analysis', readonly=True)
    repetition = fields.Integer('Repetition', readonly=True)
    result_modifier = fields.Many2One('lims.result_modifier',
        'Result modifier', readonly=True)
    result = fields.Char('Result', readonly=True)
    initial_unit = fields.Many2One('product.uom', 'Initial unit',
        readonly=True)
    use = fields.Boolean('Use')


class NotebookInitialConcentrationCalc2(Wizard):
    'Initial Concentration Calculation'
    __name__ = 'lims.notebook.initial_concentration_calc_2'

    start = StateView('lims.notebook.initial_concentration_calc_2.start',
        'lims.lims_notebook_initial_concentration_calc_2_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Confirm', 'confirm', 'tryton-ok', default=True),
            ])
    confirm = StateTransition()

    def _get_notebook_lines(self):
        NotebookLine = Pool().get('lims.notebook.line')
        notebook_id = Transaction().context['active_id']
        with Transaction().set_context(_check_access=True):
            notebook_lines = NotebookLine.search([
                ('notebook', '=', notebook_id),
                ])
            return notebook_lines

    def default_start(self, fields):
        notebook_lines = self._get_notebook_lines()
        if not notebook_lines:
            return {}

        notebook, formula = None, ''
        for notebook_line in notebook_lines:
            if notebook_line.accepted:
                continue
            analysis_code = notebook_line.analysis.code
            if not analysis_code or not notebook_line.initial_concentration:
                continue
            ic = notebook_line.initial_concentration
            if ic[0] != 'A' and ic[0] != 'R':
                continue
            formula += '{%s}' % ic
            if not notebook:
                notebook = notebook_line.notebook

        if not formula:
            return {}

        variables = self._get_variables_list(formula, notebook, {})
        if not variables:
            return {}
        return {'variables': variables}

    def _get_variables_list(self, formula, notebook, analysis={}):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')

        variables = {}
        for variable in re.findall(r'\{.*?\}', formula):
            var = variable.replace('{', '').replace('}', '')
            for prefix in ('A', 'D', 'T', 'Y', 'R'):
                if var.startswith(prefix):
                    variables[var] = None

        for var in variables.keys():
            if var[0] in ('A', 'D', 'T'):
                analysis_code = var[1:]
                with Transaction().set_user(0):
                    notebook_lines = NotebookLine.search([
                        ('notebook', '=', notebook.id),
                        ('analysis.code', '=', analysis_code),
                        ('annulment_date', '=', None),
                        ])
                if not notebook_lines:
                    continue
                for nl in notebook_lines:
                    analysis[nl.id] = {
                        'line': nl.id,
                        'analysis': nl.analysis.id,
                        'repetition': nl.repetition,
                        'use': True if nl.repetition == 0 else False,
                        'result_modifier': (nl.result_modifier and
                            nl.result_modifier.id),
                        'result': nl.result,
                        'initial_unit': (nl.initial_unit and
                            nl.initial_unit.id),
                        }

            elif var[0] in ('R', 'Y'):
                analysis_code = var[1:]
                internal_relations = Analysis.search([
                    ('code', '=', analysis_code),
                    ])
                if not internal_relations:
                    continue
                more_formulas = internal_relations[0].result_formula
                for i in (' ', '\t', '\n', '\r'):
                    more_formulas = more_formulas.replace(i, '')
                self._get_variables_list(more_formulas, notebook, analysis)

        return [v for v in analysis.values()]

    def transition_confirm(self):
        NotebookLine = Pool().get('lims.notebook.line')

        # check_variables
        variables = {}
        for var in self.start.variables:
            analysis_code = var.analysis.code
            if analysis_code not in variables:
                variables[analysis_code] = False
            if var.use:
                if variables[analysis_code]:
                    variables[analysis_code] = False
                else:
                    variables[analysis_code] = True
        for var in variables.values():
            if var is False:
                return 'start'

        var_values = {}
        for var in self.start.variables:
            if not var.use:
                continue
            try:
                res = float(var.line.result)
            except (TypeError, ValueError):
                var_values[var.analysis.code] = None
            else:
                var_values[var.analysis.code] = res

        notebook_lines_to_save = []
        notebook_lines = self._get_notebook_lines()
        for notebook_line in notebook_lines:
            ic = notebook_line.initial_concentration
            if not ic:
                continue
            analysis_code = ic[1:]
            if ic[0] == 'A':
                result = self._get_analysis_result(analysis_code, var_values)
                if result is not None:
                    notebook_line.initial_concentration = str(result)
                    notebook_lines_to_save.append(notebook_line)
            elif ic[0] == 'R':
                result = self._get_relation_result(analysis_code, var_values)
                if result is not None:
                    notebook_line.initial_concentration = str(result)
                    notebook_lines_to_save.append(notebook_line)
            else:
                continue
        if notebook_lines_to_save:
            NotebookLine.save(notebook_lines_to_save)
        return 'end'

    def _get_analysis_result(self, analysis_code, var_values):
        res = var_values[analysis_code]
        return res

    def _get_relation_result(self, analysis_code, var_values):
        pool = Pool()
        Analysis = pool.get('lims.analysis')

        internal_relations = Analysis.search([
            ('code', '=', analysis_code),
            ])
        if not internal_relations:
            return None
        formula = internal_relations[0].result_formula
        if not formula:
            return None
        for i in (' ', '\t', '\n', '\r'):
            formula = formula.replace(i, '')
        variables = self._get_variables(formula, var_values)
        if not variables:
            return None

        parser = FormulaParser(formula, variables)
        value = parser.getValue()

        if int(value) == value:
            res = int(value)
        else:
            epsilon = 0.0000000001
            if int(value + epsilon) != int(value):
                res = int(value + epsilon)
            elif int(value - epsilon) != int(value):
                res = int(value)
            else:
                res = float(value)

        return res

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

        variables = {}
        for variable in re.findall(r'\{.*?\}', formula):
            var = variable.replace('{', '').replace('}', '')
            for prefix in ('A', 'D', 'T', 'Y', 'R'):
                if var.startswith(prefix):
                    variables[var] = None

        for var in variables.keys():
            if var[0] == 'A':
                analysis_code = var[1:]
                result = self._get_analysis_result(analysis_code, var_values)
                if result is not None:
                    variables[var] = result
            elif var[0] == 'D':
                analysis_code = var[1:]
                result = self._get_analysis_result(analysis_code, var_values)
                if result is not None:
                    result = VolumeConversion.brixToDensity(result)
                    if result is not None:
                        variables[var] = result
            elif var[0] == 'T':
                analysis_code = var[1:]
                result = self._get_analysis_result(analysis_code, var_values)
                if result is not None:
                    result = VolumeConversion.brixToSolubleSolids(result)
                    if result is not None:
                        variables[var] = result
            elif var[0] == 'R':
                analysis_code = var[1:]
                result = self._get_relation_result(analysis_code, var_values)
                if result is not None:
                    result = VolumeConversion.brixToSolubleSolids(result)
                    if result is not None:
                        variables[var] = result
            elif var[0] == 'Y':
                analysis_code = var[1:]
                result = self._get_relation_result(analysis_code, var_values)
                if result is not None:
                    result = VolumeConversion.brixToDensity(result)
                    if result is not None:
                        variables[var] = result
        for var in variables.values():
            if var is None:
                return None
        return variables

    def end(self):
        return 'reload'


class NotebookLineInitialConcentrationCalc2(NotebookInitialConcentrationCalc2):
    'Initial Concentration Calculation'
    __name__ = 'lims.notebook_line.initial_concentration_calc_2'

    def _get_notebook_lines(self):
        NotebookLine = Pool().get('lims.notebook.line')
        notebook_lines = NotebookLine.browse(
            Transaction().context['active_ids'])
        return notebook_lines


class NotebookResultsConversionStart(ModelView):
    'Results Conversion'
    __name__ = 'lims.notebook.results_conversion.start'


class NotebookResultsConversion(Wizard):
    'Results Conversion'
    __name__ = 'lims.notebook.results_conversion'

    start_state = 'ok'
    start = StateView('lims.notebook.results_conversion.start',
        'lims.lims_notebook_results_conversion_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Ok', 'ok', 'tryton-ok', default=True),
            ])
    ok = StateTransition()

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        for notebook_id in Transaction().context['active_ids']:
            with Transaction().set_context(_check_access=True):
                notebook_lines = NotebookLine.search([
                    ('notebook', '=', notebook_id),
                    ])
            if not notebook_lines:
                continue
            self.lines_results_conversion(notebook_lines)
        return 'end'

    def lines_results_conversion(self, notebook_lines):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        UomConversion = pool.get('lims.uom.conversion')
        VolumeConversion = pool.get('lims.volume.conversion')

        lines_to_save = []
        for notebook_line in notebook_lines:
            if notebook_line.accepted:
                continue
            if (notebook_line.converted_result or not notebook_line.result or
                    notebook_line.result_modifier):
                continue
            iu = notebook_line.initial_unit
            if not iu:
                continue
            fu = notebook_line.final_unit
            if not fu:
                continue
            try:
                ic = float(notebook_line.initial_concentration)
            except (TypeError, ValueError):
                continue
            try:
                fc = float(notebook_line.final_concentration)
            except (TypeError, ValueError):
                continue
            try:
                result = float(notebook_line.result)
            except (TypeError, ValueError):
                continue

            if (iu == fu and ic == fc):
                converted_result = result
                notebook_line.converted_result = str(converted_result)
                notebook_line.converted_result_modifier = None
                lines_to_save.append(notebook_line)
            elif (iu != fu and ic == fc):
                formula = UomConversion.get_conversion_formula(iu, fu)
                if not formula:
                    continue
                variables = self._get_variables(formula, notebook_line)
                parser = FormulaParser(formula, variables)
                formula_result = parser.getValue()

                converted_result = result * formula_result
                notebook_line.converted_result = str(converted_result)
                notebook_line.converted_result_modifier = None
                lines_to_save.append(notebook_line)
            elif (iu == fu and ic != fc):
                converted_result = result * (fc / ic)
                notebook_line.converted_result = str(converted_result)
                notebook_line.converted_result_modifier = None
                lines_to_save.append(notebook_line)
            else:
                formula = None
                conversions = UomConversion.search([
                    ('initial_uom', '=', iu),
                    ('final_uom', '=', fu),
                    ])
                if conversions:
                    formula = conversions[0].conversion_formula
                if not formula:
                    continue

                initial_uom_volume = conversions[0].initial_uom_volume
                final_uom_volume = conversions[0].final_uom_volume
                variables = self._get_variables(formula, notebook_line,
                    initial_uom_volume, final_uom_volume)
                parser = FormulaParser(formula, variables)
                formula_result = parser.getValue()

                if initial_uom_volume and final_uom_volume:
                    d_ic = VolumeConversion.brixToDensity(ic)
                    d_fc = VolumeConversion.brixToDensity(fc)
                    converted_result = (result * (fc / ic) * (d_fc / d_ic) *
                        formula_result)
                    notebook_line.converted_result = str(converted_result)
                    notebook_line.converted_result_modifier = None
                    lines_to_save.append(notebook_line)
                else:
                    converted_result = result * (fc / ic) * formula_result
                    notebook_line.converted_result = str(converted_result)
                    notebook_line.converted_result_modifier = None
                    lines_to_save.append(notebook_line)
        if lines_to_save:
            NotebookLine.save(lines_to_save)

    def _get_variables(self, formula, notebook_line,
            initial_uom_volume=False, final_uom_volume=False):
        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':
                if initial_uom_volume:
                    c = float(notebook_line.initial_concentration)
                    result = VolumeConversion.brixToDensity(c)
                    if result:
                        variables[var] = result
                elif final_uom_volume:
                    c = float(notebook_line.final_concentration)
                    result = VolumeConversion.brixToDensity(c)
                    if result:
                        variables[var] = result
        return variables

    def end(self):
        return 'reload'


class NotebookLineResultsConversion(NotebookResultsConversion):
    'Results Conversion'
    __name__ = 'lims.notebook_line.results_conversion'

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        notebook_lines = NotebookLine.browse(
            Transaction().context['active_ids'])
        if not notebook_lines:
            return 'end'

        self.lines_results_conversion(notebook_lines)
        return 'end'


class NotebookLimitsValidationStart(ModelView):
    'Limits Validation'
    __name__ = 'lims.notebook.limits_validation.start'


class NotebookLimitsValidation(Wizard):
    'Limits Validation'
    __name__ = 'lims.notebook.limits_validation'

    start_state = 'ok'
    start = StateView('lims.notebook.limits_validation.start',
        'lims.lims_notebook_limits_validation_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Ok', 'ok', 'tryton-ok', default=True),
            ])
    ok = StateTransition()

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        for notebook_id in Transaction().context['active_ids']:
            with Transaction().set_context(_check_access=True):
                notebook_lines = NotebookLine.search([
                    ('notebook', '=', notebook_id),
                    ])
            if not notebook_lines:
                continue
            self.lines_limits_validation(notebook_lines)
        return 'end'

    def lines_limits_validation(self, notebook_lines):
        pool = Pool()
        ModelData = pool.get('ir.model.data')
        NotebookLine = pool.get('lims.notebook.line')

        result_modifier_low = ModelData.get_id('lims', 'result_modifier_low')
        result_modifier_nd = ModelData.get_id('lims', 'result_modifier_nd')
        result_modifier_ni = ModelData.get_id('lims', 'result_modifier_ni')

        lines_to_save = []
        for notebook_line in notebook_lines:
            if notebook_line.accepted:
                continue
            try:
                dl = float(notebook_line.detection_limit)
                ql = float(notebook_line.quantification_limit)
            except (TypeError, ValueError):
                continue

            ll = (notebook_line.lower_limit and
                float(notebook_line.lower_limit) or None)
            ul = (notebook_line.upper_limit and
                float(notebook_line.upper_limit) or None)

            if (notebook_line.result and (
                    notebook_line.check_result_limits or
                    not notebook_line.converted_result)):
                if notebook_line.result_modifier:
                    continue
                try:
                    value = float(notebook_line.result)
                except ValueError:
                    continue
                if (ll and value < ll) or (ul and value > ul):
                    raise UserError(gettext('lims.msg_error_limits_allowed',
                        line=notebook_line.rec_name))
                if dl < value and value < ql:
                    notebook_line.result = str(ql)
                    notebook_line.result_modifier = result_modifier_low
                    notebook_line.converted_result = None
                    notebook_line.converted_result_modifier = None
                    notebook_line.rm_correction_formula = None
                elif value < dl:
                    notebook_line.result = None
                    notebook_line.result_modifier = result_modifier_nd
                    notebook_line.converted_result = None
                    notebook_line.converted_result_modifier = None
                    notebook_line.rm_correction_formula = None
                elif value == dl:
                    notebook_line.result = str(ql)
                    notebook_line.result_modifier = result_modifier_low
                    notebook_line.converted_result = None
                    notebook_line.converted_result_modifier = None
                    notebook_line.rm_correction_formula = None
                notebook_line.backup = str(value)
                lines_to_save.append(notebook_line)

            elif notebook_line.converted_result:
                if notebook_line.converted_result_modifier:
                    continue
                try:
                    value = float(notebook_line.converted_result)
                except ValueError:
                    continue
                if (ll and value < ll) or (ul and value > ul):
                    raise UserError(gettext('lims.msg_error_limits_allowed',
                        line=notebook_line.rec_name))
                if dl < value and value < ql:
                    notebook_line.converted_result = str(ql)
                    notebook_line.converted_result_modifier = (
                        result_modifier_low)
                    notebook_line.result_modifier = result_modifier_ni
                    notebook_line.rm_correction_formula = None
                elif value < dl:
                    notebook_line.converted_result = None
                    notebook_line.converted_result_modifier = (
                        result_modifier_nd)
                    notebook_line.result_modifier = result_modifier_ni
                    notebook_line.rm_correction_formula = None
                elif value == dl:
                    notebook_line.converted_result = str(ql)
                    notebook_line.converted_result_modifier = (
                        result_modifier_low)
                    notebook_line.result_modifier = result_modifier_ni
                    notebook_line.rm_correction_formula = None
                notebook_line.backup = str(value)
                lines_to_save.append(notebook_line)

            else:
                continue

        if lines_to_save:
            NotebookLine.save(lines_to_save)

    def end(self):
        return 'reload'


class NotebookLineLimitsValidation(NotebookLimitsValidation):
    'Limits Validation'
    __name__ = 'lims.notebook_line.limits_validation'

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        notebook_lines = NotebookLine.browse(
            Transaction().context['active_ids'])
        if not notebook_lines:
            return 'end'

        self.lines_limits_validation(notebook_lines)
        return 'end'


class NotebookInternalRelationsCalc1Start(ModelView):
    'Internal Relations Calculation'
    __name__ = 'lims.notebook.internal_relations_calc_1.start'


class NotebookInternalRelationsCalc1Relation(ModelSQL):
    'Internal Relations Calculation'
    __name__ = 'lims.notebook.internal_relations_calc_1.relation'
    _table = 'lims_notebook_internal_relations_c_1_rel'

    notebook = fields.Many2One('lims.notebook', 'Laboratory notebook')
    internal_relation = fields.Many2One('lims.analysis', 'Internal relation')
    variables = fields.One2Many(
        'lims.notebook.internal_relations_calc_1.variable', 'relation',
        'Variables')
    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 NotebookInternalRelationsCalc1Variable(ModelSQL):
    'Formula Variable'
    __name__ = 'lims.notebook.internal_relations_calc_1.variable'

    relation = fields.Many2One(
        'lims.notebook.internal_relations_calc_1.relation', 'Relation',
        ondelete='CASCADE', readonly=True)
    line = fields.Many2One('lims.notebook.line', 'Line')
    analysis = fields.Many2One('lims.analysis', 'Analysis', readonly=True)
    repetition = fields.Integer('Repetition', readonly=True)
    use = fields.Boolean('Use')

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


class NotebookInternalRelationsCalc1(Wizard):
    'Internal Relations Calculation'
    __name__ = 'lims.notebook.internal_relations_calc_1'

    start_state = 'search'
    start = StateView('lims.notebook.internal_relations_calc_1.start',
        'lims.lims_notebook_internal_relations_calc_1_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Search', 'search', 'tryton-forward', default=True),
            ])
    search = StateTransition()
    confirm = StateTransition()

    def transition_search(self):
        NotebookLine = Pool().get('lims.notebook.line')

        for notebook_id in Transaction().context['active_ids']:
            with Transaction().set_context(_check_access=True):
                notebook_lines = NotebookLine.search([
                    ('notebook', '=', notebook_id),
                    ])
            if not notebook_lines:
                continue
            self.get_relations(notebook_lines)
        return 'confirm'

    def get_relations(self, notebook_lines):
        NotebookInternalRelationsCalc1Relation = Pool().get(
            'lims.notebook.internal_relations_calc_1.relation')

        relations = {}
        for notebook_line in notebook_lines:
            if notebook_line.accepted:
                continue
            analysis_code = notebook_line.analysis.code
            if (not analysis_code or notebook_line.analysis.behavior !=
                    'internal_relation'):
                continue
            if notebook_line.result or notebook_line.converted_result:
                continue

            formulas = notebook_line.analysis.result_formula
            formulas += ("+" +
                notebook_line.analysis.converted_result_formula)
            for i in (' ', '\t', '\n', '\r'):
                formulas = formulas.replace(i, '')
            variables = self._get_variables_list(formulas,
                notebook_line.notebook, {})
            if not variables:
                continue
            has_repetition_zero = False
            for var in variables:
                if var['use']:
                    has_repetition_zero = True
            if not has_repetition_zero:
                continue

            relations[notebook_line.analysis.id] = {
                'notebook': notebook_line.notebook.id,
                'internal_relation': notebook_line.analysis.id,
                'variables': [('create', variables)],
                'session_id': self._session_id,
                }
        if relations:
            NotebookInternalRelationsCalc1Relation.create(
                [ir for ir in relations.values()])
            return True
        return False

    def _get_variables_list(self, formula, notebook, analysis={}):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')

        variables = {}
        for variable in re.findall(r'\{.*?\}', formula):
            var = variable.replace('{', '').replace('}', '')
            for prefix in ('A', 'D', 'T', 'Y', 'R'):
                if var.startswith(prefix):
                    variables[var] = None

        for var in variables.keys():
            if var[0] in ('A', 'D', 'T'):
                analysis_code = var[1:]
                with Transaction().set_user(0):
                    notebook_lines = NotebookLine.search([
                        ('notebook', '=', notebook.id),
                        ('analysis.code', '=', analysis_code),
                        ('annulment_date', '=', None),
                        ])
                if not notebook_lines:
                    continue
                for nl in notebook_lines:
                    analysis[nl.id] = {
                        'line': nl.id,
                        'analysis': nl.analysis.id,
                        'repetition': nl.repetition,
                        'use': True if nl.repetition == 0 else False,
                        }
            elif var[0] in ('Y', 'R'):
                analysis_code = var[1:]
                internal_relations = Analysis.search([
                    ('code', '=', analysis_code),
                    ])
                if not internal_relations:
                    continue
                more_formulas = internal_relations[0].converted_result_formula
                more_formulas += "+" + internal_relations[0].result_formula
                for i in (' ', '\t', '\n', '\r'):
                    more_formulas = more_formulas.replace(i, '')
                self._get_variables_list(more_formulas, notebook, analysis)

        return [v for v in analysis.values()]

    def transition_confirm(self):
        pool = Pool()
        Date = pool.get('ir.date')
        NotebookInternalRelationsCalc1Relation = pool.get(
            'lims.notebook.internal_relations_calc_1.relation')
        NotebookLine = pool.get('lims.notebook.line')

        date = Date.today()

        relations = NotebookInternalRelationsCalc1Relation.search([
            ('session_id', '=', self._session_id),
            ])
        lines_to_save = []
        lines_to_validate_limits = []
        for relation in relations:
            notebook_lines = NotebookLine.search([
                ('notebook', '=', relation.notebook.id),
                ('analysis', '=', relation.internal_relation.id)
                ])
            if len(notebook_lines) != 1:
                continue

            analysis_code = relation.internal_relation.code
            result = self._get_relation_result(analysis_code,
                relation.notebook, analysis_code, round_=True)
            converted_result = self._get_relation_result(analysis_code,
                relation.notebook, analysis_code, converted=True)

            notebook_line = notebook_lines[0]
            if result is not None:
                notebook_line.result = str(result)
            if converted_result is not None:
                notebook_line.converted_result = str(converted_result)
            if result is not None or converted_result is not None:
                notebook_line.start_date = date
                notebook_line.end_date = date
                if notebook_line.laboratory.automatic_accept_result:
                    notebook_line.accepted = True
                    notebook_line.acceptance_date = datetime.now()
                lines_to_save.append(notebook_line)
                if notebook_line.analysis.validate_limits_after_calculation:
                    lines_to_validate_limits.append(notebook_line)
        NotebookLine.save(lines_to_save)

        if lines_to_validate_limits:
            self.validate_limits(lines_to_validate_limits)
        return 'end'

    def validate_limits(self, notebook_lines):
        pool = Pool()
        LimitsValidation = pool.get('lims.notebook.limits_validation',
            type='wizard')
        session_id, _, _ = LimitsValidation.create()
        limits_validation = LimitsValidation(session_id)
        limits_validation.lines_limits_validation(notebook_lines)

    def _get_analysis_result(self, analysis_code, notebook, relation_code,
            converted=False):
        NotebookInternalRelationsCalc1Variable = Pool().get(
            'lims.notebook.internal_relations_calc_1.variable')

        variables = NotebookInternalRelationsCalc1Variable.search([
            ('relation.session_id', '=', self._session_id),
            ('relation.notebook', '=', notebook.id),
            ('relation.internal_relation.code', '=', relation_code),
            ('analysis.code', '=', analysis_code),
            ('use', '=', True),
            ])
        if not variables:
            return None

        notebook_line = variables[0].line
        if not notebook_line:
            return None

        try:
            if converted:
                res = float(notebook_line.converted_result)
            else:
                res = float(notebook_line.result)
        except (TypeError, ValueError):
            return None
        return round(res, notebook_line.decimals)

    def _get_relation_result(self, analysis_code, notebook, relation_code,
            converted=False, round_=False):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')

        internal_relations = Analysis.search([
            ('code', '=', analysis_code),
            ])
        if not internal_relations:
            return None
        if converted:
            formula = internal_relations[0].converted_result_formula
        else:
            formula = internal_relations[0].result_formula
        if not formula:
            return None
        for i in (' ', '\t', '\n', '\r'):
            formula = formula.replace(i, '')
        variables = self._get_variables(formula, notebook, relation_code,
            converted)
        if not variables:
            return None

        parser = FormulaParser(formula, variables)
        value = parser.getValue()

        if int(value) == value:
            res = int(value)
        else:
            epsilon = 0.0000000001
            if int(value + epsilon) != int(value):
                res = int(value + epsilon)
            elif int(value - epsilon) != int(value):
                res = int(value)
            else:
                res = float(value)
        if not round_:
            return res

        with Transaction().set_user(0):
            notebook_lines = NotebookLine.search([
                ('notebook', '=', notebook.id),
                ('analysis.code', '=', analysis_code),
                ('repetition', '=', 0),
                ('annulment_date', '=', None),
                ])
        if not notebook_lines:
            return None
        return round(res, notebook_lines[0].decimals)

    def _get_variables(self, formula, notebook, relation_code,
            converted=False):
        pool = Pool()
        VolumeConversion = pool.get('lims.volume.conversion')

        variables = {}
        for variable in re.findall(r'\{.*?\}', formula):
            var = variable.replace('{', '').replace('}', '')
            for prefix in ('A', 'D', 'T', 'Y', 'R'):
                if var.startswith(prefix):
                    variables[var] = None

        for var in variables.keys():
            if var[0] == 'A':
                analysis_code = var[1:]
                result = self._get_analysis_result(analysis_code, notebook,
                    relation_code, converted)
                if result is not None:
                    variables[var] = result
            elif var[0] == 'D':
                analysis_code = var[1:]
                result = self._get_analysis_result(analysis_code, notebook,
                    relation_code, converted)
                if result is not None:
                    result = VolumeConversion.brixToDensity(result)
                    if result is not None:
                        variables[var] = result
            elif var[0] == 'T':
                analysis_code = var[1:]
                result = self._get_analysis_result(analysis_code, notebook,
                    relation_code, converted)
                if result is not None:
                    result = VolumeConversion.brixToSolubleSolids(result)
                    if result is not None:
                        variables[var] = result
            elif var[0] == 'R':
                analysis_code = var[1:]
                result = self._get_relation_result(analysis_code, notebook,
                    relation_code, converted, round_=True)
                if result is not None:
                    result = VolumeConversion.brixToSolubleSolids(result)
                    if result is not None:
                        variables[var] = result
            elif var[0] == 'Y':
                analysis_code = var[1:]
                result = self._get_relation_result(analysis_code, notebook,
                    relation_code, converted, round_=True)
                if result is not None:
                    result = VolumeConversion.brixToDensity(result)
                    if result is not None:
                        variables[var] = result
        for var in variables.values():
            if var is None:
                return None
        return variables

    def end(self):
        return 'reload'


class NotebookLineInternalRelationsCalc1(NotebookInternalRelationsCalc1):
    'Internal Relations Calculation'
    __name__ = 'lims.notebook_line.internal_relations_calc_1'

    def transition_search(self):
        NotebookLine = Pool().get('lims.notebook.line')

        notebook_lines = NotebookLine.browse(
            Transaction().context['active_ids'])
        if not notebook_lines:
            return 'end'

        if self.get_relations(notebook_lines):
            return 'confirm'
        return 'end'


class NotebookInternalRelationsCalc2Start(ModelView):
    'Internal Relations Calculation'
    __name__ = 'lims.notebook.internal_relations_calc_2.start'


class NotebookInternalRelationsCalc2Result(ModelView):
    'Internal Relations Calculation'
    __name__ = 'lims.notebook.internal_relations_calc_2.result'

    relations = fields.Many2Many(
        'lims.notebook.internal_relations_calc_2.relation', None, None,
        'Relation')
    total = fields.Integer('Total')
    index = fields.Integer('Index')


class NotebookInternalRelationsCalc2Relation(ModelSQL, ModelView):
    'Internal Relations Calculation'
    __name__ = 'lims.notebook.internal_relations_calc_2.relation'
    _table = 'lims_notebook_internal_relations_c_2_rel'

    notebook = fields.Many2One('lims.notebook', 'Laboratory notebook')
    internal_relation = fields.Many2One('lims.analysis', 'Internal relation')
    variables = fields.One2Many(
        'lims.notebook.internal_relations_calc_2.variable', 'relation',
        'Variables')
    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 NotebookInternalRelationsCalc2Variable(ModelSQL, ModelView):
    'Formula Variable'
    __name__ = 'lims.notebook.internal_relations_calc_2.variable'

    relation = fields.Many2One(
        'lims.notebook.internal_relations_calc_2.relation', 'Relation',
        ondelete='CASCADE', readonly=True)
    line = fields.Many2One('lims.notebook.line', 'Line')
    analysis = fields.Many2One('lims.analysis', 'Analysis', readonly=True)
    repetition = fields.Integer('Repetition', readonly=True)
    result_modifier = fields.Function(fields.Many2One('lims.result_modifier',
        'Result modifier'), 'get_line_field')
    result = fields.Function(fields.Char('Result'), 'get_line_field')
    initial_unit = fields.Function(fields.Many2One('product.uom',
        'Initial unit'), 'get_line_field')
    initial_concentration = fields.Function(fields.Char(
        'Initial concentration'), 'get_line_field')
    converted_result_modifier = fields.Function(fields.Many2One(
        'lims.result_modifier', 'Converted result modifier'), 'get_line_field')
    converted_result = fields.Function(fields.Char('Converted result'),
        'get_line_field')
    final_unit = fields.Function(fields.Many2One('product.uom', 'Final unit'),
        'get_line_field')
    final_concentration = fields.Function(fields.Char('Final concentration'),
        'get_line_field')
    use = fields.Boolean('Use')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('relation', 'ASC'))
        cls._order.insert(1, ('analysis', 'ASC'))
        cls._order.insert(2, ('repetition', 'ASC'))

    @classmethod
    def get_line_field(cls, variables, names):
        result = {}
        for name in names:
            result[name] = {}
            if cls._fields[name]._type == 'many2one':
                for v in variables:
                    field = getattr(v.line, name, None)
                    result[name][v.id] = field.id if field else None
            else:
                for v in variables:
                    result[name][v.id] = getattr(v.line, name, None)
        return result


class NotebookInternalRelationsCalc2Process(ModelView):
    'Internal Relations Calculation'
    __name__ = 'lims.notebook.internal_relations_calc_2.process'

    notebook = fields.Many2One('lims.notebook', 'Laboratory notebook',
        readonly=True)
    internal_relation = fields.Many2One('lims.analysis', 'Internal relation',
        readonly=True)
    variables = fields.One2Many(
        'lims.notebook.internal_relations_calc_2.variable', None,
        'Variables')


class NotebookInternalRelationsCalc2(Wizard):
    'Internal Relations Calculation'
    __name__ = 'lims.notebook.internal_relations_calc_2'

    start_state = 'search'
    start = StateView('lims.notebook.internal_relations_calc_2.start',
        'lims.lims_notebook_internal_relations_calc_2_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Search', 'search', 'tryton-forward', default=True),
            ])
    search = StateTransition()
    result = StateView('lims.notebook.internal_relations_calc_2.result',
        'lims.lims_notebook_internal_relations_calc_2_result_view_form', [])
    next_ = StateTransition()
    process = StateView('lims.notebook.internal_relations_calc_2.process',
        'lims.lims_notebook_internal_relations_calc_2_process_view_form', [
            Button('Next', 'check_variables', 'tryton-forward', default=True),
            ])
    check_variables = StateTransition()
    confirm = StateTransition()

    def transition_search(self):
        NotebookLine = Pool().get('lims.notebook.line')

        for notebook_id in Transaction().context['active_ids']:
            with Transaction().set_context(_check_access=True):
                notebook_lines = NotebookLine.search([
                    ('notebook', '=', notebook_id),
                    ])
            if not notebook_lines:
                continue
            if self.get_relations(notebook_lines):
                return 'next_'
        return 'end'

    def get_relations(self, notebook_lines):
        NotebookInternalRelationsCalc2Relation = Pool().get(
            'lims.notebook.internal_relations_calc_2.relation')

        relations = {}
        for notebook_line in notebook_lines:
            if notebook_line.accepted:
                continue
            analysis_code = notebook_line.analysis.code
            if (not analysis_code or notebook_line.analysis.behavior !=
                    'internal_relation'):
                continue
            if notebook_line.result or notebook_line.converted_result:
                continue

            formulas = notebook_line.analysis.result_formula
            formulas += ("+" +
                notebook_line.analysis.converted_result_formula)
            for i in (' ', '\t', '\n', '\r'):
                formulas = formulas.replace(i, '')
            variables = self._get_variables_list(formulas,
                notebook_line.notebook, {})
            if not variables:
                continue
            has_repetitions = False
            for var in variables:
                if var['repetition'] > 0:
                    has_repetitions = True
            if not has_repetitions:
                continue

            relations[notebook_line.analysis.id] = {
                'notebook': notebook_line.notebook.id,
                'internal_relation': notebook_line.analysis.id,
                'variables': [('create', variables)],
                'session_id': self._session_id,
                }

        if relations:
            res_lines = NotebookInternalRelationsCalc2Relation.create(
                [ir for ir in relations.values()])
            self.result.relations = res_lines
            self.result.total = len(self.result.relations)
            self.result.index = 0
            return True
        return False

    def transition_next_(self):
        if self.result.index < self.result.total:
            relation = self.result.relations[self.result.index]
            self.process.notebook = relation.notebook.id
            self.process.internal_relation = relation.internal_relation.id
            self.process.variables = None
            self.result.index += 1
            return 'process'
        return 'confirm'

    def default_process(self, fields):
        NotebookInternalRelationsCalc2Variable = Pool().get(
            'lims.notebook.internal_relations_calc_2.variable')

        if not self.process.internal_relation:
            return {}

        default = {}
        default['notebook'] = self.process.notebook.id
        default['internal_relation'] = self.process.internal_relation.id
        if self.process.variables:
            default['variables'] = [v.id for v in self.process.variables]
        else:
            variables = NotebookInternalRelationsCalc2Variable.search([
                ('relation.session_id', '=', self._session_id),
                ('relation.notebook', '=', self.process.notebook.id),
                ('relation.internal_relation', '=',
                    self.process.internal_relation.id),
                ])
            if variables:
                default['variables'] = [v.id for v in variables]
        return default

    def _get_variables_list(self, formula, notebook, analysis={}):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')

        variables = {}
        for variable in re.findall(r'\{.*?\}', formula):
            var = variable.replace('{', '').replace('}', '')
            for prefix in ('A', 'D', 'T', 'Y', 'R'):
                if var.startswith(prefix):
                    variables[var] = None

        for var in variables.keys():
            if var[0] in ('A', 'D', 'T'):
                analysis_code = var[1:]
                with Transaction().set_user(0):
                    notebook_lines = NotebookLine.search([
                        ('notebook', '=', notebook.id),
                        ('analysis.code', '=', analysis_code),
                        ('annulment_date', '=', None),
                        ])
                if not notebook_lines:
                    continue
                for nl in notebook_lines:
                    analysis[nl.id] = {
                        'line': nl.id,
                        'analysis': nl.analysis.id,
                        'repetition': nl.repetition,
                        'use': True if nl.repetition == 0 else False,
                        }
            elif var[0] in ('R', 'Y'):
                analysis_code = var[1:]
                internal_relations = Analysis.search([
                    ('code', '=', analysis_code),
                    ])
                if not internal_relations:
                    continue
                more_formulas = internal_relations[0].converted_result_formula
                more_formulas += "+" + internal_relations[0].result_formula
                for i in (' ', '\t', '\n', '\r'):
                    more_formulas = more_formulas.replace(i, '')
                self._get_variables_list(more_formulas, notebook, analysis)

        return [v for v in analysis.values()]

    def transition_check_variables(self):
        variables = {}
        for var in self.process.variables:
            analysis_code = var.analysis.code
            if analysis_code not in variables:
                variables[analysis_code] = False
            if var.use:
                if variables[analysis_code]:
                    variables[analysis_code] = False
                else:
                    variables[analysis_code] = True
            var.save()

        for var in variables.values():
            if not var:
                return 'process'
        return 'next_'

    def transition_confirm(self):
        pool = Pool()
        Date = pool.get('ir.date')
        NotebookInternalRelationsCalc2Relation = pool.get(
            'lims.notebook.internal_relations_calc_2.relation')
        NotebookLine = pool.get('lims.notebook.line')

        date = Date.today()

        relations = NotebookInternalRelationsCalc2Relation.search([
            ('session_id', '=', self._session_id),
            ])
        notebook_lines_to_save = []
        for relation in relations:
            notebook_lines = NotebookLine.search([
                ('notebook', '=', relation.notebook.id),
                ('analysis', '=', relation.internal_relation.id)
                ])
            if len(notebook_lines) != 1:
                continue

            analysis_code = relation.internal_relation.code
            result = self._get_relation_result(analysis_code,
                relation.notebook, analysis_code, round_=True)
            converted_result = self._get_relation_result(analysis_code,
                relation.notebook, analysis_code, converted=True)

            notebook_line = notebook_lines[0]
            if result is not None:
                notebook_line.result = str(result)
            if converted_result is not None:
                notebook_line.converted_result = str(converted_result)
            if result is not None or converted_result is not None:
                notebook_line.start_date = date
                notebook_line.end_date = date
                if notebook_line.laboratory.automatic_accept_result:
                    notebook_line.accepted = True
                    notebook_line.acceptance_date = datetime.now()
                notebook_lines_to_save.append(notebook_line)
        NotebookLine.save(notebook_lines_to_save)
        return 'end'

    def _get_analysis_result(self, analysis_code, notebook, relation_code,
            converted=False):
        NotebookInternalRelationsCalc2Variable = Pool().get(
            'lims.notebook.internal_relations_calc_2.variable')

        variables = NotebookInternalRelationsCalc2Variable.search([
            ('relation.session_id', '=', self._session_id),
            ('relation.notebook', '=', notebook.id),
            ('relation.internal_relation.code', '=', relation_code),
            ('analysis.code', '=', analysis_code),
            ('use', '=', True),
            ])
        if not variables:
            return None

        notebook_line = variables[0].line
        if not notebook_line:
            return None

        try:
            if converted:
                res = float(notebook_line.converted_result)
            else:
                res = float(notebook_line.result)
        except (TypeError, ValueError):
            return None
        return round(res, notebook_line.decimals)

    def _get_relation_result(self, analysis_code, notebook, relation_code,
            converted=False, round_=False):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')

        internal_relations = Analysis.search([
            ('code', '=', analysis_code),
            ])
        if not internal_relations:
            return None
        if converted:
            formula = internal_relations[0].converted_result_formula
        else:
            formula = internal_relations[0].result_formula
        if not formula:
            return None
        for i in (' ', '\t', '\n', '\r'):
            formula = formula.replace(i, '')
        variables = self._get_variables(formula, notebook, relation_code,
            converted)
        if not variables:
            return None

        parser = FormulaParser(formula, variables)
        value = parser.getValue()

        if int(value) == value:
            res = int(value)
        else:
            epsilon = 0.0000000001
            if int(value + epsilon) != int(value):
                res = int(value + epsilon)
            elif int(value - epsilon) != int(value):
                res = int(value)
            else:
                res = float(value)
        if not round_:
            return res

        with Transaction().set_user(0):
            notebook_lines = NotebookLine.search([
                ('notebook', '=', notebook.id),
                ('analysis.code', '=', analysis_code),
                ('repetition', '=', 0),
                ('annulment_date', '=', None),
                ])
        if not notebook_lines:
            return None
        return round(res, notebook_lines[0].decimals)

    def _get_variables(self, formula, notebook, relation_code,
            converted=False):
        pool = Pool()
        VolumeConversion = pool.get('lims.volume.conversion')

        variables = {}
        for variable in re.findall(r'\{.*?\}', formula):
            var = variable.replace('{', '').replace('}', '')
            for prefix in ('A', 'D', 'T', 'Y', 'R'):
                if var.startswith(prefix):
                    variables[var] = None

        for var in variables.keys():
            if var[0] == 'A':
                analysis_code = var[1:]
                result = self._get_analysis_result(analysis_code, notebook,
                    relation_code, converted)
                if result is not None:
                    variables[var] = result
            elif var[0] == 'D':
                analysis_code = var[1:]
                result = self._get_analysis_result(analysis_code, notebook,
                    relation_code, converted)
                if result is not None:
                    result = VolumeConversion.brixToDensity(result)
                    if result is not None:
                        variables[var] = result
            elif var[0] == 'T':
                analysis_code = var[1:]
                result = self._get_analysis_result(analysis_code, notebook,
                    relation_code, converted)
                if result is not None:
                    result = VolumeConversion.brixToSolubleSolids(result)
                    if result is not None:
                        variables[var] = result
            elif var[0] == 'R':
                analysis_code = var[1:]
                result = self._get_relation_result(analysis_code, notebook,
                    relation_code, converted, round_=True)
                if result is not None:
                    result = VolumeConversion.brixToSolubleSolids(result)
                    if result is not None:
                        variables[var] = result
            elif var[0] == 'Y':
                analysis_code = var[1:]
                result = self._get_relation_result(analysis_code, notebook,
                    relation_code, converted, round_=True)
                if result is not None:
                    result = VolumeConversion.brixToDensity(result)
                    if result is not None:
                        variables[var] = result
        for var in variables.values():
            if var is None:
                return None
        return variables

    def end(self):
        return 'reload'


class NotebookLineInternalRelationsCalc2(NotebookInternalRelationsCalc2):
    'Internal Relations Calculation'
    __name__ = 'lims.notebook_line.internal_relations_calc_2'

    def transition_search(self):
        NotebookLine = Pool().get('lims.notebook.line')

        notebook_lines = NotebookLine.browse(
            Transaction().context['active_ids'])
        if not notebook_lines:
            return 'end'

        if self.get_relations(notebook_lines):
            return 'next_'
        return 'end'


class NotebookLoadResultsFormulaStart(ModelView):
    'Load Results by Formula'
    __name__ = 'lims.notebook.load_results_formula.start'

    analysis = fields.Many2One('lims.analysis', 'Analysis',
        domain=[('state', '=', 'active'), ('formula', '!=', None)])
    method = fields.Many2One('lims.lab.method', 'Method')
    start_date = fields.Date('Start date', required=True)


class NotebookLoadResultsFormulaEmpty(ModelView):
    'Load Results by Formula'
    __name__ = 'lims.notebook.load_results_formula.empty'


class NotebookLoadResultsFormulaResult(ModelView):
    'Load Results by Formula'
    __name__ = 'lims.notebook.load_results_formula.result'

    lines = fields.Many2Many('lims.notebook.load_results_formula.line',
        None, None, 'Lines')
    total = fields.Integer('Total')
    index = fields.Integer('Index')


class NotebookLoadResultsFormulaLine(ModelSQL, ModelView):
    'Load Results by Formula'
    __name__ = 'lims.notebook.load_results_formula.line'

    index = fields.Integer('Index')
    line = fields.Many2One('lims.notebook.line', 'Line')
    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 NotebookLoadResultsFormulaAction(ModelSQL):
    'Load Results by Formula'
    __name__ = 'lims.notebook.load_results_formula.action'

    line = fields.Many2One('lims.notebook.line', 'Line')
    result = fields.Char('Result')
    result_modifier = fields.Many2One('lims.result_modifier',
        'Result modifier')
    end_date = fields.Date('End date')
    professional = fields.Many2One('lims.laboratory.professional',
        'Laboratory professional')
    chromatogram = fields.Char('Chromatogram')
    initial_concentration = fields.Char('Initial concentration')
    comments = fields.Text('Comments')
    formula = fields.Many2One('lims.formula', 'Formula')
    variables = fields.One2Many('lims.notebook.load_results_formula.variable',
        'action', 'Variables')
    session_id = fields.Integer('Session ID')

    @classmethod
    def __register__(cls, module_name):
        cursor = Transaction().connection.cursor()
        table_h = cls.__table_handler__(module_name)
        migrate_result_modifier = (
            table_h.column_exist('result_modifier') and
            table_h.column_is_type('result_modifier', 'VARCHAR'))
        if migrate_result_modifier:
            table_h.column_rename('result_modifier',
                'temp_result_modifier')
        super().__register__(module_name)
        if migrate_result_modifier:
            table_h.drop_column('temp_result_modifier')
        cursor.execute('DELETE FROM "' + cls._table + '"')


class NotebookLoadResultsFormulaProcess(ModelView):
    'Load Results by Formula'
    __name__ = 'lims.notebook.load_results_formula.process'

    line = fields.Many2One('lims.notebook.line', 'Line', readonly=True)
    repetition = fields.Integer('Repetition', readonly=True)
    fraction = fields.Many2One('lims.fraction', 'Fraction', readonly=True)
    product_type = fields.Many2One('lims.product.type', 'Product type',
        readonly=True)
    matrix = fields.Many2One('lims.matrix', 'Matrix', readonly=True)
    formula = fields.Many2One('lims.formula', 'Formula', readonly=True)
    formula_formula = fields.Function(fields.Char('Formula'),
        'on_change_with_formula_formula')
    variables = fields.One2Many('lims.notebook.load_results_formula.variable',
        None, 'Variables')
    result = fields.Char('Result', required=True)
    result_modifier = fields.Many2One('lims.result_modifier',
        'Result modifier')
    end_date = fields.Date('End date')
    end_date_copy = fields.Boolean('Field copy')
    professional = fields.Many2One('lims.laboratory.professional',
        'Laboratory professional', required=True, readonly=True)
    chromatogram = fields.Char('Chromatogram')
    chromatogram_copy = fields.Boolean('Field copy')
    initial_concentration = fields.Char('Initial concentration')
    initial_concentration_copy = fields.Boolean('Field copy')
    comments = fields.Text('Comments')
    comments_copy = fields.Boolean('Field copy')

    @fields.depends('formula', 'variables', '_parent_formula.formula')
    def on_change_with_result(self, name=None):
        if not self.formula or not self.variables:
            return None

        formula = self.formula.formula
        variables = {}
        for variable in self.variables:
            if not variable.value:
                return ''
            variables[variable.number] = variable.value

        parser = FormulaParser(formula, variables)
        value = parser.getValue()

        return str(value)

    @fields.depends('formula', '_parent_formula.formula')
    def on_change_with_formula_formula(self, name=None):
        if self.formula:
            formula = self.formula.formula
            variables = {}
            for variable in self.variables:
                variables[variable.number] = variable.description
            for k, v in variables.items():
                formula = formula.replace(k, v)
            return formula
        return ''


class NotebookLoadResultsFormulaVariable(ModelSQL, ModelView):
    'Formula Variable'
    __name__ = 'lims.notebook.load_results_formula.variable'

    action = fields.Many2One('lims.notebook.load_results_formula.action',
        'Action', ondelete='CASCADE')
    number = fields.Char('Number', readonly=True)
    description = fields.Char('Description', readonly=True)
    value = fields.Char('Value')


class NotebookLoadResultsFormulaBeginning(ModelView):
    'Load Results by Formula'
    __name__ = 'lims.notebook.load_results_formula.beginning'


class NotebookLoadResultsFormulaConfirm(ModelView):
    'Load Results by Formula'
    __name__ = 'lims.notebook.load_results_formula.confirm'


class NotebookLoadResultsFormulaSit1(ModelView):
    'Professionals Control'
    __name__ = 'lims.notebook.load_results_formula.sit1'

    msg = fields.Text('Message')


class NotebookLoadResultsFormulaSit2(ModelView):
    'Professionals Control'
    __name__ = 'lims.notebook.load_results_formula.sit2'

    details = fields.One2Many('lims.notebook.load_results_formula.sit2.detail',
        None, 'Supervisors')


class NotebookLoadResultsFormulaSit2Detail(ModelSQL, ModelView):
    'Supervisor'
    __name__ = 'lims.notebook.load_results_formula.sit2.detail'
    _table = 'lims_notebook_load_results_formula_s2_detail'

    professional = fields.Many2One('lims.laboratory.professional',
        'Professional', readonly=True)
    method = fields.Many2One('lims.lab.method', 'Method', readonly=True)
    supervisor = fields.Many2One('lims.laboratory.professional',
        'Supervisor', depends=['supervisor_domain'],
        domain=[('id', 'in', Eval('supervisor_domain'))])
    supervisor_domain = fields.Function(fields.Many2Many(
        'lims.laboratory.professional', None, None, 'Supervisor domain'),
        'get_supervisor_domain')
    lines = fields.Many2Many(
        'lims.notebook.load_results_formula.sit2.detail.line',
        'load_results', 'notebook_line', 'Lines')
    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 + '"')

    def get_supervisor_domain(self, name=None):
        LabProfessionalMethod = Pool().get('lims.lab.professional.method')

        res = []
        qualifications = LabProfessionalMethod.search([
            ('method', '=', self.method.id),
            ('type', '=', 'analytical'),
            ('state', 'in', ('qualified', 'requalified')),
            ])
        if qualifications:
            res = [q.professional.id for q in qualifications]
        return res


class NotebookLoadResultsFormulaSit2DetailLine(ModelSQL):
    'Notebook Line'
    __name__ = 'lims.notebook.load_results_formula.sit2.detail.line'
    _table = 'lims_notebook_load_results_formula_sit2_d_l'

    load_results = fields.Many2One(
        'lims.notebook.load_results_formula.sit2.detail', 'Load Results',
        ondelete='CASCADE', select=True, required=True)
    notebook_line = fields.Many2One('lims.notebook.line', 'Notebook Line',
        ondelete='CASCADE', select=True, required=True)


class NotebookLoadResultsFormula(Wizard):
    'Load Results by Formula'
    __name__ = 'lims.notebook.load_results_formula'

    start = StateView('lims.notebook.load_results_formula.start',
        'lims.lims_notebook_load_results_formula_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Search', 'search', 'tryton-forward', default=True),
            ])
    search = StateTransition()
    empty = StateView('lims.notebook.load_results_formula.empty',
        'lims.lims_notebook_load_results_formula_empty_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Search again', 'start', 'tryton-forward', default=True),
            ])
    result = StateView('lims.notebook.load_results_formula.result',
        'lims.lims_notebook_load_results_formula_result_view_form', [])
    next_ = StateTransition()
    prev_ = StateTransition()
    process = StateView('lims.notebook.load_results_formula.process',
        'lims.lims_notebook_load_results_formula_process_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Previous', 'prev_', 'tryton-back'),
            Button('Next', 'next_', 'ttryton-forward', default=True),
            ])
    beginning = StateView('lims.notebook.load_results_formula.beginning',
        'lims.lims_notebook_load_results_formula_beginning_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Next', 'next_', 'tryton-forward', default=True),
            ])
    confirm = StateView('lims.notebook.load_results_formula.confirm',
        'lims.lims_notebook_load_results_formula_confirm_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Previous', 'prev_', 'tryton-back'),
            Button('Confirm', 'check_professional', 'tryton-ok', default=True),
            ])
    check_professional = StateTransition()
    sit1 = StateView('lims.notebook.load_results_formula.sit1',
        'lims.lims_notebook_load_results_formula_sit1_view_form', [
            Button('Cancel', 'end', 'tryton-cancel', default=True),
            ])
    sit2 = StateView('lims.notebook.load_results_formula.sit2',
        'lims.lims_notebook_load_results_formula_sit2_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Confirm', 'sit2_ok', 'tryton-ok', default=True),
            ])
    sit2_ok = StateTransition()
    confirm_ = StateTransition()

    def transition_search(self):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        NotebookLoadResultsFormulaLine = pool.get(
            'lims.notebook.load_results_formula.line')

        clause = [
            ('start_date', '=', self.start.start_date),
            ('end_date', '=', None),
            ('analysis.formula', '!=', None),
            ]
        if self.start.analysis:
            clause.append(('analysis', '=', self.start.analysis.id))
        if self.start.method:
            clause.append(('method', '=', self.start.method.id))

        lines = NotebookLine.search(clause, order=[
            ('analysis_order', 'ASC'), ('id', 'ASC')])
        if lines:
            res_lines = []
            count = 1
            for line in lines:
                res_line, = NotebookLoadResultsFormulaLine.create([{
                    'session_id': self._session_id,
                    'index': count,
                    'line': line.id,
                    }])
                res_lines.append(res_line.id)
                count += 1

            self.result.lines = res_lines
            self.result.total = len(res_lines)
            self.result.index = 0
            return 'next_'
        return 'empty'

    def transition_next_(self):
        pool = Pool()
        NotebookLoadResultsFormulaAction = pool.get(
            'lims.notebook.load_results_formula.action')
        NotebookLoadResultsFormulaLine = pool.get(
            'lims.notebook.load_results_formula.line')
        LaboratoryProfessional = pool.get('lims.laboratory.professional')

        has_prev = (hasattr(self.process, 'line') and
            getattr(self.process, 'line'))
        if has_prev:
            defaults = {
                'session_id': self._session_id,
                'line': self.process.line.id,
                'result': self.process.result,
                'result_modifier': (self.process.result_modifier.id if
                    self.process.result_modifier else None),
                'end_date': self.process.end_date,
                'professional': self.process.professional.id,
                'chromatogram': self.process.chromatogram,
                'initial_concentration': self.process.initial_concentration,
                'comments': self.process.comments,
                'formula': (self.process.formula.id if
                    self.process.formula else None),
                }
            variables = []
            for var in self.process.variables:
                variables.append({
                    'number': var.number,
                    'description': var.description,
                    'value': var.value,
                    })
            defaults['variables'] = [('create', variables)]

            action = NotebookLoadResultsFormulaAction.search([
                ('session_id', '=', self._session_id),
                ('line', '=', self.process.line.id),
                ])
            if action:
                defaults['variables'] = [(
                    'delete', [v.id for a in action for v in a.variables],
                    )] + defaults['variables']
                NotebookLoadResultsFormulaAction.write(action, defaults)
            else:
                NotebookLoadResultsFormulaAction.create([defaults])

        self.result.index += 1
        if self.result.index <= self.result.total:

            line = NotebookLoadResultsFormulaLine.search([
                ('session_id', '=', self._session_id),
                ('index', '=', self.result.index),
                ])
            self.process.line = line[0].line.id

            action = NotebookLoadResultsFormulaAction.search([
                ('session_id', '=', self._session_id),
                ('line', '=', line[0].line.id),
                ])
            if action:
                self.process.result = action[0].result
                self.process.result_modifier = (action[0].result_modifier.id if
                    action[0].result_modifier else None)
                self.process.end_date = action[0].end_date
                self.process.professional = action[0].professional.id
                self.process.chromatogram = action[0].chromatogram
                self.process.initial_concentration = (
                    action[0].initial_concentration)
                self.process.comments = action[0].comments
                self.process.formula = action[0].formula
                self.process.variables = [v.id for v in action[0].variables]
            elif has_prev:
                self.process.result = None
                self.process.result_modifier = None
                if not self.process.end_date_copy:
                    self.process.end_date = None
                if not self.process.chromatogram_copy:
                    self.process.chromatogram = None
                if not self.process.initial_concentration_copy:
                    self.process.initial_concentration = None
                if not self.process.comments_copy:
                    self.process.comments = None
                self.process.formula = None
                self.process.variables = None
            else:
                professional_id = (
                    LaboratoryProfessional.get_lab_professional())
                self.process.professional = professional_id

            return 'process'
        return 'confirm'

    def transition_prev_(self):
        pool = Pool()
        NotebookLoadResultsFormulaAction = pool.get(
            'lims.notebook.load_results_formula.action')
        NotebookLoadResultsFormulaLine = pool.get(
            'lims.notebook.load_results_formula.line')

        self.result.index -= 1
        if self.result.index >= 1:
            line = NotebookLoadResultsFormulaLine.search([
                ('session_id', '=', self._session_id),
                ('index', '=', self.result.index),
                ])
            self.process.line = line[0].line.id

            action = NotebookLoadResultsFormulaAction.search([
                ('session_id', '=', self._session_id),
                ('line', '=', line[0].line.id),
                ])
            if action:
                self.process.result = action[0].result
                self.process.result_modifier = (action[0].result_modifier.id if
                    action[0].result_modifier else None)
                self.process.end_date = action[0].end_date
                self.process.professional = action[0].professional.id
                self.process.chromatogram = action[0].chromatogram
                self.process.initial_concentration = (
                    action[0].initial_concentration)
                self.process.comments = action[0].comments
                self.process.formula = action[0].formula
                self.process.variables = [v.id for v in action[0].variables]
            else:
                self.process.result = None
                self.process.result_modifier = None
                self.process.end_date = None
                self.process.professional = None
                self.process.chromatogram = None
                self.process.initial_concentration = None
                self.process.comments = None
                self.process.formula = None
                self.process.variables = None

            return 'process'

        self.process.line = None
        return 'beginning'

    def default_process(self, fields):
        if not self.process.line:
            return {}

        default = {}
        default['line'] = self.process.line.id
        default['fraction'] = self.process.line.notebook.fraction.id
        default['repetition'] = self.process.line.repetition
        default['product_type'] = (
            self.process.line.notebook.product_type.id)
        default['matrix'] = self.process.line.notebook.matrix.id

        if (hasattr(self.process, 'formula') and
                getattr(self.process, 'formula')):
            formula = self.process.formula
            default['formula'] = formula.id
            variables = []
            variables_desc = {}
            for var in self.process.variables:
                variables.append({
                    'number': var.number,
                    'description': var.description,
                    'value': var.value,
                    })
                variables_desc[var.number] = var.description
            default['variables'] = variables
            formula_formula = formula.formula
            for k, v in variables_desc.items():
                formula_formula = formula_formula.replace(k, v)
            default['formula_formula'] = formula_formula

            default['result'] = self.process.result
            default['result_modifier'] = (self.process.result_modifier.id if
                self.process.result_modifier else None)

            default['initial_concentration'] = (
                self.process.initial_concentration)
            default['comments'] = self.process.comments
            default['professional'] = self.process.professional.id
            default['end_date'] = self.process.end_date
            default['chromatogram'] = self.process.chromatogram

        else:
            formula = self.process.line.analysis.formula
            if formula:
                default['formula'] = formula.id
                variables = []
                variables_desc = {}
                for var in formula.variables:
                    variables.append({
                        'number': var.number,
                        'description': var.description,
                        'value': var.constant,
                        })
                    variables_desc[var.number] = var.description
                default['variables'] = variables
                formula_formula = formula.formula
                for k, v in variables_desc.items():
                    formula_formula = formula_formula.replace(k, v)
                default['formula_formula'] = formula_formula
            default['result_modifier'] = None

            for field in ('initial_concentration', 'comments'):
                if (hasattr(self.process, field + '_copy') and
                        getattr(self.process, field + '_copy')):
                    default[field] = getattr(self.process, field)
                    default[field + '_copy'] = getattr(self.process,
                        field + '_copy')
                else:
                    default[field] = getattr(self.process.line, field)
            for field in ('professional',):
                if (hasattr(self.process, field) and
                        getattr(self.process, field)):
                    default[field] = getattr(self.process, field).id
            for field in ('end_date', 'chromatogram'):
                if (hasattr(self.process, field) and
                        getattr(self.process, field)):
                    default[field] = getattr(self.process, field)
            for field in ('end_date_copy', 'chromatogram_copy'):
                if (hasattr(self.process, field) and
                        getattr(self.process, field)):
                    default[field] = getattr(self.process, field)

        return default

    def transition_check_professional(self):
        pool = Pool()
        NotebookLoadResultsFormulaAction = pool.get(
            'lims.notebook.load_results_formula.action')
        LabProfessionalMethod = pool.get('lims.lab.professional.method')
        LaboratoryProfessional = pool.get('lims.laboratory.professional')
        LabMethod = pool.get('lims.lab.method')
        NotebookLoadResultsFormulaSit2Detail = pool.get(
            'lims.notebook.load_results_formula.sit2.detail')

        actions = NotebookLoadResultsFormulaAction.search([
            ('session_id', '=', self._session_id),
            ])

        situations = {}
        prof_lines = {}
        for data in actions:
            key = (data.professional.id, data.line.method.id)
            if key not in situations:
                situations[key] = 0
            if key not in prof_lines:
                prof_lines[key] = []
            prof_lines[key].append(data.line.id)

        situation_1 = []
        for key in situations.keys():
            qualifications = LabProfessionalMethod.search([
                ('professional', '=', key[0]),
                ('method', '=', key[1]),
                ('type', '=', 'analytical'),
                ])
            if not qualifications:
                situations[key] = 1
                situation_1.append(key)
            elif qualifications[0].state == 'training':
                situations[key] = 2
            elif (qualifications[0].state in ('qualified', 'requalified')):
                situations[key] = 3
        if situation_1:
            msg = ''
            for key in situation_1:
                professional = LaboratoryProfessional(key[0])
                method = LabMethod(key[1])
                msg += '%s: %s\n' % (professional.rec_name, method.code)
            self.sit1.msg = msg
            return 'sit1'

        situation_2 = []
        for key, sit in situations.items():
            if sit == 2:
                situation_2.append({
                    'session_id': self._session_id,
                    'professional': key[0],
                    'method': key[1],
                    'lines': [('add', prof_lines[key])],
                    })
        if situation_2:
            details = NotebookLoadResultsFormulaSit2Detail.create(
                situation_2)
            self.sit2.details = details
            return 'sit2'

        return 'confirm_'

    def default_sit1(self, fields):
        defaults = {}
        if self.sit1.msg:
            defaults['msg'] = self.sit1.msg
        return defaults

    def default_sit2(self, fields):
        defaults = {}
        if self.sit2.details:
            defaults['details'] = [d.id for d in self.sit2.details]
        return defaults

    def transition_sit2_ok(self):
        for detail in self.sit2.details:
            if not detail.supervisor:
                return 'sit2'
        return 'confirm_'

    def transition_confirm_(self):
        pool = Pool()
        NotebookLoadResultsFormulaAction = pool.get(
            'lims.notebook.load_results_formula.action')
        NotebookLine = pool.get('lims.notebook.line')
        LabProfessionalMethod = pool.get('lims.lab.professional.method')
        LabProfessionalMethodRequalification = pool.get(
            'lims.lab.professional.method.requalification')
        Date = pool.get('ir.date')

        # Write Results to Notebook lines
        actions = NotebookLoadResultsFormulaAction.search([
            ('session_id', '=', self._session_id),
            ])
        for data in actions:
            notebook_line = NotebookLine(data.line.id)
            if not notebook_line:
                continue
            notebook_line_write = {
                'result': data.result,
                'result_modifier': (data.result_modifier.id if
                    data.result_modifier else None),
                'end_date': data.end_date,
                'chromatogram': data.chromatogram,
                'initial_concentration': data.initial_concentration,
                'comments': data.comments,
                'converted_result': None,
                'converted_result_modifier': None,
                'backup': None,
                'verification': None,
                'uncertainty': None,
                }
            if data.result_modifier and data.result_modifier.code == 'na':
                notebook_line_write['annulled'] = True
                notebook_line_write['annulment_date'] = datetime.now()
                notebook_line_write['report'] = False
            professionals = [{'professional': data.professional.id}]
            notebook_line_write['professionals'] = (
                [('delete', [p.id for p in notebook_line.professionals])] +
                [('create', professionals)])
            NotebookLine.write([notebook_line], notebook_line_write)

        # Write Supervisors to Notebook lines
        supervisor_lines = {}
        if hasattr(self.sit2, 'details'):
            for detail in self.sit2.details:
                if detail.supervisor.id not in supervisor_lines:
                    supervisor_lines[detail.supervisor.id] = []
                supervisor_lines[detail.supervisor.id].extend([
                    l.id for l in detail.lines])
        for prof_id, lines in supervisor_lines.items():
            notebook_lines = NotebookLine.search([
                ('id', 'in', lines),
                ])
            if notebook_lines:
                professionals = [{'professional': prof_id}]
                notebook_line_write = {
                    'professionals': [('create', professionals)],
                    }
                NotebookLine.write(notebook_lines, notebook_line_write)

        # Write the execution of method
        all_prof = {}
        for data in actions:
            key = (data.professional.id, data.line.method.id)
            if key not in all_prof:
                all_prof[key] = []
        if hasattr(self.sit2, 'details'):
            for detail in self.sit2.details:
                key = (detail.supervisor.id, detail.method.id)
                if key not in all_prof:
                    all_prof[key] = []
                key = (detail.professional.id, detail.method.id)
                if detail.supervisor.id not in all_prof[key]:
                    all_prof[key].append(detail.supervisor.id)

        today = Date.today()
        for key, sup in all_prof.items():
            professional_method, = LabProfessionalMethod.search([
                ('professional', '=', key[0]),
                ('method', '=', key[1]),
                ('type', '=', 'analytical'),
                ])
            if professional_method.state == 'training':
                history = LabProfessionalMethodRequalification.search([
                    ('professional_method', '=', professional_method.id),
                    ('type', '=', 'training'),
                    ])
                if history:
                    prev_supervisors = [s.supervisor.id for s in
                        history[0].supervisors]
                    supervisors = [{'supervisor': s} for s in sup
                        if s not in prev_supervisors]
                    LabProfessionalMethodRequalification.write(history, {
                        'last_execution_date': today,
                        'supervisors': [('create', supervisors)],
                        })
                else:
                    supervisors = [{'supervisor': s} for s in sup]
                    to_create = [{
                        'professional_method': professional_method.id,
                        'type': 'training',
                        'date': today,
                        'last_execution_date': today,
                        'supervisors': [('create', supervisors)],
                        }]
                    LabProfessionalMethodRequalification.create(to_create)

            elif professional_method.state == 'qualified':
                history = LabProfessionalMethodRequalification.search([
                    ('professional_method', '=', professional_method.id),
                    ('type', '=', 'qualification'),
                    ])
                if history:
                    LabProfessionalMethodRequalification.write(history, {
                        'last_execution_date': today,
                        })
                else:
                    to_create = [{
                        'professional_method': professional_method.id,
                        'type': 'qualification',
                        'date': today,
                        'last_execution_date': today,
                        }]
                    LabProfessionalMethodRequalification.create(to_create)

            else:
                history = LabProfessionalMethodRequalification.search([
                    ('professional_method', '=', professional_method.id),
                    ('type', '=', 'requalification'),
                    ])
                if history:
                    LabProfessionalMethodRequalification.write(history, {
                        'last_execution_date': today,
                        })
                else:
                    to_create = [{
                        'professional_method': professional_method.id,
                        'type': 'requalification',
                        'date': today,
                        'last_execution_date': today,
                        }]
                    LabProfessionalMethodRequalification.create(to_create)

        return 'end'


class NotebookLoadResultsManualStart(ModelView):
    'Load Results Manually'
    __name__ = 'lims.notebook.load_results_manual.start'

    method = fields.Many2One('lims.lab.method', 'Method', required=True)
    start_date = fields.Date('Start date', required=True)


class NotebookLoadResultsManualEmpty(ModelView):
    'Load Results Manually'
    __name__ = 'lims.notebook.load_results_manual.empty'


class NotebookLoadResultsManualResult(ModelView):
    'Load Results Manually'
    __name__ = 'lims.notebook.load_results_manual.result'

    method = fields.Many2One('lims.lab.method', 'Method', readonly=True)
    start_date = fields.Date('Start date', readonly=True)
    professional = fields.Many2One('lims.laboratory.professional',
        'Laboratory professional', required=True)
    lines = fields.One2Many('lims.notebook.load_results_manual.line',
        None, 'Lines')


class NotebookLoadResultsManualLine(ModelSQL, ModelView):
    'Load Results Manually'
    __name__ = 'lims.notebook.load_results_manual.line'

    line = fields.Many2One('lims.notebook.line', 'Analysis', readonly=True,
        ondelete='CASCADE')
    repetition = fields.Integer('Repetition', readonly=True)
    fraction = fields.Many2One('lims.fraction', 'Fraction', readonly=True)
    result = fields.Char('Result')
    result_modifier = fields.Many2One('lims.result_modifier',
        'Result modifier')
    end_date = fields.Date('End date')
    chromatogram = fields.Char('Chromatogram')
    initial_unit = fields.Many2One('product.uom', 'Initial unit',
        domain=[('category.lims_only_available', '=', True)])
    comments = fields.Text('Comments')
    literal_result = fields.Char('Literal result')
    fraction_type = fields.Many2One('lims.fraction.type', 'Fraction type',
        readonly=True)
    device = fields.Many2One('lims.lab.device', 'Device',
        domain=[('id', 'in', Eval('device_domain'))],
        depends=['device_domain'])
    device_domain = fields.Function(fields.Many2Many('lims.lab.device',
        None, None, 'Device domain'), 'on_change_with_device_domain')
    uncertainty = fields.Char('Uncertainty')
    session_id = fields.Integer('Session ID')

    @classmethod
    def __register__(cls, module_name):
        cursor = Transaction().connection.cursor()
        table_h = cls.__table_handler__(module_name)
        migrate_result_modifier = (
            table_h.column_exist('result_modifier') and
            table_h.column_is_type('result_modifier', 'VARCHAR'))
        if migrate_result_modifier:
            table_h.column_rename('result_modifier',
                'temp_result_modifier')
        super().__register__(module_name)
        if migrate_result_modifier:
            table_h.drop_column('temp_result_modifier')
        cursor.execute('DELETE FROM "' + cls._table + '"')

    @fields.depends('result', 'literal_result', 'result_modifier', 'end_date')
    def on_change_with_end_date(self):
        pool = Pool()
        Date = pool.get('ir.date')
        if self.end_date:
            return self.end_date
        if (self.result or self.literal_result or
                (self.result_modifier and self.result_modifier.code != 'low')):
            return Date.today()
        return None

    @fields.depends('line')
    def on_change_with_device_domain(self, name=None):
        cursor = Transaction().connection.cursor()
        AnalysisDevice = Pool().get('lims.analysis.device')

        cursor.execute('SELECT DISTINCT(device) '
            'FROM "' + AnalysisDevice._table + '" '
            'WHERE active IS TRUE '
                'AND analysis = %s  '
                'AND laboratory = %s',
            (self.line.analysis.id, self.line.laboratory.id))
        res = cursor.fetchall()
        if not res:
            return []
        return [x[0] for x in res]


class NotebookLoadResultsManualSit1(ModelView):
    'Professionals Control'
    __name__ = 'lims.notebook.load_results_manual.sit1'

    msg = fields.Text('Message')


class NotebookLoadResultsManualSit2(ModelView):
    'Professionals Control'
    __name__ = 'lims.notebook.load_results_manual.sit2'

    supervisor = fields.Many2One('lims.laboratory.professional',
        'Supervisor', depends=['supervisor_domain'],
        domain=[('id', 'in', Eval('supervisor_domain'))], required=True)
    supervisor_domain = fields.Many2Many('lims.laboratory.professional',
        None, None, 'Supervisor domain')
    lines = fields.Many2Many('lims.notebook.line', None, None, 'Lines')


class NotebookLoadResultsManual(Wizard):
    'Load Results Manually'
    __name__ = 'lims.notebook.load_results_manual'

    start = StateView('lims.notebook.load_results_manual.start',
        'lims.lims_notebook_load_results_manual_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Search', 'search', 'tryton-forward', default=True),
            ])
    search = StateTransition()
    empty = StateView('lims.notebook.load_results_manual.empty',
        'lims.lims_notebook_load_results_manual_empty_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Search again', 'start', 'tryton-forward', default=True),
            ])
    result = StateView('lims.notebook.load_results_manual.result',
        'lims.lims_notebook_load_results_manual_result_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Confirm', 'check_professional', 'tryton-ok', default=True),
            ])
    check_professional = StateTransition()
    sit1 = StateView('lims.notebook.load_results_manual.sit1',
        'lims.lims_notebook_load_results_manual_sit1_view_form', [
            Button('Cancel', 'end', 'tryton-cancel', default=True),
            ])
    sit2 = StateView('lims.notebook.load_results_manual.sit2',
        'lims.lims_notebook_load_results_manual_sit2_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Confirm', 'confirm_', 'tryton-ok', default=True),
            ])
    confirm_ = StateTransition()

    def transition_search(self):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        LaboratoryProfessional = pool.get('lims.laboratory.professional')
        NotebookLoadResultsManualLine = pool.get(
            'lims.notebook.load_results_manual.line')

        clause = [
            ('end_date', '=', None),
            ('method', '=', self.start.method.id),
            ('start_date', '=', self.start.start_date),
            ]

        with Transaction().set_context(_check_access=True):
            lines = NotebookLine.search(clause,
                order=[('analysis_order', 'ASC'), ('id', 'ASC')])
        if lines:
            res_lines = []
            for line in lines:
                res_line, = NotebookLoadResultsManualLine.create([{
                    'session_id': self._session_id,
                    'line': line.id,
                    'repetition': line.repetition,
                    'result': line.result,
                    'result_modifier': (line.result_modifier.id if
                        line.result_modifier else None),
                    'end_date': line.end_date,
                    'chromatogram': line.chromatogram,
                    'initial_unit': (line.initial_unit.id if
                        line.initial_unit else None),
                    'comments': line.comments,
                    'fraction': line.fraction.id,
                    'fraction_type': line.fraction_type.id,
                    'literal_result': line.literal_result,
                    'device': (line.device.id if
                        line.device else None),
                    'uncertainty': line.uncertainty,
                    }])
                res_lines.append(res_line.id)

            professional_id = LaboratoryProfessional.get_lab_professional()
            self.result.method = self.start.method.id
            self.result.start_date = self.start.start_date
            self.result.professional = professional_id
            self.result.lines = res_lines
            return 'result'
        return 'empty'

    def default_result(self, fields):
        default = {}
        default['method'] = self.result.method.id
        default['start_date'] = self.result.start_date
        default['professional'] = (self.result.professional.id
            if self.result.professional else None)
        default['lines'] = [l.id for l in self.result.lines]
        return default

    def transition_check_professional(self):
        pool = Pool()
        NotebookLoadResultsManualLine = pool.get(
            'lims.notebook.load_results_manual.line')
        LabProfessionalMethod = pool.get('lims.lab.professional.method')

        lines_to_save = []
        for line in self.result.lines:
            if not line.line:  # Avoid empty lines created with ENTER key
                continue
            lines_to_save.append(line)
        NotebookLoadResultsManualLine.save(lines_to_save)

        professional = self.result.professional
        method = self.result.method

        qualifications = LabProfessionalMethod.search([
            ('professional', '=', professional.id),
            ('method', '=', method.id),
            ('type', '=', 'analytical'),
            ])
        if not qualifications:
            msg = '%s: %s' % (professional.rec_name, method.code)
            self.sit1.msg = msg
            return 'sit1'
        elif qualifications[0].state == 'training':
            situation_2_lines = [l.line.id for l in lines_to_save]
            supervisor_domain = []
            qualifications = LabProfessionalMethod.search([
                ('method', '=', method.id),
                ('type', '=', 'analytical'),
                ('state', 'in', ('qualified', 'requalified')),
                ])
            if qualifications:
                supervisor_domain = [q.professional.id for q in qualifications]

            self.sit2.lines = situation_2_lines
            self.sit2.supervisor_domain = supervisor_domain
            return 'sit2'

        return 'confirm_'

    def default_sit1(self, fields):
        defaults = {}
        if self.sit1.msg:
            defaults['msg'] = self.sit1.msg
        return defaults

    def default_sit2(self, fields):
        defaults = {}
        if self.sit2.supervisor_domain:
            defaults['supervisor_domain'] = [p.id for p in
                self.sit2.supervisor_domain]
        if self.sit2.lines:
            defaults['lines'] = [l.id for l in self.sit2.lines]
        return defaults

    def transition_confirm_(self):
        pool = Pool()
        NotebookLoadResultsManualLine = pool.get(
            'lims.notebook.load_results_manual.line')
        NotebookLine = pool.get('lims.notebook.line')
        LabProfessionalMethod = pool.get('lims.lab.professional.method')
        LabProfessionalMethodRequalification = pool.get(
            'lims.lab.professional.method.requalification')
        Date = pool.get('ir.date')

        # Write Results to Notebook lines
        actions = NotebookLoadResultsManualLine.search([
            ('session_id', '=', self._session_id),
            ])

        for data in actions:
            notebook_line = NotebookLine(data.line.id)
            if not notebook_line:
                continue
            notebook_line_write = {
                'result': data.result,
                'result_modifier': (data.result_modifier.id if
                    data.result_modifier else None),
                'chromatogram': data.chromatogram,
                'initial_unit': (data.initial_unit.id if
                    data.initial_unit else None),
                'comments': data.comments,
                'literal_result': data.literal_result,
                'converted_result': None,
                'converted_result_modifier': None,
                'backup': None,
                'verification': None,
                'uncertainty': data.uncertainty,
                'device': (data.device.id if
                    data.device else None),
                }
            if (not (not data.result_modifier and not data.result) or
                    data.literal_result):
                notebook_line_write['end_date'] = data.end_date
            if data.result_modifier and data.result_modifier.code == 'na':
                notebook_line_write['annulled'] = True
                notebook_line_write['annulment_date'] = datetime.now()
                notebook_line_write['report'] = False
            if (notebook_line_write.get('end_date') or
                    notebook_line_write.get('annulment_date')):
                professionals = [{'professional': self.result.professional.id}]
                notebook_line_write['professionals'] = (
                    [('delete', [p.id for p in notebook_line.professionals])] +
                    [('create', professionals)])
            NotebookLine.write([notebook_line], notebook_line_write)

        # Write Supervisors to Notebook lines
        supervisor_lines = {}
        if hasattr(self.sit2, 'supervisor'):
            supervisor_lines[self.sit2.supervisor.id] = [
                l.id for l in self.sit2.lines]
        for prof_id, lines in supervisor_lines.items():
            notebook_lines = NotebookLine.search([
                ('id', 'in', lines),
                ])
            if notebook_lines:
                professionals = [{'professional': prof_id}]
                notebook_line_write = {
                    'professionals': [('create', professionals)],
                    }
                NotebookLine.write(notebook_lines, notebook_line_write)

        # Write the execution of method
        all_prof = {}
        key = (self.result.professional.id, self.result.method.id)
        all_prof[key] = []
        if hasattr(self.sit2, 'supervisor'):
            for detail in self.sit2.lines:
                key = (self.sit2.supervisor.id, detail.method.id)
                if key not in all_prof:
                    all_prof[key] = []
                key = (self.result.professional.id, detail.method.id)
                if self.sit2.supervisor.id not in all_prof[key]:
                    all_prof[key].append(self.sit2.supervisor.id)

        today = Date.today()
        for key, sup in all_prof.items():
            professional_method, = LabProfessionalMethod.search([
                ('professional', '=', key[0]),
                ('method', '=', key[1]),
                ('type', '=', 'analytical'),
                ])
            if professional_method.state == 'training':
                history = LabProfessionalMethodRequalification.search([
                    ('professional_method', '=', professional_method.id),
                    ('type', '=', 'training'),
                    ])
                if history:
                    prev_supervisors = [s.supervisor.id for s in
                        history[0].supervisors]
                    supervisors = [{'supervisor': s} for s in sup
                        if s not in prev_supervisors]
                    LabProfessionalMethodRequalification.write(history, {
                        'last_execution_date': today,
                        'supervisors': [('create', supervisors)],
                        })
                else:
                    supervisors = [{'supervisor': s} for s in sup]
                    to_create = [{
                        'professional_method': professional_method.id,
                        'type': 'training',
                        'date': today,
                        'last_execution_date': today,
                        'supervisors': [('create', supervisors)],
                        }]
                    LabProfessionalMethodRequalification.create(to_create)

            elif professional_method.state == 'qualified':
                history = LabProfessionalMethodRequalification.search([
                    ('professional_method', '=', professional_method.id),
                    ('type', '=', 'qualification'),
                    ])
                if history:
                    LabProfessionalMethodRequalification.write(history, {
                        'last_execution_date': today,
                        })
                else:
                    to_create = [{
                        'professional_method': professional_method.id,
                        'type': 'qualification',
                        'date': today,
                        'last_execution_date': today,
                        }]
                    LabProfessionalMethodRequalification.create(to_create)

            else:
                history = LabProfessionalMethodRequalification.search([
                    ('professional_method', '=', professional_method.id),
                    ('type', '=', 'requalification'),
                    ])
                if history:
                    LabProfessionalMethodRequalification.write(history, {
                        'last_execution_date': today,
                        })
                else:
                    to_create = [{
                        'professional_method': professional_method.id,
                        'type': 'requalification',
                        'date': today,
                        'last_execution_date': today,
                        }]
                    LabProfessionalMethodRequalification.create(to_create)

        return 'end'


class NotebookLoadResultsExceptionalStart(ModelView):
    'Exceptional Loading of Results'
    __name__ = 'lims.notebook.load_results_exceptional.start'

    notebook = fields.Many2One('lims.notebook', 'Notebook', required=True)


class NotebookLoadResultsExceptionalResult(ModelView):
    'Exceptional Loading of Results'
    __name__ = 'lims.notebook.load_results_exceptional.result'

    start_date = fields.Date('Start date', required=True)
    end_date = fields.Date('End date', required=True)
    lines = fields.One2Many('lims.notebook.load_results_exceptional.line',
        None, 'Lines')


class NotebookLoadResultsExceptionalLine(ModelSQL, ModelView):
    'Exceptional Loading of Results'
    __name__ = 'lims.notebook.load_results_exceptional.line'

    line = fields.Many2One('lims.notebook.line', 'Analysis', readonly=True)
    result = fields.Char('Result')
    literal_result = fields.Char('Literal result')


class NotebookLoadResultsExceptional(Wizard):
    'Exceptional Loading of Results'
    __name__ = 'lims.notebook.load_results_exceptional'

    start = StateView('lims.notebook.load_results_exceptional.start',
        'lims.lims_notebook_load_results_exceptional_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Load results', 'result', 'tryton-forward', default=True),
            ])
    result = StateView('lims.notebook.load_results_exceptional.result',
        'lims.lims_notebook_load_results_exceptional_result_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Confirm', 'confirm', 'tryton-ok', default=True),
            ])
    confirm = StateTransition()

    def default_result(self, fields):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        Date = pool.get('ir.date')

        today = Date.today()
        res_lines = []
        with Transaction().set_context(_check_access=True):
            lines = NotebookLine.search([
                ('notebook', '=', self.start.notebook),
                ('start_date', '=', None),
                ],
                order=[('analysis_order', 'ASC'), ('id', 'ASC')])
        for line in lines:
            res_lines.append({
                'line': line.id,
                'result': None,
                'literal_result': None,
                })

        default = {}
        default['start_date'] = today
        default['end_date'] = today
        default['lines'] = res_lines
        return default

    def transition_confirm(self):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')

        start_date = self.result.start_date
        end_date = self.result.end_date

        lines_to_save = []
        for line in self.result.lines:
            if not line.line:  # Avoid empty lines created with ENTER key
                continue
            if not line.result and not line.literal_result:
                continue
            notebook_line = NotebookLine(line.line.id)
            notebook_line.result = line.result
            notebook_line.literal_result = line.literal_result
            notebook_line.start_date = start_date
            notebook_line.end_date = end_date
            notebook_line.exceptional_load = True
            notebook_line.exceptional_load_uid = int(Transaction().user)
            lines_to_save.append(notebook_line)
        NotebookLine.save(lines_to_save)
        return 'end'


class NotebookAddInternalRelationsStart(ModelView):
    'Add Internal Relations'
    __name__ = 'lims.notebook.add_internal_relations.start'

    analysis = fields.Many2Many('lims.analysis', None, None,
        'Internal relations', required=True,
        domain=[('id', 'in', Eval('analysis_domain'))],
        depends=['analysis_domain'])
    analysis_domain = fields.One2Many('lims.analysis', None,
        'Internal relations domain')


class NotebookAddInternalRelations(Wizard):
    'Add Internal Relations'
    __name__ = 'lims.notebook.add_internal_relations'

    start = StateView('lims.notebook.add_internal_relations.start',
        'lims.lims_notebook_add_internal_relations_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Add', 'add', 'tryton-ok', default=True),
            ])
    add = StateTransition()

    def default_start(self, fields):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        Notebook = pool.get('lims.notebook')
        NotebookLine = pool.get('lims.notebook.line')
        Analysis = pool.get('lims.analysis')
        Typification = pool.get('lims.typification')

        default = {
            'analysis_domain': [],
            }

        notebook = Notebook(Transaction().context['active_id'])
        with Transaction().set_context(_check_access=True):
            notebook_lines = NotebookLine.search([
                ('notebook', '=', notebook.id),
                ])
        if not notebook_lines:
            return default

        notebook_analysis = []
        for notebook_line in notebook_lines:
            notebook_analysis.append(notebook_line.analysis.code)
        notebook_analysis_codes = '\', \''.join(str(a) for a
            in notebook_analysis)

        cursor.execute('SELECT DISTINCT(a.id, a.result_formula) '
            'FROM "' + Analysis._table + '" a '
                'INNER JOIN "' + Typification._table + '" t '
                'ON a.id = t.analysis '
            'WHERE t.product_type = %s '
                'AND t.matrix = %s '
                'AND t.valid '
                'AND a.behavior = \'internal_relation\' '
                'AND a.code NOT IN (\'' + notebook_analysis_codes + '\')',
            (notebook.fraction.product_type.id,
            notebook.fraction.matrix.id))
        internal_relations = cursor.fetchall()
        if not internal_relations:
            return default

        for internal_relation in internal_relations:
            formula = internal_relation[0].split(',')[1][:-1]
            if not formula:
                continue
            for i in (' ', '\t', '\n', '\r'):
                formula = formula.replace(i, '')
            variables = self._get_variables(formula)

            available = True
            if variables:
                for v in variables:
                    if v not in notebook_analysis:
                        available = False
                        break
            if available:
                ir_id = int(internal_relation[0].split(',')[0][1:])
                default['analysis_domain'].append(ir_id)

        return default

    def _get_variables(self, formula):
        variables = []
        for variable in re.findall(r'\{.*?\}', formula):
            var = variable.replace('{', '').replace('}', '')
            for prefix in ('A', 'D', 'T', 'Y', 'R'):
                if var.startswith(prefix):
                    variables.append(var[1:])
        return variables

    def transition_add(self):
        Notebook = Pool().get('lims.notebook')
        notebook = Notebook(Transaction().context['active_id'])
        for analysis in self.start.analysis:
            self._create_service(analysis, notebook.fraction)
        return 'end'

    def _create_service(self, analysis, fraction):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        Typification = pool.get('lims.typification')
        Service = pool.get('lims.service')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')

        divide, report_grouper = self._get_report_grouper(analysis)

        laboratory_id = None
        cursor.execute('SELECT laboratory '
            'FROM "' + Typification._table + '" '
            'WHERE product_type = %s '
                'AND matrix = %s '
                'AND analysis = %s '
                'AND valid IS TRUE '
                'AND by_default IS TRUE '
                'AND laboratory IS NOT NULL',
            (fraction.product_type.id, fraction.matrix.id, analysis.id))
        res = cursor.fetchone()
        if res:
            laboratory_id = res[0]
        if not laboratory_id and analysis.laboratories:
            for l in analysis.laboratories:
                if l.by_default is True:
                    laboratory_id = l.laboratory.id

        method_id = None
        cursor.execute('SELECT method '
            'FROM "' + Typification._table + '" '
            'WHERE product_type = %s '
                'AND matrix = %s '
                'AND analysis = %s '
                'AND valid IS TRUE '
                'AND by_default IS TRUE',
            (fraction.product_type.id, fraction.matrix.id, analysis.id))
        res = cursor.fetchone()
        if res:
            method_id = res[0]

        device_id = None
        if analysis.devices:
            for d in analysis.devices:
                if (d.laboratory.id == laboratory_id and
                        d.by_default is True):
                    device_id = d.device.id

        service_create = [{
            'fraction': fraction.id,
            'analysis': analysis.id,
            'laboratory': laboratory_id,
            'method': method_id,
            'device': device_id,
            'divide': divide,
            }]
        new_service, = Service.create(service_create)
        Service.set_confirmation_date([new_service])
        analysis_detail = list(new_service.analysis_detail)
        if report_grouper != 0:
            EntryDetailAnalysis.write(analysis_detail, {
                'report_grouper': report_grouper,
                })

        EntryDetailAnalysis.create_notebook_lines(analysis_detail,
            fraction)
        EntryDetailAnalysis.write(analysis_detail, {
            'state': 'unplanned',
            })

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

        divide = False
        report_grouper = 0

        notebook = Notebook(Transaction().context['active_id'])
        notebook_analysis = {}
        for notebook_line in notebook.lines:
            notebook_analysis[notebook_line.analysis.code] = notebook_line

        formula = analysis.result_formula
        for i in (' ', '\t', '\n', '\r'):
            formula = formula.replace(i, '')
        variables = self._get_variables(formula)
        for v in variables:
            if v in notebook_analysis:
                divide = notebook_analysis[v].service.divide
                report_grouper = (
                    notebook_analysis[v].analysis_detail.report_grouper)
                break
        return divide, report_grouper

    def end(self):
        return 'reload'


class NotebookRepeatAnalysisStart(ModelView):
    'Repeat Analysis'
    __name__ = 'lims.notebook.repeat_analysis.start'

    analysis = fields.Many2Many('lims.analysis', None, None,
        'Analysis', required=True,
        domain=[('id', 'in', Eval('analysis_domain'))],
        depends=['analysis_domain'])
    analysis_domain = fields.One2Many('lims.analysis', None,
        'Analysis domain')
    repetition_reason = fields.Char('Reason')


class NotebookRepeatAnalysis(Wizard):
    'Repeat Analysis'
    __name__ = 'lims.notebook.repeat_analysis'

    start = StateView('lims.notebook.repeat_analysis.start',
        'lims.lims_notebook_repeat_analysis_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Repeat', 'repeat', 'tryton-ok', default=True),
            ])
    repeat = StateTransition()

    @classmethod
    def check_access(cls):
        pass

    def default_start(self, fields):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        Analysis = pool.get('lims.analysis')

        analysis_domain = set()
        first = True
        for notebook_id in Transaction().context['active_ids']:
            analysis_origin_list = []
            with Transaction().set_context(_check_access=True):
                notebook_lines = NotebookLine.search([
                    ('notebook', '=', notebook_id),
                    ])
            for nl in notebook_lines:
                analysis_origin_list.extend(nl.analysis_origin.split(' > '))
                analysis_origin_list.append(nl.analysis.code)
            analysis = Analysis.search([
                ('code', 'in', analysis_origin_list),
                ('behavior', '=', 'normal'),
                ])
            notebook_analysis = set(a.id for a in analysis)
            if first:
                analysis_domain = notebook_analysis
                first = False
                continue
            analysis_domain = analysis_domain.intersection(
                notebook_analysis)

        default = {
            'analysis_domain': list(analysis_domain),
            }
        return default

    def _unaccept_original(self):
        return True

    def transition_repeat(self):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        Config = pool.get('lims.configuration')

        config = Config(1)

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

        for notebook in Notebook.browse(Transaction().context['active_ids']):
            rm_type = (notebook.fraction.special_type == 'rm')
            if rm_type:
                rm_start_uom = (config.rm_start_uom.id if config.rm_start_uom
                    else None)

            to_create = []
            to_update = []
            details_to_update = []
            for analysis_id in analysis_to_repeat:
                clause = [
                    ('notebook', '=', notebook.id),
                    ('analysis', '=', analysis_id),
                    ('analysis.behavior', '=', 'normal'),
                    ]
                nlines = NotebookLine.search(clause,
                    order=[('repetition', 'DESC')], limit=1)
                if not nlines:
                    continue
                nline_to_repeat = nlines[0]

                defaults = self._get_repetition_defaults(nline_to_repeat)
                if rm_type:
                    defaults['final_concentration'] = None
                    defaults['literal_final_concentration'] = None
                    defaults['initial_unit'] = rm_start_uom
                    defaults['final_unit'] = None
                    defaults['detection_limit'] = None
                    defaults['quantification_limit'] = None
                    defaults['lower_limit'] = None
                    defaults['upper_limit'] = None
                to_create.append(defaults)
                if not nline_to_repeat.results_report:
                    to_update.append(nline_to_repeat)
                details_to_update.append(nline_to_repeat.analysis_detail.id)

            Notebook.write([notebook], {
                'lines': [('create', to_create)],
                })
            if self._unaccept_original():
                NotebookLine.write(to_update, {
                    'accepted': False,
                    'acceptance_date': None,
                    'report': False,
                    })

            details = EntryDetailAnalysis.search([
                ('id', 'in', details_to_update),
                ])
            if details:
                EntryDetailAnalysis.write(details, {
                    'state': 'unplanned',
                    })

        return 'end'

    def _get_repetition_defaults(self, line):
        defaults = {
            'analysis_detail': line.analysis_detail.id,
            'service': line.service.id,
            'analysis': line.analysis.id,
            'analysis_origin': line.analysis_origin,
            'urgent': line.urgent,
            'repetition': line.repetition + 1,
            'laboratory': line.laboratory.id,
            'method': line.method.id,
            'device': line.device.id if line.device else None,
            'decimals': line.decimals,
            'significant_digits': line.significant_digits,
            'scientific_notation': line.scientific_notation,
            'report': line.report,
            'results_estimated_waiting': (
                line.results_estimated_waiting),
            'department': line.department,
            'concentration_level': (line.concentration_level.id
                if line.concentration_level else None),
            'initial_concentration': line.initial_concentration,
            'final_concentration': line.final_concentration,
            'literal_final_concentration': line.literal_final_concentration,
            'initial_unit': (line.initial_unit.id
                if line.initial_unit else None),
            'final_unit': line.final_unit.id if line.final_unit else None,
            'detection_limit': line.detection_limit,
            'quantification_limit': line.quantification_limit,
            'lower_limit': line.lower_limit,
            'upper_limit': line.upper_limit,
            }
        defaults['repetition_reason'] = self.start.repetition_reason
        if line.results_report:
            defaults['report'] = False
        return defaults


class NotebookLineRepeatAnalysisStart(ModelView):
    'Repeat Analysis'
    __name__ = 'lims.notebook.line.repeat_analysis.start'

    analysis = fields.Many2One('lims.analysis', 'Analysis', required=True,
        domain=[('id', 'in', Eval('analysis_domain'))],
        depends=['analysis_domain'])
    analysis_domain = fields.One2Many('lims.analysis', None,
        'Analysis domain')
    repetition_reason = fields.Char('Reason')
    analysis_type = fields.Function(fields.Selection([
        (None, ''),
        ('analysis', 'Analysis'),
        ('set', 'Set'),
        ('group', 'Group'),
        ], 'Type', sort=False),
        'on_change_with_analysis_type')
    method = fields.Many2One('lims.lab.method', 'Method',
        domain=[('id', 'in', Eval('method_domain'))],
        states={'invisible': Eval('analysis_type') != 'analysis'},
        depends=['method_domain', 'analysis_type'])
    method_domain = fields.Function(fields.Many2Many('lims.lab.method',
        None, None, 'Method domain'),
        'on_change_with_method_domain')

    @fields.depends('analysis')
    def on_change_with_analysis_type(self, name=None):
        return self.analysis and self.analysis.type or None

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


class NotebookLineRepeatAnalysis(Wizard):
    'Repeat Analysis'
    __name__ = 'lims.notebook.line.repeat_analysis'

    start = StateView('lims.notebook.line.repeat_analysis.start',
        'lims.lims_notebook_line_repeat_analysis_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Repeat', 'repeat', 'tryton-ok', default=True),
            ])
    repeat = StateTransition()

    @classmethod
    def check_access(cls):
        pass

    def _get_notebook_line_id(self):
        return Transaction().context.get('active_id', None)

    def default_start(self, fields):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        Analysis = pool.get('lims.analysis')

        line_id = self._get_notebook_line_id()
        if not line_id:
            return {}
        notebook_line = NotebookLine(line_id)

        analysis_origin = notebook_line.analysis_origin
        analysis_origin_list = analysis_origin.split(' > ')

        analysis_code = notebook_line.analysis.code
        analysis_origin_list.append(analysis_code)

        analysis = Analysis.search([
            ('code', 'in', analysis_origin_list),
            ('behavior', '=', 'normal'),
            ])
        notebook_analysis = [a.id for a in analysis]
        default = {
            'analysis_domain': notebook_analysis,
            }
        if len(notebook_analysis) == 1:
            default['analysis'] = notebook_line.analysis.id
            default['method'] = notebook_line.method.id
        return default

    def _unaccept_original(self):
        return True

    def transition_repeat(self):
        pool = Pool()
        Analysis = pool.get('lims.analysis')
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')
        EntryDetailAnalysis = pool.get('lims.entry.detail.analysis')
        Config = pool.get('lims.configuration')

        analysis = self.start.analysis
        analysis_type = analysis.type
        if analysis_type == 'analysis':
            analysis_to_repeat = [analysis.id]
        else:
            analysis_to_repeat = Analysis.get_included_analysis_analysis(
                analysis.id)

        line_id = self._get_notebook_line_id()
        notebook_line = NotebookLine(line_id)
        notebook = Notebook(notebook_line.notebook.id)

        rm_type = (notebook.fraction.special_type == 'rm')
        if rm_type:
            config = Config(1)
            rm_start_uom = (config.rm_start_uom.id if config.rm_start_uom
                else None)

        to_create = []
        to_update = []
        details_to_update = []
        for analysis_id in analysis_to_repeat:
            clause = [
                ('notebook', '=', notebook.id),
                ('analysis', '=', analysis_id),
                ('analysis.behavior', '=', 'normal'),
                ]
            if self.start.method:
                clause.append(('method', '=', self.start.method.id))
            nlines = NotebookLine.search(clause,
                order=[('repetition', 'DESC')], limit=1)
            if not nlines:
                continue
            nline_to_repeat = nlines[0]

            defaults = self._get_repetition_defaults(nline_to_repeat)
            if analysis_type == 'analysis' and self.start.method:
                defaults['method'] = self.start.method.id
            if rm_type:
                defaults['final_concentration'] = None
                defaults['literal_final_concentration'] = None
                defaults['initial_unit'] = rm_start_uom
                defaults['final_unit'] = None
                defaults['detection_limit'] = None
                defaults['quantification_limit'] = None
                defaults['lower_limit'] = None
                defaults['upper_limit'] = None
            to_create.append(defaults)
            if not nline_to_repeat.results_report:
                to_update.append(nline_to_repeat)
            details_to_update.append(nline_to_repeat.analysis_detail.id)

        Notebook.write([notebook], {
            'lines': [('create', to_create)],
            })
        if self._unaccept_original():
            NotebookLine.write(to_update, {
                'accepted': False,
                'acceptance_date': None,
                'report': False,
                })

        details = EntryDetailAnalysis.search([
            ('id', 'in', details_to_update),
            ])
        if details:
            EntryDetailAnalysis.write(details, {
                'state': 'unplanned',
                })

        return 'end'

    def _get_repetition_defaults(self, line):
        defaults = {
            'analysis_detail': line.analysis_detail.id,
            'service': line.service.id,
            'analysis': line.analysis.id,
            'analysis_origin': line.analysis_origin,
            'urgent': line.urgent,
            'repetition': line.repetition + 1,
            'laboratory': line.laboratory.id,
            'method': line.method.id,
            'device': line.device.id if line.device else None,
            'decimals': line.decimals,
            'significant_digits': line.significant_digits,
            'scientific_notation': line.scientific_notation,
            'report': line.report,
            'results_estimated_waiting': (
                line.results_estimated_waiting),
            'department': line.department,
            'concentration_level': (line.concentration_level.id
                if line.concentration_level else None),
            'initial_concentration': line.initial_concentration,
            'final_concentration': line.final_concentration,
            'literal_final_concentration': line.literal_final_concentration,
            'initial_unit': (line.initial_unit.id
                if line.initial_unit else None),
            'final_unit': line.final_unit.id if line.final_unit else None,
            'detection_limit': line.detection_limit,
            'quantification_limit': line.quantification_limit,
            'lower_limit': line.lower_limit,
            'upper_limit': line.upper_limit,
            }
        defaults['repetition_reason'] = self.start.repetition_reason
        if line.results_report:
            defaults['report'] = False
        return defaults

    def end(self):
        return 'reload'


class NotebookAcceptLinesStart(ModelView):
    'Accept Lines'
    __name__ = 'lims.notebook.accept_lines.start'


class NotebookAcceptLines(Wizard):
    'Accept Lines'
    __name__ = 'lims.notebook.accept_lines'

    start_state = 'ok'
    start = StateView('lims.notebook.accept_lines.start',
        'lims.lims_notebook_accept_lines_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Ok', 'ok', 'tryton-ok', default=True),
            ])
    ok = StateTransition()

    def transition_ok(self):
        pool = Pool()
        Config = pool.get('lims.configuration')
        NotebookLine = pool.get('lims.notebook.line')

        notebook_lines_acceptance = Config(1).notebook_lines_acceptance

        for notebook_id in Transaction().context['active_ids']:

            if notebook_lines_acceptance == 'none':
                repeated_analysis = []
                with Transaction().set_context(_check_access=True):
                    repetitions = NotebookLine.search([
                        ('notebook', '=', notebook_id),
                        ('repetition', '>', 0),
                        ])
                if repetitions:
                    repeated_analysis = [l.analysis.id for l in repetitions]

                with Transaction().set_context(_check_access=True):
                    notebook_lines = NotebookLine.search([
                        ('notebook', '=', notebook_id),
                        ('analysis', 'not in', repeated_analysis),
                        ])

            else:  # last
                with Transaction().set_context(_check_access=True):
                    notebook_lines = NotebookLine.search([
                        ('notebook', '=', notebook_id),
                        ], order=[('repetition', 'DESC')])

            if not notebook_lines:
                continue
            self.lines_accept(notebook_lines)
        return 'end'

    def lines_accept(self, notebook_lines):
        pool = Pool()
        Config = pool.get('lims.configuration')
        NotebookLine = pool.get('lims.notebook.line')

        allow_same_analysis = Config(1).notebook_lines_acceptance_method

        accepted_analysis = {}
        lines_to_write = []
        for notebook_line in notebook_lines:
            if notebook_line.accepted:
                continue
            if not notebook_line.report:
                continue
            if notebook_line.annulled:
                continue
            if not notebook_line.end_date:
                continue
            if not (notebook_line.result or notebook_line.converted_result or
                    notebook_line.literal_result or
                    (notebook_line.result_modifier and
                    notebook_line.result_modifier.code in
                    ('d', 'nd', 'pos', 'neg', 'ni', 'abs', 'pre')) or
                    (notebook_line.converted_result_modifier and
                    notebook_line.converted_result_modifier.code in
                    ('d', 'nd', 'pos', 'neg', 'ni', 'abs', 'pre'))):
                continue
            if (notebook_line.converted_result and (
                    notebook_line.converted_result_modifier and
                    notebook_line.converted_result_modifier.code not in
                    ('ni', 'low'))):
                continue
            if (notebook_line.result and (
                    notebook_line.result_modifier and
                    notebook_line.result_modifier.code not in
                    ('ni', 'low'))):
                continue

            notebook_id = notebook_line.notebook.id
            if notebook_id not in accepted_analysis:
                accepted_lines = NotebookLine.search([
                    ('notebook', '=', notebook_id),
                    ('accepted', '=', True),
                    ])
                if allow_same_analysis:
                    accepted_analysis[notebook_id] = [
                        (l.analysis.id, l.method.id) for l in accepted_lines]
                else:
                    accepted_analysis[notebook_id] = [
                        l.analysis.id for l in accepted_lines]
            if allow_same_analysis:
                key = (notebook_line.analysis.id, notebook_line.method.id)
            else:
                key = notebook_line.analysis.id
            if key in accepted_analysis[notebook_id]:
                continue

            accepted_analysis[notebook_id].append(key)
            lines_to_write.append(notebook_line)

        if lines_to_write:
            acceptance_date = datetime.now()
            NotebookLine.write(lines_to_write, {
                'accepted': True,
                'acceptance_date': acceptance_date,
                })

    def end(self):
        return 'reload'


class NotebookLineAcceptLines(NotebookAcceptLines):
    'Accept Lines'
    __name__ = 'lims.notebook_line.accept_lines'

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        notebook_lines = NotebookLine.browse(
            Transaction().context['active_ids'])
        if not notebook_lines:
            return 'end'

        self.lines_accept(notebook_lines)
        return 'end'


class NotebookLineUnacceptLines(Wizard):
    'Revert Lines Acceptance'
    __name__ = 'lims.notebook_line.unaccept_lines'

    start_state = 'ok'
    ok = StateTransition()

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        notebook_lines = NotebookLine.browse(
            Transaction().context['active_ids'])
        if not notebook_lines:
            return 'end'

        self.lines_unaccept(notebook_lines)
        return 'end'

    def lines_unaccept(self, notebook_lines):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        ResultsLine = pool.get('lims.results_report.version.detail.line')

        lines_to_write = []
        for notebook_line in notebook_lines:
            if not notebook_line.accepted:
                continue
            report_lines = ResultsLine.search([
                ('notebook_line', '=', notebook_line.id),
                ('detail_sample.version_detail.state', '!=', 'annulled'),
                ])
            if report_lines:
                continue

            lines_to_write.append(notebook_line)

        if lines_to_write:
            NotebookLine.write(lines_to_write, {
                'accepted': False,
                'acceptance_date': None,
                })

    def end(self):
        return 'reload'


class NotebookAnnulLinesStart(ModelView):
    'Annul Lines'
    __name__ = 'lims.notebook.annul_lines.start'

    annulment_reason = fields.Text('Annulment reason')


class NotebookAnnulLines(Wizard):
    'Annul Lines'
    __name__ = 'lims.notebook.annul_lines'

    start = StateView('lims.notebook.annul_lines.start',
        'lims.lims_notebook_annul_lines_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Ok', 'ok', 'tryton-ok', default=True),
            ])
    ok = StateTransition()

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        with Transaction().set_context(_check_access=True):
            notebook_lines = NotebookLine.search([
                ('notebook', 'in', Transaction().context['active_ids']),
                ('accepted', '=', False),
                ('annulled', '=', False),
                ])
        if notebook_lines:
            self.lines_annul(notebook_lines)
        return 'end'

    def lines_annul(self, notebook_lines):
        pool = Pool()
        ModelData = pool.get('ir.model.data')
        NotebookLine = pool.get('lims.notebook.line')

        result_modifier_na = ModelData.get_id('lims', 'result_modifier_na')

        NotebookLine.write(notebook_lines, {
            'result_modifier': result_modifier_na,
            'annulled': True,
            'annulment_date': datetime.now(),
            'annulment_reason': self.start.annulment_reason,
            'report': False,
            })

    def end(self):
        return 'reload'


class NotebookLineAnnulLines(NotebookAnnulLines):
    'Annul Lines'
    __name__ = 'lims.notebook_line.annul_lines'

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        notebook_lines = NotebookLine.search([
            ('id', 'in', Transaction().context['active_ids']),
            ('accepted', '=', False),
            ('annulled', '=', False),
            ])
        if notebook_lines:
            self.lines_annul(notebook_lines)
        return 'end'


class NotebookUnannulLines(Wizard):
    'Revert Lines Annulment'
    __name__ = 'lims.notebook.unannul_lines'

    start_state = 'ok'
    ok = StateTransition()

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        for notebook_id in Transaction().context['active_ids']:
            with Transaction().set_context(_check_access=True):
                notebook_lines = NotebookLine.search([
                    ('notebook', '=', notebook_id),
                    ('annulled', '=', True),
                    ])
            if notebook_lines:
                self.lines_unannul(notebook_lines)
        return 'end'

    def lines_unannul(self, notebook_lines):
        NotebookLine = Pool().get('lims.notebook.line')

        NotebookLine.write(notebook_lines, {
            'result_modifier': None,
            'annulled': False,
            'annulment_date': None,
            'annulment_reason': None,
            'report': True,
            })

    def end(self):
        return 'reload'


class NotebookLineUnannulLines(NotebookUnannulLines):
    'Revert Lines Annulment'
    __name__ = 'lims.notebook_line.unannul_lines'

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        notebook_lines = NotebookLine.search([
            ('id', 'in', Transaction().context['active_ids']),
            ('annulled', '=', True),
            ])
        if notebook_lines:
            self.lines_unannul(notebook_lines)
        return 'end'


class NotebookResultsVerificationStart(ModelView):
    'Results Verification'
    __name__ = 'lims.notebook.results_verification.start'

    range_type = fields.Many2One('lims.range.type', 'Origin', required=True,
        domain=[('use', '=', 'results_verification')])


class NotebookResultsVerification(Wizard):
    'Results Verification'
    __name__ = 'lims.notebook.results_verification'

    start = StateView('lims.notebook.results_verification.start',
        'lims.lims_notebook_results_verification_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Ok', 'ok', 'tryton-ok', default=True),
            ])
    ok = StateTransition()

    def default_start(self, fields):
        RangeType = Pool().get('lims.range.type')

        default = {}
        default_range_type = RangeType.search([
            ('use', '=', 'results_verification'),
            ('by_default', '=', True),
            ])
        if default_range_type:
            default['range_type'] = default_range_type[0].id
        return default

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        for notebook_id in Transaction().context['active_ids']:
            with Transaction().set_context(_check_access=True):
                notebook_lines = NotebookLine.search([
                    ('notebook', '=', notebook_id),
                    ])
            if not notebook_lines:
                continue
            self.lines_results_verification(notebook_lines)
        return 'end'

    def lines_results_verification(self, notebook_lines):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        Range = pool.get('lims.range')
        UomConversion = pool.get('lims.uom.conversion')
        VolumeConversion = pool.get('lims.volume.conversion')

        verifications = self._get_verifications()

        lines_to_save = []
        for notebook_line in notebook_lines:
            if notebook_line.accepted:
                continue

            result = notebook_line.converted_result
            if not result:
                result = notebook_line.result
                iu = notebook_line.initial_unit
                if not iu:
                    continue
                try:
                    ic = float(notebook_line.initial_concentration)
                except (TypeError, ValueError):
                    continue
            else:
                iu = notebook_line.final_unit
                if not iu:
                    continue
                try:
                    ic = float(notebook_line.final_concentration)
                except (TypeError, ValueError):
                    continue

            try:
                result = float(result)
            except (TypeError, ValueError):
                continue

            ranges = Range.search([
                ('range_type', '=', self.start.range_type),
                ('analysis', '=', notebook_line.analysis.id),
                ('product_type', '=', notebook_line.notebook.product_type.id),
                ('matrix', '=', notebook_line.notebook.matrix.id),
                ])
            if not ranges:
                continue
            fu = ranges[0].uom
            try:
                fc = float(ranges[0].concentration)
            except (TypeError, ValueError):
                continue

            if fu and fu.rec_name != '-':
                converted_result = None
                if (iu == fu and ic == fc):
                    converted_result = result
                elif (iu != fu and ic == fc):
                    formula = UomConversion.get_conversion_formula(iu,
                        fu)
                    if not formula:
                        continue
                    variables = self._get_variables(formula, notebook_line)
                    parser = FormulaParser(formula, variables)
                    formula_result = parser.getValue()

                    converted_result = result * formula_result
                elif (iu == fu and ic != fc):
                    converted_result = result * (fc / ic)
                else:
                    formula = None
                    conversions = UomConversion.search([
                        ('initial_uom', '=', iu),
                        ('final_uom', '=', fu),
                        ])
                    if conversions:
                        formula = conversions[0].conversion_formula
                    if not formula:
                        continue
                    variables = self._get_variables(formula, notebook_line)
                    parser = FormulaParser(formula, variables)
                    formula_result = parser.getValue()

                    if (conversions[0].initial_uom_volume and
                            conversions[0].final_uom_volume):
                        d_ic = VolumeConversion.brixToDensity(ic)
                        d_fc = VolumeConversion.brixToDensity(fc)
                        converted_result = (result * (fc / ic) *
                            (d_fc / d_ic) * formula_result)
                    else:
                        converted_result = (result * (fc / ic) *
                            formula_result)
                result = float(converted_result)

            verification = self._verificate_result(result, ranges[0])
            notebook_line.verification = verifications.get(verification)
            lines_to_save.append(notebook_line)
        if lines_to_save:
            NotebookLine.save(lines_to_save)

    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

    def _verificate_result(self, result, range_):
        if range_.min95 and range_.max95:
            if result < range_.min:
                return 'out'
            elif result < range_.min95:
                return 'ok*'
            elif result <= range_.max95:
                return 'ok'
            elif result <= range_.max:
                return 'ok*'
            else:
                return 'out'
        else:
            if (range_.min and result < range_.min):
                return 'out'
            elif (range_.max and result <= range_.max):
                return 'ok'
            else:
                return 'out'

    def _get_verifications(self):
        pool = Pool()
        User = pool.get('res.user')
        Lang = pool.get('ir.lang')

        lang = User(Transaction().user).language
        if not lang:
            lang, = Lang.search([
                    ('code', '=', 'en'),
                    ], limit=1)

        verifications = {}
        with Transaction().set_context(language=lang.code):
            verifications['ok'] = gettext('lims.msg_ok')
            verifications['ok*'] = gettext('lims.msg_ok*')
            verifications['out'] = gettext('lims.msg_out')

        return verifications

    def end(self):
        return 'reload'


class NotebookLineResultsVerification(NotebookResultsVerification):
    'Results Verification'
    __name__ = 'lims.notebook_line.results_verification'

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        notebook_lines = NotebookLine.browse(
            Transaction().context['active_ids'])
        if not notebook_lines:
            return 'end'

        self.lines_results_verification(notebook_lines)
        return 'end'


class UncertaintyCalcStart(ModelView):
    'Uncertainty Calculation'
    __name__ = 'lims.notebook.uncertainty_calc.start'

    range_type = fields.Many2One('lims.range.type', 'Origin', required=True,
        domain=[('use', '=', 'uncertainty_calc')])


class UncertaintyCalc(Wizard):
    'Uncertainty Calculation'
    __name__ = 'lims.notebook.uncertainty_calc'

    start_state = 'check'
    check = StateTransition()
    start = StateView('lims.notebook.uncertainty_calc.start',
        'lims.lims_notebook_uncertainty_calc_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Ok', 'ok', 'tryton-ok', default=True),
            ])
    ok = StateTransition()

    def transition_check(self):
        RangeType = Pool().get('lims.range.type')

        default_range_type = RangeType.search([
            ('use', '=', 'uncertainty_calc'),
            ('by_default', '=', True),
            ])
        if default_range_type:
            self.start.range_type = default_range_type[0].id
            return 'ok'
        return 'start'

    def default_start(self, fields):
        RangeType = Pool().get('lims.range.type')

        default = {}
        default_range_type = RangeType.search([
            ('use', '=', 'uncertainty_calc'),
            ('by_default', '=', True),
            ])
        if default_range_type:
            default['range_type'] = default_range_type[0].id
        return default

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        for notebook_id in Transaction().context['active_ids']:
            with Transaction().set_context(_check_access=True):
                notebook_lines = NotebookLine.search([
                    ('notebook', '=', notebook_id),
                    ])
            if not notebook_lines:
                continue
            self.lines_uncertainty_calc(notebook_lines)
        return 'end'

    def lines_uncertainty_calc(self, notebook_lines):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        Range = pool.get('lims.range')

        lines_to_save = []
        for notebook_line in notebook_lines:
            if notebook_line.accepted:
                continue
            result = notebook_line.converted_result
            if not result:
                result = notebook_line.result
            try:
                result = float(result)
            except (TypeError, ValueError):
                continue

            ranges = Range.search([
                ('range_type', '=', self.start.range_type),
                ('analysis', '=', notebook_line.analysis.id),
                ('product_type', '=', notebook_line.notebook.product_type.id),
                ('matrix', '=', notebook_line.notebook.matrix.id),
                ])
            if not ranges:
                continue

            uncertainty = self._get_uncertainty(result, notebook_line,
                ranges[0])
            if uncertainty is None:
                continue
            notebook_line.uncertainty = str(uncertainty)
            lines_to_save.append(notebook_line)
        if lines_to_save:
            NotebookLine.save(lines_to_save)

    def _get_uncertainty(self, result, notebook_line, range_):
        dilution_factor = notebook_line.dilution_factor
        if not dilution_factor or dilution_factor == 0.0:
            dilution_factor = 1.0
        diluted_result = result / dilution_factor
        try:
            factor = range_.factor or 1.0
            low_level = range_.low_level or 0.0 * factor
            middle_level = range_.middle_level or 0.0 * factor
            high_level = range_.high_level or 0.0 * factor
        except TypeError:
            return None

        uncertainty = 0.0
        if (range_.low_level_value and
                not (range_.middle_level_value and range_.high_level_value) and
                (diluted_result > low_level)):
            uncertainty = range_.low_level_value
        elif (range_.low_level_value and range_.middle_level_value and
                range_.high_level_value):
            if (low_level <= diluted_result and
                    diluted_result < middle_level):
                uncertainty = range_.low_level_value
            elif (middle_level <= diluted_result and
                    diluted_result < high_level):
                uncertainty = range_.middle_level_value
            elif diluted_result >= high_level:
                uncertainty = range_.high_level_value
        if (range_.low_level_value and range_.middle_level_value and not
                range_.high_level_value):
            if (low_level <= diluted_result and
                    diluted_result < middle_level):
                uncertainty = range_.low_level_value
            elif (middle_level <= diluted_result and
                    diluted_result < high_level):
                uncertainty = range_.middle_level_value

        if uncertainty > 0.0:
            uncertainty = result * uncertainty / 100

        return uncertainty

    def end(self):
        return 'reload'


class NotebookLineUncertaintyCalc(UncertaintyCalc):
    'Uncertainty Calculation'
    __name__ = 'lims.notebook_line.uncertainty_calc'

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        notebook_lines = NotebookLine.browse(
            Transaction().context['active_ids'])
        if not notebook_lines:
            return 'end'

        self.lines_uncertainty_calc(notebook_lines)
        return 'end'


class NotebookPrecisionControlStart(ModelView):
    'Precision Control'
    __name__ = 'lims.notebook.precision_control.start'

    range_type = fields.Many2One('lims.range.type', 'Origin', required=True,
        domain=[('use', '=', 'repeatability_calc')])
    product_type = fields.Many2One('lims.product.type', 'Product type',
        required=True)
    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')
    factor = fields.Float('Factor', required=True)

    @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,))
        res = cursor.fetchall()
        if not res:
            return []
        return [x[0] for x in res]


class NotebookPrecisionControl(Wizard):
    'Precision Control'
    __name__ = 'lims.notebook.precision_control'

    start_state = 'check'
    check = StateTransition()
    start = StateView('lims.notebook.precision_control.start',
        'lims.lims_notebook_precision_control_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Ok', 'ok', 'tryton-ok', default=True),
            ])
    ok = StateTransition()

    def transition_check(self):
        Notebook = Pool().get('lims.notebook')

        notebook = Notebook(Transaction().context['active_id'])
        if notebook.fraction.special_type in ('rm', 'con'):
            return 'start'
        return 'end'

    def default_start(self, fields):
        pool = Pool()
        Notebook = pool.get('lims.notebook')
        RangeType = pool.get('lims.range.type')

        notebook = Notebook(Transaction().context['active_id'])
        default = {
            'product_type': notebook.product_type.id,
            'matrix': notebook.matrix.id,
            'factor': 2,
            }
        default_range_type = RangeType.search([
            ('use', '=', 'repeatability_calc'),
            ('by_default', '=', True),
            ])
        if default_range_type:
            default['range_type'] = default_range_type[0].id
        return default

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        with Transaction().set_context(_check_access=True):
            notebook_lines = NotebookLine.search([
                ('notebook', '=', Transaction().context['active_id']),
                ])
        if not notebook_lines:
            return 'end'

        self.lines_precision_control(notebook_lines)
        return 'end'

    def lines_precision_control(self, notebook_lines):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        Range = pool.get('lims.range')

        lines_to_save = []
        for notebook_line in notebook_lines:
            if notebook_line.verification:
                continue
            concentration_level = notebook_line.concentration_level
            if not concentration_level:
                continue

            ranges = Range.search([
                ('range_type', '=', self.start.range_type.id),
                ('analysis', '=', notebook_line.analysis.id),
                ('product_type', '=', self.start.product_type.id),
                ('matrix', '=', self.start.matrix.id),
                ])
            if not ranges:
                continue

            if concentration_level.code == 'NC':
                cv = ranges[0].low_level_coefficient_variation
            elif concentration_level.code == 'NM':
                cv = ranges[0].middle_level_coefficient_variation
            elif concentration_level.code == 'NA':
                cv = ranges[0].high_level_coefficient_variation
            else:
                continue
            if not cv:
                continue

            try:
                if notebook_line.repetition == 0:
                    rep_0 = float(notebook_line.result)
                    rep_1 = float(self._get_repetition_result(notebook_line,
                        1))
                elif notebook_line.repetition == 1:
                    rep_0 = float(self._get_repetition_result(notebook_line,
                        0))
                    rep_1 = float(notebook_line.result)
                else:
                    continue
            except (TypeError, ValueError):
                continue

            if not rep_0 or not rep_1:
                continue

            average = (rep_0 + rep_1) / 2
            error = abs(rep_0 - rep_1) / average * 100

            if error < (cv * self.start.factor):
                res = gettext(
                    'lims.msg_acceptable', factor=self.start.factor, cv=cv)
            else:
                res = gettext(
                    'lims.msg_unacceptable', factor=self.start.factor, cv=cv)
            notebook_line.verification = res
            lines_to_save.append(notebook_line)
        if lines_to_save:
            NotebookLine.save(lines_to_save)

    def _get_repetition_result(self, notebook_line, repetition):
        NotebookLine = Pool().get('lims.notebook.line')

        repetition = NotebookLine.search([
            ('notebook', '=', notebook_line.notebook.id),
            ('analysis', '=', notebook_line.analysis.id),
            ('repetition', '=', repetition),
            ])
        if not repetition:
            return None
        return repetition[0].result

    def end(self):
        return 'reload'


class NotebookLinePrecisionControl(NotebookPrecisionControl):
    'Precision Control'
    __name__ = 'lims.notebook_line.precision_control'

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

        reference_line = NotebookLine(Transaction().context['active_id'])
        if reference_line.notebook.fraction.special_type in ('rm', 'con'):
            return 'start'
        return 'end'

    def default_start(self, fields):
        NotebookLine = Pool().get('lims.notebook.line')

        reference_line = NotebookLine(Transaction().context['active_id'])
        with Transaction().set_context(active_id=reference_line.notebook.id):
            return super().default_start(fields)

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        reference_line = NotebookLine(Transaction().context['active_id'])

        notebook_lines = NotebookLine.browse(
            Transaction().context['active_ids'])
        notebook_lines = [l for l in notebook_lines
            if l.notebook.id == reference_line.notebook.id]
        if not notebook_lines:
            return 'end'

        self.lines_precision_control(notebook_lines)
        return 'end'


class OpenNotebookLines(Wizard):
    'Open Notebook Lines'
    __name__ = 'lims.open_notebook_lines'
    start_state = 'open_'
    open_ = StateAction('lims.act_lims_notebook_line_related1')

    def do_open_(self, action):
        Notebook = Pool().get('lims.notebook')

        notebook = Notebook.browse(Transaction().context['active_ids'])[0]
        action['pyson_domain'] = PYSONEncoder().encode(
            [('notebook', 'in', Transaction().context['active_ids'])])

        action['name'] = \
            '%s - %s - %s - %s - %s' % (notebook.fraction.number,
                notebook.party.name, notebook.product_type.description,
                notebook.matrix.description, notebook.label)
        return action, {}


class ChangeEstimatedDaysForResultsStart(ModelView):
    'Change Estimated Days For Results'
    __name__ = 'lims.change_results_estimated_waiting.start'

    date_from = fields.Date('Confirmation date From', required=True)
    date_to = fields.Date('Confirmation date To', required=True)
    methods = fields.Many2Many('lims.lab.method',
        None, None, 'Methods', required=True)
    results_estimated_waiting = fields.Integer('Days to add')
    party = fields.Many2One('party.party', 'Party')


class ChangeEstimatedDaysForResults(Wizard):
    'Change Estimated Days For Results'
    __name__ = 'lims.change_results_estimated_waiting'

    start = StateView('lims.change_results_estimated_waiting.start',
        'lims.change_results_estimated_waiting_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Change', 'change', 'tryton-ok', default=True),
            ])
    change = StateTransition()

    def transition_change(self):
        NotebookLine = Pool().get('lims.notebook.line')

        methods_ids = [m.id for m in self.start.methods]
        clause = [('method', 'in', methods_ids)]
        clause.append(('analysis_detail.confirmation_date',
            '>=', self.start.date_from))
        clause.append(('analysis_detail.confirmation_date',
            '<=', self.start.date_to))
        clause.append(('accepted', '=', False))
        party_id = self.start.party and self.start.party.id or None
        if party_id:
            clause.append(('party', '=', party_id))

        with Transaction().set_context(_check_access=True):
            notebook_lines = NotebookLine.search(clause)
        if notebook_lines:
            lines_to_save = []
            for line in notebook_lines:
                line.results_estimated_waiting = ((
                    line.results_estimated_waiting or 0) +
                    self.start.results_estimated_waiting)
                lines_to_save.append(line)
            NotebookLine.save(lines_to_save)
        return 'end'


class PrintAnalysisPendingInformStart(ModelView):
    'Analysis Pending of Inform'
    __name__ = 'lims.print_analysis_pending_inform.start'

    date_from = fields.Date('From confirmation date', required=True)
    date_to = fields.Date('To confirmation date', required=True)
    laboratory = fields.Many2One('lims.laboratory', 'Laboratory',
        required=True)
    party = fields.Many2One('party.party', 'Party')
    include_comments_of_fraction = fields.Boolean(
        'Include comments of the fraction')


class PrintAnalysisPendingInform(Wizard):
    'Analysis Pending of Inform'
    __name__ = 'lims.print_analysis_pending_inform'

    start = StateView('lims.print_analysis_pending_inform.start',
        'lims.print_analysis_pending_inform_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Print', 'print_', 'tryton-print', default=True),
        ])
    print_ = StateReport('lims.analysis_pending_inform')

    def do_print_(self, action):
        data = {
            'date_from': self.start.date_from,
            'date_to': self.start.date_to,
            'laboratory': self.start.laboratory.id,
            'party': self.start.party and self.start.party.id or None,
            'include_comments_of_fraction': (
                self.start.include_comments_of_fraction),
            }
        return action, data

    def transition_print_(self):
        return 'end'


class AnalysisPendingInform(Report):
    'Analysis Pending of Inform'
    __name__ = 'lims.analysis_pending_inform'

    @classmethod
    def get_context(cls, records, header, data):
        pool = Pool()
        Laboratory = pool.get('lims.laboratory')
        Party = pool.get('party.party')

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

        today = get_print_date()
        report_context['today'] = today
        report_context['company'] = report_context['user'].company
        report_context['date_from'] = data['date_from']
        report_context['date_to'] = data['date_to']
        report_context['laboratory'] = Laboratory(data['laboratory']).rec_name
        report_context['party'] = ''
        if data['party']:
            report_context['party'] = Party(data['party']).rec_name
        report_context['include_comments_of_fraction'] = \
            data['include_comments_of_fraction']

        objects = cls._get_report_records(data['date_from'], data['date_to'],
            data['laboratory'], data['party'])

        report_context['records'] = objects
        report_context['get_notice'] = cls.get_notice

        return report_context

    @classmethod
    def _get_report_records(cls, date_from, date_to, laboratory, party):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')

        res = []

        excluded_notebooks = cls._get_excluded_notebooks(date_from, date_to,
            laboratory, party)
        if excluded_notebooks:
            for n_id, a_ids in excluded_notebooks.items():
                clause = [
                    ('notebook.id', '=', n_id),
                    ('analysis', 'in', a_ids),
                    ('laboratory', '=', laboratory),
                    ('report', '=', True),
                    ('annulled', '=', False),
                    ]
                excluded_lines = NotebookLine.search(clause)
                if excluded_lines:
                    res.extend(excluded_lines)
        return res

    @classmethod
    def _get_excluded_notebooks(cls, date_from, date_to, laboratory, party):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        Service = pool.get('lims.service')
        Fraction = pool.get('lims.fraction')
        Sample = pool.get('lims.sample')
        Entry = pool.get('lims.entry')
        FractionType = pool.get('lims.fraction.type')

        party_clause = ''
        if party:
            party_clause = 'AND e.party = ' + str(party)

        cursor.execute('SELECT nl.notebook, nl.analysis, nl.method, '
                'nl.accepted '
            'FROM "' + NotebookLine._table + '" nl '
                'INNER JOIN "' + Service._table + '" srv '
                'ON srv.id = nl.service '
                'INNER JOIN "' + Fraction._table + '" f '
                'ON f.id = srv.fraction '
                'INNER JOIN "' + Sample._table + '" s '
                'ON s.id = f.sample '
                'INNER JOIN "' + Entry._table + '" e '
                'ON e.id = s.entry '
                'INNER JOIN "' + FractionType._table + '" ft '
                'ON ft.id = f.type '
            'WHERE ft.report = TRUE '
                'AND srv.confirmation_date::date >= %s::date '
                'AND srv.confirmation_date::date <= %s::date '
                'AND nl.laboratory = %s '
                'AND nl.report = TRUE '
                'AND nl.annulled = FALSE ' +
                party_clause,
            (date_from, date_to, laboratory,))
        notebook_lines = cursor.fetchall()

        # Check repetitions
        oks, to_check = [], []
        for line in notebook_lines:
            key = (line[0], line[1], line[2])
            if not line[3]:
                to_check.append(key)
            else:
                oks.append(key)

        to_check = list(set(to_check) - set(oks))

        excluded_notebooks = {}
        for n_id, a_id, m_id in to_check:
            if n_id not in excluded_notebooks:
                excluded_notebooks[n_id] = []
            excluded_notebooks[n_id].append(a_id)
        return excluded_notebooks

    @classmethod
    def get_notice(cls, line):
        pool = Pool()
        Date = pool.get('ir.date')
        today = Date.today()

        if line.report_date:
            if line.report_date < today:
                return 'Timed out'
        if line.results_estimated_date and not line.report_date:
            if line.results_estimated_date < today:
                return 'Timed out'
        if line.report_date:
            if ((line.report_date - today).days < 3 and
                    (line.report_date - today).days >= 0):
                return 'To expire'
        if line.results_estimated_date and not line.report_date:
            if ((line.results_estimated_date - today).days < 3 and
                    (line.results_estimated_date - today).days >= 0):
                return 'To expire'
        return ''


class PrintAnalysisCheckedPendingInformStart(ModelView):
    'Analysis checked pending of Inform'
    __name__ = 'lims.print_analysis_checked_pending_inform.start'

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


class PrintAnalysisCheckedPendingInform(Wizard):
    'Analysis Checked pending of Inform'
    __name__ = 'lims.print_analysis_checked_pending_inform'

    start = StateView('lims.print_analysis_checked_pending_inform.start',
        'lims.print_analysis_checked_pending_inform_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Print', 'print_', 'tryton-print', default=True),
        ])
    print_ = StateReport('lims.analysis_checked_pending_inform')

    def do_print_(self, action):
        data = {
            'date_from': self.start.date_from,
            'date_to': self.start.date_to,
            'laboratory': self.start.laboratory.id,
            'party': self.start.party and self.start.party.id or None,
            }
        return action, data

    def transition_print_(self):
        return 'end'


class AnalysisCheckedPendingInform(Report):
    'Analysis checked pending of Inform'
    __name__ = 'lims.analysis_checked_pending_inform'

    @classmethod
    def get_context(cls, records, header, data):
        pool = Pool()
        Laboratory = pool.get('lims.laboratory')
        Party = pool.get('party.party')

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

        today = get_print_date()
        report_context['today'] = today
        report_context['company'] = report_context['user'].company
        report_context['date_from'] = data['date_from']
        report_context['date_to'] = data['date_to']
        report_context['laboratory'] = Laboratory(data['laboratory']).rec_name
        report_context['party'] = ''
        if data['party']:
            report_context['party'] = Party(data['party']).rec_name

        objects = cls._get_report_records(data['date_from'], data['date_to'],
            data['laboratory'], data['party'])

        report_context['records'] = objects
        report_context['get_notice'] = cls.get_notice

        return report_context

    @classmethod
    def _get_report_records(cls, date_from, date_to, laboratory, party):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')

        res = []

        included_notebooks = cls._get_included_notebooks(date_from, date_to,
            laboratory, party)
        if included_notebooks:
            for n_id, a_ids in included_notebooks:
                clause = [
                    ('notebook.id', '=', n_id),
                    ('analysis', 'in', [a_ids]),
                    ('laboratory', '=', laboratory),
                    ('report', '=', True),
                    ('annulled', '=', False),
                    ]
                included_lines = NotebookLine.search(clause)
                if included_lines:
                    res.extend(line for line in included_lines)
        return res

    @classmethod
    def _get_included_notebooks(cls, date_from, date_to, laboratory, party):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        Notebook = pool.get('lims.notebook')
        Fraction = pool.get('lims.fraction')
        Sample = pool.get('lims.sample')
        Entry = pool.get('lims.entry')
        FractionType = pool.get('lims.fraction.type')

        party_clause = ''
        if party:
            party_clause = 'AND e.party = ' + str(party)

        cursor.execute('SELECT nl.notebook, nl.analysis '
            'FROM "' + NotebookLine._table + '" nl '
                '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 "' + Entry._table + '" e '
                'ON e.id = s.entry '
                'INNER JOIN "' + FractionType._table + '" ft '
                'ON ft.id = f.type '
            'WHERE ft.report = TRUE '
                'AND s.date::date >= %s::date '
                'AND s.date::date <= %s::date '
                'AND nl.laboratory = %s '
                'AND nl.report = TRUE '
                'AND nl.accepted = TRUE '
                'AND nl.results_report IS NULL '
                'AND nl.annulled = FALSE ' +
                party_clause,
            (date_from, date_to, laboratory,))

        notebook_lines = cursor.fetchall()
        return notebook_lines

    @classmethod
    def get_notice(cls, line):
        pool = Pool()
        Date = pool.get('ir.date')
        today = Date.today()

        if line.report_date:
            if line.report_date < today:
                return 'Timed out'
        if line.results_estimated_date and not line.report_date:
            if line.results_estimated_date < today:
                return 'Timed out'
        if line.report_date:
            if ((line.report_date - today).days < 3 and
                    (line.report_date - today).days >= 0):
                return 'To expire'
        if line.results_estimated_date and not line.report_date:
            if ((line.results_estimated_date - today).days < 3 and
                    (line.results_estimated_date - today).days >= 0):
                return 'To expire'
        return ''


class NotebookEvaluateRulesStart(ModelView):
    'Evaluate Rules'
    __name__ = 'lims.notebook.evaluate_rules.start'


class NotebookEvaluateRules(Wizard):
    'Evaluate Rules'
    __name__ = 'lims.notebook.evaluate_rules'

    start_state = 'ok'
    start = StateView('lims.notebook.evaluate_rules.start',
        'lims.notebook_evaluate_rules_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Ok', 'ok', 'tryton-ok', default=True),
            ])
    ok = StateTransition()

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        for notebook_id in Transaction().context['active_ids']:
            with Transaction().set_context(_check_access=True):
                notebook_lines = NotebookLine.search([
                    ('notebook', '=', notebook_id),
                    ])
            if not notebook_lines:
                continue
            self.evaluate_rules(notebook_lines)
        return 'end'

    def evaluate_rules(self, notebook_lines):
        pool = Pool()
        NotebookRule = pool.get('lims.rule')

        for line in notebook_lines:
            rules = NotebookRule.search([
                ('analysis', '=', line.analysis),
                ])
            for rule in rules:
                if rule.eval_condition(line):
                    rule.exec_action(line)

    def end(self):
        return 'reload'


class NotebookLineEvaluateRules(NotebookEvaluateRules):
    'Evaluate Rules'
    __name__ = 'lims.notebook_line.evaluate_rules'

    def transition_ok(self):
        NotebookLine = Pool().get('lims.notebook.line')

        notebook_lines = NotebookLine.browse(
            Transaction().context['active_ids'])
        if not notebook_lines:
            return 'end'

        self.evaluate_rules(notebook_lines)
        return 'end'


class SampleNotebook(Wizard):
    'Sample Notebook'
    __name__ = 'lims.sample.notebook'

    start = StateAction('lims.act_lims_notebook_list')

    @classmethod
    def check_access(cls):
        pass

    def do_start(self, action):
        pool = Pool()
        Sample = pool.get('lims.sample')
        Notebook = pool.get('lims.notebook')

        active_ids = Transaction().context['active_ids']
        samples = Sample.browse(active_ids)

        notebooks = Notebook.search([
            ('fraction.sample', 'in', active_ids),
            ])
        action['pyson_domain'] = PYSONEncoder().encode([
            ('id', 'in', [nb.id for nb in notebooks]),
            ])
        action['name'] += ' (%s)' % ', '.join(
            s.rec_name for s in samples)
        return action, {}
