# SPDX-FileCopyrightText: 2008-2025 Luis Falcón <falcon@gnuhealth.org>
# SPDX-FileCopyrightText: 2011-2025 GNU Solidario <health@gnusolidario.org>
# SPDX-FileCopyrightText: 2015 Cédric Krier <cedric.krier@b2ck.com>
# SPDX-FileCopyrightText: 2014-2015 Chris Zimmerman <siv@riseup.net>
#
# SPDX-License-Identifier: GPL-3.0-or-later

#########################################################################
#   Hospital Management Information System (HMIS) component of the      #
#                       GNU Health project                              #
#                   https://www.gnuhealth.org                           #
#########################################################################
#                           HEALTH package                              #
#                       health.py: main module                          #
#########################################################################

import platform
import os
import string
import secrets
import pytz
import importlib.metadata

from dateutil.relativedelta import relativedelta
from datetime import datetime, timedelta, date
from urllib.parse import urlencode
from urllib.parse import urlunparse
from collections import OrderedDict
from io import BytesIO
from uuid import uuid4

from sql import Literal, Join

from trytond.model import (ModelView, ModelSingleton, ModelSQL,
                           MultiValueMixin, fields, Unique, tree)
from trytond.wizard import Wizard, StateAction, StateView, Button
from trytond.transaction import Transaction
from trytond.pyson import Eval, Not, Bool, PYSONEncoder, Equal, And, Or
from trytond.pool import Pool, PoolMeta
from trytond.rpc import RPC
from trytond.i18n import gettext


from .exceptions import (
    WrongDateofBirth, DateHealedBeforeDx, EndTreatmentDateBeforeStart,
    MedEndDateBeforeStart, NextDoseBeforeFirst, DrugPregnancySafetyCheck,
    DrugAllergySafetyCheck, PrescriptionSafetyCheck,
    EvaluationEndBeforeStart, MustBeAPerson, NoAssociatedHealthProfessional,
    DupOfficialName, FedAccountMismatch, BirthCertDateMismatch,
    CanNotModifyVaccination, DupMainSP
)

from .core import (get_institution,
                   compute_age_from_dates,
                   format_years_months_days,
                   estimated_date_from_years,
                   get_health_professional)


try:
    from PIL import Image
except ImportError:
    Image = None


__all__ = [
    'OperationalArea', 'OperationalSector', 'Occupation',
    'Ethnicity', 'DomiciliaryUnit', 'FederationCountryConfig',
    'BirthCertificate', 'DeathCertificate', 'Party', 'ContactMechanism',
    'PersonName', 'PartyAddress', 'DrugDoseUnits', 'MedicationFrequency',
    'DrugForm', 'DrugRoute',
    'MedicalSpecialty', 'HealthInstitution', 'HealthInstitutionSpecialties',
    'HealthInstitutionOperationalSector', 'HealthInstitutionO2M',
    'HospitalBuilding', 'HospitalUnit', 'HospitalOR', 'HospitalWard',
    'HospitalBed', 'HealthProfessional', 'HealthProfessionalSpecialties',
    'Family', 'FamilyMember', 'FamilyDiseases', 'MedicamentCategory',
    'Medicament', 'ImmunizationSchedule', 'ImmunizationScheduleLine',
    'ImmunizationScheduleDose', 'PathologyCategory', 'PathologyGroup',
    'Pathology', 'DiseaseMembers', 'ProcedureCode',
    'BirthCertExtraInfo', 'DeathCertExtraInfo', 'DeathUnderlyingCondition',
    'InsurancePlan', 'Insurance', 'AlternativePersonID',
    'Product', 'PatientData', 'ProceduresConfig',
    'PatientDiseaseInfo', 'Appointment', 'AppointmentReport',
    'OpenAppointmentReportStart', 'OpenAppointmentReport',
    'PatientPrescriptionOrder', 'PrescriptionLine', 'PatientMedication',
    'PatientVaccination', 'PatientEvaluation', 'PatientProcedure',
    'Directions', 'SecondaryCondition', 'DiagnosticHypothesis',
    'SignsAndSymptoms', 'PatientECG', 'ProductTemplate', 'PageOfLife',
    'Commands', 'Modules', 'Help', 'OnlineDocument']


class DomiciliaryUnit(ModelSQL, ModelView):
    'Domiciliary Unit'
    __name__ = 'gnuhealth.du'

    def get_parent(self, subdivision):
        # Recursively get the parent subdivisions
        if (subdivision.parent):
            return str(subdivision.rec_name) + ', ' + \
                str(self.get_parent(subdivision.parent))
        else:
            return subdivision.rec_name

    def get_du_address(self, name):
        du_addr = ''
        # Street
        if (self.address_street):
            du_addr = \
                f"{self.address_street} {self.address_street_number}, " \
                f"{self.address_street_bis}, \n"

        if (self.address_district):
            du_addr = f"{du_addr}{self.address_district}, "

        if (self.address_municipality):
            du_addr = f"{du_addr}{self.address_municipality}, "

        if (self.address_city):
            du_addr = f"{du_addr}{self.address_city}, "

        # Grab the parent subdivisions
        if (self.address_subdivision):
            du_addr = f"{du_addr}" \
                f"{self.get_parent(subdivision=self.address_subdivision)}"

        # Zip Code
        if (self.address_zip):
            du_addr = f"{du_addr} - {self.address_zip}, "

        # Country
        if (self.address_country):
            # address_country.rec_name has flag emoji, which will not
            # show properly when no proper fonts is installed.
            du_addr = f"{du_addr}\n{self.address_country.name}"

        return du_addr

    name = fields.Char('Code', required=True)
    desc = fields.Char('Desc')
    address_street = fields.Char('Street')
    address_street_number = fields.Char('Number')
    address_street_bis = fields.Char('Unit')

    address_district = fields.Char(
        'District', help="Neighborhood, Village, Barrio....")

    address_municipality = fields.Char(
        'Municipality', help="Municipality, Township, county ..")
    address_city = fields.Char('City', help="City")
    address_zip = fields.Char('Zip')
    address_country = fields.Many2One(
        'country.country', 'Country', help='Country')

    address_subdivision = fields.Many2One(
        'country.subdivision', 'Subdivision',
        domain=[('country', '=', Eval('address_country'))],
        depends=['address_country'])

    operational_sector = fields.Many2One(
        'gnuhealth.operational_sector', 'Op. Sector',
        help='Operational Sector')

    picture = fields.Binary('Picture')

    latitude = fields.Numeric('Latitude', digits=(3, 14))
    longitude = fields.Numeric('Longitude', digits=(4, 14))
    altitude = fields.Integer('Altitude', help="Altitude in meters")

    urladdr = fields.Char(
        'OSM Map',
        help="Locates the DU on the Open Street Map by default")

    # Text Representation
    address_repr = fields.Function(fields.Text("DU Address"),
                                   'get_du_address')

    # Infrastructure

    dwelling = fields.Selection([
        (None, ''),
        ('single_house', 'Single / Detached House'),
        ('apartment', 'Apartment'),
        ('townhouse', 'Townhouse'),
        ('factory', 'Factory'),
        ('building', 'Building'),
        ('mobilehome', 'Mobile House'),
    ], 'Type', sort=False)

    materials = fields.Selection([
        (None, ''),
        ('concrete', 'Concrete'),
        ('adobe', 'Adobe'),
        ('wood', 'Wood'),
        ('mud', 'Mud / Straw'),
        ('stone', 'Stone'),
    ], 'Material', sort=False)

    roof_type = fields.Selection([
        (None, ''),
        ('concrete', 'Concrete'),
        ('adobe', 'Adobe'),
        ('wood', 'Wood'),
        ('mud', 'Mud'),
        ('thatch', 'Thatched'),
        ('stone', 'Stone'),
    ], 'Roof', sort=False)

    total_surface = fields.Integer('Surface', help="Surface in sq. meters")
    bedrooms = fields.Integer('Bedrooms')
    bathrooms = fields.Integer('Bathrooms')

    housing = fields.Selection([
        (None, ''),
        ('0', 'Shanty, deficient sanitary conditions'),
        ('1', 'Small, crowded but with good sanitary conditions'),
        ('2', 'Comfortable and good sanitary conditions'),
        ('3', 'Roomy and excellent sanitary conditions'),
        ('4', 'Luxury and excellent sanitary conditions'),
    ], 'Conditions',
        help="Housing and sanitary living conditions", sort=False)

    sewers = fields.Boolean('Sanitary Sewers')
    water = fields.Boolean('Running Water')
    trash = fields.Boolean('Trash recollection')
    electricity = fields.Boolean('Electrical supply')
    gas = fields.Boolean('Gas supply')
    telephone = fields.Boolean('Telephone')
    television = fields.Boolean('Television')
    internet = fields.Boolean('Internet')

    members = fields.One2Many('party.party', 'du', 'Members', readonly=True)

    @fields.depends('latitude', 'longitude', 'address_street',
                    'address_street_bis', 'address_street_number',
                    'address_district', 'address_municipality', 'address_city',
                    'address_zip', 'address_subdivision', 'address_country')
    def on_change_with_urladdr(self):
        # Generates the URL to be used in OpenStreetMap
        #   If latitude and longitude are known, use them.
        #   Otherwise, use street, municipality, city, and so on.

        parts = OrderedDict()
        parts['scheme'] = 'http'
        parts['netloc'] = ''
        parts['path'] = ''
        parts['params'] = ''
        parts['query'] = ''
        parts['fragment'] = ''

        if (self.latitude and self.longitude):
            parts['netloc'] = 'openstreetmap.org'
            parts['path'] = '/'
            parts['query'] = urlencode({'mlat': self.latitude,
                                        'mlon': self.longitude})

        else:
            state = country = postalcode = city = municipality = \
                street = number = unit = ''
            if self.address_street_number is not None:
                number = self.address_street_number
            if self.address_street:
                street = self.address_street
            if self.address_street_bis:
                unit = self.address_street_bis
            if self.address_municipality:
                municipality = self.address_municipality
            if self.address_city:
                city = self.address_city
            if self.address_zip:
                postalcode = self.address_zip
            if self.address_subdivision:
                state = self.address_subdivision.name
            if self.address_country:
                country = self.address_country.code

            parts['netloc'] = 'nominatim.openstreetmap.org'
            parts['path'] = 'search'
            parts['query'] = urlencode(
                {'street': ' '.join(
                    [number, street]).strip(),
                 'unit': unit,
                 'county': municipality,
                 'city': city,
                 'state': state,
                 'postalcode': postalcode,
                 'country': country})

        return urlunparse(list(parts.values()))

    # Show the resulting Address representation in realtime
    @fields.depends(
        'address_street', 'address_subdivision', 'address_city',
        'address_zip', 'address_district', 'address_municipality',
        'address_street_number', 'address_country')
    def on_change_with_address_repr(self):
        return self.get_du_address(name=None)

    # Use the value from the Federation country for newly created DUs
    @staticmethod
    def default_address_country():
        Fedcountry = Pool().get('gnuhealth.federation.country.config')(1)
        if (Fedcountry and Fedcountry.country):
            return int(Fedcountry.country)

    @classmethod
    def __setup__(cls):
        super(DomiciliaryUnit, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('code_uniq', Unique(t, t.name),
             'The Domiciliary Unit must be unique !')
        ]


class FederationCountryConfig(ModelSingleton, ModelSQL, ModelView):
    'Federation Account Country'
    __name__ = 'gnuhealth.federation.country.config'

    country = fields.Many2One(
        'country.country', 'Country', required=True,
        help="Country code to be used as the prefix for the Federation"
             "ID account.")

    code = fields.Function(fields.Char('Code'), 'get_country_code')

    use_citizenship = fields.Boolean(
        'Use Citizenship',
        help="If this option is set\n"
             "the default country code will be the person citizenship.\n"
             "This feature is reserved for future use")

    def get_country_code(self, name):
        return self.country.code3


class Party(metaclass=PoolMeta):
    __name__ = 'party.party'

    def person_age(self, name):
        return compute_age_from_dates(
            self.dob, self.deceased,
            self.dod, self.gender, name, None)

    def get_du_address(self, name):
        if (self.du):
            return self.du.address_repr

    person_names = fields.One2Many(
        'gnuhealth.person_name', 'party', 'Person Names',
        states={'invisible': Not(Bool(Eval('is_person')))})

    name_representation = fields.Selection([
        (None, ''),
        ('pgfs', 'Prefix Given Family, Suffix'),
        ('gf', 'Given Family'),
        ('fg', 'Family, Given'),
        ('cjk', 'CJK: Family+Given'),
    ], 'Name Representation', sort=False,
        states={'invisible': Not(Bool(Eval('is_person')))})

    activation_date = fields.Date(
        'Activation date', help='Date of activation of the party')

    federation_account = fields.Char(
        'Federation ID',
        help='Federation Account',
        states={'invisible': Not(Bool(Eval('is_person')))})

    ref = fields.Char(
        'PUID',
        help='Person Unique Identifier',
        states={'invisible': Not(Bool(Eval('is_person')))})

    unidentified = fields.Boolean(
        'Unidentified',
        help='Patient is currently unidentified',
        states={'invisible': Not(Bool(Eval('is_person')))})

    is_person = fields.Boolean(
        'Person',
        help='Check if the party is a person.')

    is_patient = fields.Boolean(
        'Patient',
        states={'invisible': Not(Bool(Eval('is_person')))},
        help='Check if the party is a patient')

    is_healthprof = fields.Boolean(
        'Health Prof',
        states={'invisible': Not(Bool(Eval('is_person')))},
        help='Check if the party is a health professional')

    is_institution = fields.Boolean(
        'Institution', help='Check if the party is a Health Care Institution')
    is_insurance_company = fields.Boolean(
        'Insurance Company', help='Check if the party is an Insurance Company')
    is_pharmacy = fields.Boolean(
        'Pharmacy', help='Check if the party is a Pharmacy')

    lastname = fields.Char(
        'Family names', help='Family or last names',
        states={'invisible': Not(Bool(Eval('is_person')))})
    dob = fields.Date('DoB', help='Date of Birth')
    est_dob = fields.Boolean('Est', help="Estimated from referred years")
    est_years = fields.Integer('Years', help="Referred years")

    age = fields.Function(fields.Char('Age'), 'person_age')

    gender = fields.Selection([
        (None, ''),
        ('m', 'Male'),
        ('f', 'Female'),
        ('nb', 'Non-binary'),
        ('other', 'Other'),
        ('nd', 'Non disclosed'),
        ('u', 'Unknown')
    ], 'Gender', states={'required': Bool(Eval('is_person'))})

    gender_str = gender.translated('gender')

    photo = fields.Binary('Picture')
    ethnic_group = fields.Many2One('gnuhealth.ethnicity', 'Ethnicity')
    proclaimed_ethnicity = fields.Many2One(
        'gnuhealth.ethnicity', 'Proclaimed ethnicity',
        help="The ethnic group the person identifies most"
    )

    marital_status = fields.Selection([
        (None, ''),
        ('s', 'Single'),
        ('m', 'Married'),
        ('c', 'Concubinage'),
        ('w', 'Widowed'),
        ('d', 'Divorced'),
        ('x', 'Separated'),
    ], 'Marital Status', sort=False)

    marital_status_str = marital_status.translated('marital_status')

    citizenship = fields.Many2One(
        'country.country', 'Citizenship', help='Country of Citizenship')
    residence = fields.Many2One(
        'country.country', 'Residence', help='Country of Residence')
    alternative_identification = fields.Boolean(
        'Other IDs', help='Other types of '
        'identification, not the official PUID . '
        'Examples : Passport, foreign ID,..',
        states={'invisible': Not(Bool(Eval('is_person')))})

    alternative_ids = fields.One2Many(
        'gnuhealth.person_alternative_identification',
        'party', 'Other IDs',
        states={'invisible': Not(Bool(Eval('alternative_identification')))})

    insurance = fields.One2Many(
        'gnuhealth.insurance', 'party', 'Insurances',
        help="Insurance Plans associated to this party")

    internal_user = fields.Many2One(
        'res.user', 'Internal User',
        help='In GNU Health is the user (person) '
        'that logins. When the'
        ' party is a person, it will be the user'
        ' that maps the party.',
        states={
            'invisible': Not(Bool(Eval('is_person'))),
        })

    insurance_company_type = fields.Selection([
        (None, ''),
        ('state', 'State'),
        ('labour_union', 'Labour Union / Syndical'),
        ('private', 'Private'),
    ], 'Insurance Type')
    insurance_plan_ids = fields.One2Many(
        'gnuhealth.insurance.plan', 'company', 'Insurance Plans')

    homeless = fields.Boolean(
        "Homeless", help="The person is currently homeless")

    du = fields.Many2One('gnuhealth.du', 'DU', help="Domiciliary Unit")

    du_address = fields.Function(
        fields.Text(
            'Main address',
            help="Main Address, based on the associated DU"), 'get_du_address')

    birth_certificate = fields.Many2One(
        'gnuhealth.birth_certificate',
        'Birth Certificate', readonly=True)

    deceased = fields.Boolean(
        'Deceased', readonly=True,
        help='The information is updated from the Death Certificate',
        states={'invisible': Not(Bool(Eval('deceased')))})

    dod = fields.Function(fields.DateTime(
        'Date of Death',
        states={
            'invisible': Not(Bool(Eval('deceased'))),
        },
        depends=['deceased']), 'get_dod')

    death_certificate = fields.Many2One(
        'gnuhealth.death_certificate',
        'Death Certificate', readonly=True)

    mother = fields.Function(
        fields.Many2One(
            'party.party', 'Mother',
            help="Mother from the Birth Certificate"), 'get_mother')

    father = fields.Function(
        fields.Many2One(
            'party.party', 'Father',
            help="Father from the Birth Certificate"), 'get_father')

    fed_country = fields.Char(
        'Prefix',
        help="3-letter Country code"
        "in ISO 3166-1 alpha-3 standard that will become the prefix"
        "of the federation account. The following user-assigned codes"
        "ranges can be also used"
        "AAA to AAZ, QMA to QZZ, XAA to XZZ, and ZZA to ZZZ."
        "For example XXX is unidentified nationality and XXB is a refugee."
        "By default, it will use the code of the emiting institution country"
        "Refer to the GNU Health manual for further information",
        states={'invisible': Not(Bool(Eval('is_person')))})

    create_target = fields.Boolean(
        'Create target',
        help="By default, the associated target (eg, patient) "
             "will be created, unless this option is unchecked. "
             "You should uncheck this field if, for example, the "
             "person is a relative but will not be part of the "
             "health system.")

    def get_mother(self, name):
        if (self.birth_certificate and self.birth_certificate.mother):
            return self.birth_certificate.mother.id

    def get_father(self, name):
        if (self.birth_certificate and self.birth_certificate.father):
            return self.birth_certificate.father.id

    def get_dod(self, name):
        if (self.deceased and self.death_certificate):
            return self.death_certificate.dod

    @staticmethod
    def default_create_target():
        return True

    @staticmethod
    def default_fed_country():
        Fedcountry = Pool().get('gnuhealth.federation.country.config')(1)
        if (Fedcountry and Fedcountry.country):
            return Fedcountry.code

    # Use the Federation country as default value for citizenship
    # and residence for newly created people
    @staticmethod
    def default_citizenship():
        Fedcountry = Pool().get('gnuhealth.federation.country.config')(1)
        if (Fedcountry and Fedcountry.country):
            return int(Fedcountry.country)

    @staticmethod
    def default_residence():
        Fedcountry = Pool().get('gnuhealth.federation.country.config')(1)
        if (Fedcountry and Fedcountry.country):
            return int(Fedcountry.country)

    @staticmethod
    def default_activation_date():
        return date.today()

    @classmethod
    def generate_puid(cls):
        # Add a default random string in the ref field.
        # The STRSIZE constant provides the length of the PUID
        # The format of the PUID is XXXNNNXXX
        # By default, this field will be used only if nothing is entered

        STRSIZE = 9
        puid = ''
        for x in range(STRSIZE):
            if (x < 3 or x > 5):
                puid = puid + secrets.choice(string.ascii_uppercase)
            else:
                puid = puid + secrets.choice(string.digits)
        return puid

    @classmethod
    def convert_photo(cls, data):
        if data and Image:
            image = Image.open(BytesIO(data))
            image.thumbnail((200, 200), Image.LANCZOS)
            data = BytesIO()
            image.save(data, image.format)
            data = fields.Binary.cast(data.getvalue())
        return data

    @classmethod
    def write(cls, *args):
        actions = iter(args)
        args = []
        for parties, vals in zip(actions, actions):
            given_name = family_name = ''
            vals = vals.copy()
            person_id = parties[0].id
            # We set the value to None to make the fields that have a
            # unique constraint get the NULL value at PostgreSQL level, and not
            # the value '' coming from the client
            if vals.get('ref') == '':
                vals['ref'] = None

            if vals.get('federation_account') == '':
                vals['federation_account'] = None

            if 'photo' in vals:
                vals['photo'] = cls.convert_photo(vals['photo'])

            if ('name' in vals) or ('lastname' in vals):
                given_name = family_name = ''
                if 'name' in vals:
                    given_name = vals['name']
                if 'lastname' in vals:
                    family_name = vals['lastname']

                if parties[0].is_person:
                    cls.update_person_official_name(person_id, given_name,
                                                    family_name)

            args.append(parties)
            args.append(vals)

        return super(Party, cls).write(*args)

    @classmethod
    def update_person_official_name(cls, person_id, given_name, family_name):
        # Create / update the official PersonName entry with the Given / Family
        # names from the main entry field.
        person = []

        Pname = Pool().get('gnuhealth.person_name')
        officialnames = Pname.search(
            [("party", "=", person_id), ("use", "=", 'official')],)

        # If no official name found, create a new record
        if not (officialnames):
            values = {
                'party': person_id,
                'use': 'official',
            }

            if given_name:
                values['given'] = given_name
            if family_name:
                values['family'] = family_name

            person.append(values)
            Pname.create(person)

        # Found a related official name record, then
        # update official Person Name(s) when modified in main form
        else:
            official_rec = []
            official_rec.append(officialnames[0])

            values = {'use': 'official'}

            if given_name:
                values['given'] = given_name
            if family_name:
                values['family'] = family_name

            Pname.write(official_rec, values)

    @classmethod
    def create(cls, vlist):
        vlist = [x.copy() for x in vlist]

        tmp_act = cls.generate_puid()

        for values in vlist:
            if not values.get('ref'):
                if values.get('federation_account'):
                    # Strip the country code from the fed account
                    # and pass it to the local PUID
                    values['ref'] = values.get('federation_account')[3:]
                else:
                    values['ref'] = tmp_act
                if 'unidentified' in values and values['unidentified']:
                    values['ref'] = 'NN-' + values.get('ref')
                if 'is_person' in values and not values['is_person']:
                    values['ref'] = 'NP-' + values['ref']

            # Generate the Federation account ID
            # with the ISO 3166-1 alpha-3 as prefix
            # using the same code as in the newly created PUID
            # If the person is NN or there is no country assigned
            # use the prefix XXX
            if not values.get('federation_account') and \
                    values.get('is_person'):
                federation_account = tmp_act
                values['federation_account'] = \
                    (values['fed_country'] or "XXX") + federation_account

            # Set the value to None to make the fields that have a
            # unique constraint get the NULL value at PostgreSQL level, and not
            # the value '' coming from the client
            if values.get('federation_account') == '':
                values['federation_account'] = None

            # Generate internal code
            if not values.get('code'):
                # Use the company name . Initially, use the name
                # since the company hasn't been created yet.
                suffix = Transaction().context.get('company.rec_name') \
                    or values['name']
                # Generate the party code in the form of
                # "UUID-" . Where company is the name of the Health
                # Institution.
                #
                # The field "code" is the one that is used in distributed
                # environments, with multiple GNU Health instances across
                # a country / region
                values['code'] = '%s-%s' % (uuid4(), suffix)

            values.setdefault('addresses', None)

            if 'photo' in values:
                values['photo'] = cls.convert_photo(values['photo'])

            # If the party is a physical person,
            # add new PersonName record with the given and family name
            # as the official name

            if (values.get('is_person')):
                if ('name' in values) or ('lastname' in values):
                    official_name = []
                    given_name = family_name = ''

                    if 'name' in values:
                        given_name = values['name']
                    if 'lastname' in values:
                        family_name = values['lastname']

                    official_name.append(('create', [{
                        'use': 'official',
                        'given': given_name,
                        'family': family_name,
                    }]))

                    values['person_names'] = official_name

            # Create party before so we can assign it to the patient
            parties = super(Party, cls).create(vlist)
            party = parties[0]

            if (values.get('create_target') and party):
                # If the party creation was ok
                # and create_target is checked (default), then create
                # the related entity
                entity = None
                if values.get('is_patient'):
                    entity = 'patient'

                if entity:
                    cls.generate_target(party, entity)

        return parties

    @classmethod
    def generate_target(cls, party, entity):
        """ The method will generate the new entity depending on the
            attributes from the party (patient, institution, healthprof..)
            We initially start with the patient.
        """
        Target = None
        values = []

        # When entity is a patient
        if (entity == 'patient'):
            Target = Pool().get('gnuhealth.patient')
            values.append({'party': party.id})

        # TODO: Add more entities (health prof, institutions)
        # Warning: We have to make sure Target has no required fields
        # (like the insitution code) In that case, we need to provide it

        # Finally, create the target with their field values
        if (Target and values):
            Target.create(values)

    @classmethod
    def copy(cls, parties, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('ref', None)
        return super().copy(parties, default=default)

    @classmethod
    def __setup__(cls):
        super(Party, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('ref_uniq', Unique(t, t.ref), 'The PUID must be unique'),
            ('internal_user_uniq', Unique(t, t.internal_user),
                'This internal user is already assigned to a party'),
            ('federation_account_uniq',
                Unique(t, t.federation_account),
                'The Federation Account must be unique'), ]

        cls._order.insert(0, ('lastname', 'ASC'))
        cls._order.insert(1, ('name', 'ASC'))
        # Sort to be used when called from other models.
        cls._order_name = 'lastname'

    def get_rec_name(self, name):
        # Display name on the following sequence
        # 1 - Oficial Name from PersonName with the name representation
        # If not offficial name :
        # 2 - Last name, First name

        if self.person_names:
            prefix = given = family = suffix = ''
            for pname in self.person_names:
                if pname.prefix:
                    prefix = pname.prefix + ' '
                if pname.suffix:
                    suffix = ', ' + pname.suffix

                given = pname.given or ''
                family = pname.family or ''

                res = ''
                if pname.use == 'official':
                    if self.name_representation == 'pgfs':
                        res = prefix + given + ' ' + family + suffix
                    if self.name_representation == 'gf':
                        if pname.family:
                            family = ' ' + pname.family
                        res = given + family
                    if self.name_representation == 'fg':
                        if pname.family:
                            family = pname.family + ', '
                        res = family + given

                    if self.name_representation == 'cjk':
                        if pname.family:
                            family = pname.family
                        res = family + given

                    if not self.name_representation:
                        # Default value
                        if family:
                            return family + ', ' + given
                        else:
                            return given
                return res

        if self.lastname:
            return self.lastname + ', ' + self.name
        else:
            return self.name

    @classmethod
    def search_rec_name(cls, name, clause):
        """ Search for the name, lastname, PUID, any alternative IDs,
            and any family and / or given name from the person_names
        """
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('ref',) + tuple(clause[1:]),
                ('alternative_ids.code',) + tuple(clause[1:]),
                ('federation_account',) + tuple(clause[1:]),
                ('contact_mechanisms.value',) + tuple(clause[1:]),
                ('person_names.family',) + tuple(clause[1:]),
                ('person_names.given',) + tuple(clause[1:]),
                ('insurance.number',) + tuple(clause[1:]),
                ('name',) + tuple(clause[1:]),
                ('lastname',) + tuple(clause[1:]),
                ]

    @fields.depends('is_person', 'is_patient', 'is_healthprof')
    def on_change_with_is_person(self):
        # Set is_person if the party is a health professional or a patient
        if (self.is_healthprof or self.is_patient or self.is_person):
            return True

    # Set patient attribute if create_target and is_person attributes are set
    @fields.depends('is_person', 'create_target')
    def on_change_with_is_patient(self):
        if (self.create_target and self.is_person):
            return True

    @fields.depends('du', '_parent_du.name')
    def on_change_with_du_address(self):
        if (self.du):
            return self.get_du_address(name=None)

    @fields.depends('est_years', 'dob', 'deceased', 'dod', 'gender')
    def on_change_est_years(self):
        if (self.est_years):
            self.dob = estimated_date_from_years(self.est_years)
            self.est_dob = True
            self.age = self.person_age(name='age')
            # Resets the referred age in years to None after it computes
            # the age, so the form won't confuse the reader.
            self.est_years = None

    @fields.depends('dob', 'deceased', 'dod', 'gender')
    def on_change_dob(self):
        """ Automatically show the age in Y-M-D format upon
            entering the date of birth
        """
        self.age = self.person_age(name='age')
        self.est_years = None

    @classmethod
    def validate(cls, parties):
        super(Party, cls).validate(parties)
        for party in parties:
            party.check_person()
            party.validate_official_name()
            party.validate_dob()

    def validate_dob(self):
        """
            Check that the date is sane
                * The person is alive
                * Non-negative years, months or days
                * < 200 (future generations :) )
        """
        if (self.dob):
            years, months, days = \
                compute_age_from_dates(self.dob, self.deceased,
                                       self.dod, self.gender, 'raw_age', None)

            if (not self.deceased):
                if (years < 0 or months < 0 or days < 0) or years > 200:
                    raise WrongDateofBirth(
                        gettext('health.msg_wrong_date_of_birth')
                    )

    def check_person(self):
        # Verify that health professional and patient
        # are unchecked when is_person is False

        if not self.is_person and (self.is_patient or self.is_healthprof):
            raise MustBeAPerson(
                gettext('health.msg_must_be_a_person')
            )

    def validate_official_name(self):
        # Only allow one official name on the party name
        Pname = Pool().get('gnuhealth.person_name')
        officialnames = Pname.search_count(
            [("party", "=", self.id), ("use", "=", 'official')],)

        if (officialnames > 1):
            raise DupOfficialName(
                gettext('health.msg_dup_official_name')
            )

    @classmethod
    def view_attributes(cls):
        # Hide the group holding all the demographics when the party is not
        # a person
        return super(Party, cls).view_attributes() + [
            ('//group[@id="person_details"]', 'states', {
                'invisible': ~Eval('is_person'),
            })]


class PageOfLife(ModelSQL, ModelView):
    'Page of Life'
    __name__ = 'gnuhealth.pol'

    def age_at_page(self):
        if (self.person.dob and self.page_date):
            return compute_age_from_dates(
                self.person.dob, None,
                None, None, 'age', self.page_date.date())

    person = fields.Many2One(
        'party.party', 'Person', required=True,
        domain=[
            ('is_person', '=', True),
        ],
        states={'readonly': Eval('id', 0) > 0},
        help="Person")

    federation_account = fields.Char(
        'Account',
        required=True, help="Federation Account")

    page = fields.Char('Page', help="Page of Life")

    page_date = fields.DateTime(
        'Date',
        help="Date of this page / event. By default it takes"
             "the current date / time when creating the event,"
             "but it can be a past date "
             "and it can also be empty, or used in combination with"
             "the person age at that moment")

    age = fields.Char('Age',
                      help="Age at the moment of this page of life")

    page_type = fields.Selection([
        (None, ''),
        ('biographical', 'Biographical'),
        ('medical', 'Medical'),
        ('demographical', 'Demographical'),
        ('social', 'Social'),
    ], 'Page type', sort=False, required=True)

    medical_context = fields.Selection([
        (None, ''),
        ('health_condition', 'Health Condition'),
        ('encounter', 'Encounter'),
        ('procedure', 'Procedure'),
        ('self_monitoring', 'Self monitoring'),
        ('immunization', 'Immunization'),
        ('prescription', 'Prescription'),
        ('surgery', 'Surgery'),
        ('hospitalization', 'Hospitalization'),
        ('lab', 'lab'),
        ('dx_imaging', 'Dx Imaging'),
        ('genetics', 'Genetics'),
        ('family', 'Family history'),
        ('birth', 'Birth'),
        ('death', 'Death'),
    ], 'Medical Context', sort=False,
        states={'required': Equal(Eval('page_type'), 'medical'),
                'invisible': Not(Equal(Eval('page_type'), 'medical'))
                })

    social_context = fields.Selection([
        (None, ''),
        ('social_gradient', 'Social Gradient / Equity'),
        ('stress', 'Stress'),
        ('early_life_development', 'Early life development'),
        ('social_exclusion', 'Social exclusion'),
        ('working_conditions', 'Working conditions'),
        ('education', 'Education'),
        ('physical_environment', 'Physical environment'),
        ('unemployment', 'unemployment'),
        ('social_support', 'Social Support'),
        ('addiction', 'Addiction'),
        ('food', 'Food'),
        ('transport', 'transport'),
        ('health_services', 'Health services'),
        ('uninsured', 'uninsured'),
        ('family_functionality', 'Family functionality'),
        ('family_violence', 'Family violence'),
        ('bullying', 'Bullying'),
        ('war', 'War'),
    ], 'Social Context', sort=False,
        states={'required': Equal(Eval('page_type'), 'social'),
                'invisible': Not(Equal(Eval('page_type'), 'social'))
                })

    relevance = fields.Selection([
        (None, ''),
        ('informational', 'Informational'),
        ('important', 'Important'),
        ('critical', 'Critical'),
    ], 'Relevance', sort=False, required=True)

    health_condition = fields.Many2One(
        'gnuhealth.pathology', 'Condition',
        help='Health Condition')

    health_condition_code = fields.Char("Code")

    health_condition_text = fields.Char(
        "Condition",
        help='Health Condition')

    procedure = fields.Many2One(
        'gnuhealth.procedure', 'Procedure', help="Procedure code")

    procedure_text = fields.Char("Procedure")
    procedure_code = fields.Char("Procedure Code")

    gene = fields.Char(
        "Gene", help="Gene / protein",
        states={'invisible': Not(Equal(Eval('medical_context'), 'genetics'))})

    natural_variant = fields.Char(
        "Natural variant",
        states={'invisible': Not(Equal(Eval('medical_context'), 'genetics'))})

    phenotype = fields.Char(
        "Phenotype",
        states={'invisible': Not(Equal(Eval('medical_context'), 'genetics'))})

    summary = fields.Char("Summary")
    measurements = fields.Char("Measurements", readonly=True,
                               help="Automatically included measurements"
                               " such vital signs, HR, RR and anthropometrics")
    info = fields.Text("Extended Information")

    institution = fields.Many2One('gnuhealth.institution', 'Institution')
    node = fields.Char("Node")
    author = fields.Char("Author")
    author_acct = fields.Char("Author account")

    @staticmethod
    def default_institution():
        return get_institution()

    @staticmethod
    def default_page():
        return str(uuid4())

    @staticmethod
    def default_page_date():
        return datetime.now()

    # Get the text representation of the health condition
    @fields.depends('health_condition')
    def on_change_health_condition(self):
        if (self.health_condition):
            self.health_condition_text = self.health_condition.name
            self.health_condition_code = self.health_condition.code

    # Get the text representation of the procedure
    @fields.depends('procedure')
    def on_change_procedure(self):
        if (self.procedure):
            self.procedure_text = self.procedure.description
            self.procedure_code = self.procedure.name

    # Get the text representation of the institution
    @fields.depends('institution')
    def on_change_institution(self):
        if (self.institution):
            self.node = str(self.institution.party.name)

    # Retrieve the federation account
    @fields.depends('person')
    def on_change_person(self):
        if (self.person):
            self.federation_account = self.person.federation_account

    @fields.depends('person', 'page_date')
    def on_change_with_age(self):
        if (self.person and self.page_date):
            computed_age = self.age_at_page()
            return computed_age

    @classmethod
    def validate(cls, pages):
        super(PageOfLife, cls).validate(pages)
        for page in pages:
            page.validate_account()

    def validate_account(self):
        if (self.person.federation_account != self.federation_account):
            raise FedAccountMismatch(
                gettext('health.msg_fed_account_mismatch')
            )

    @classmethod
    def create(cls, vlist):
        Party = Pool().get('party.party')
        vlist = [x.copy() for x in vlist]
        for values in vlist:
            if values.get('federation_account') and not values.get('person'):
                party, = Party.search(
                    [("name", "=", values.get('federation_account'))], limit=1)

                values['person'] = party

            if 'page' not in values:
                values['page'] = str(uuid4())
        return super(PageOfLife, cls).create(vlist)

    @classmethod
    def __setup__(cls):
        super(PageOfLife, cls).__setup__()
        t = cls.__table__()

        cls._sql_constraints = [
            ('page_uniq', Unique(t, t.page), 'The Page must be unique !'),
        ]

        cls._order.insert(0, ('page_date', 'DESC'))

        # Do not cache default_key as it depends on time
        cls.__rpc__['default_get'].cache = None


class ContactMechanism(metaclass=PoolMeta):
    __name__ = 'party.contact_mechanism'

    emergency = fields.Boolean('Emergency')
    remarks = fields.Char('Remarks', help="Enter the name of the contact"
                          " or other remarks")


class PersonName(ModelSQL, ModelView):
    "Person Name"
    __name__ = 'gnuhealth.person_name'

    """ We are using the concept of HumanName on HL7 FHIR
    http://www.hl7.org/implement/standards/fhir/datatypes.html#HumanName
    """

    party = fields.Many2One('party.party', 'Person',
                            domain=[('is_person', '=', True)],
                            help="Related party (person)")

    use = fields.Selection([
        (None, ''),
        ('official', 'Official'),
        ('usual', 'Usual'),
        ('nickname', 'Nickname'),
        ('maiden', 'Maiden'),
        ('anonymous', 'Anonymous'),
        ('temp', 'Temp'),
        ('old', 'old'),
    ], 'Use', sort=False, required=True)

    family = fields.Char('Family', help="Family / Surname.")

    given = fields.Char(
        'Given',
        help="Given / First name. May include middle name", required=True)

    prefix = fields.Selection([
        (None, ''),
        ('Mr', 'Mr'),
        ('Mrs', 'Mrs'),
        ('Miss', 'Miss'),
        ('Dr', 'Dr'),
    ], 'Prefix', sort=False)

    suffix = fields.Char('Suffix')
    date_from = fields.Date('From')
    date_to = fields.Date('To')


class PartyAddress(metaclass=PoolMeta):
    __name__ = 'party.address'

    relationship = fields.Char(
        'Relationship',
        help='Include the relationship with the person - friend, co-worker,'
        ' brother,...')
    relative_id = fields.Many2One(
        'party.party', 'Contact', domain=[('is_person', '=', True)],
        help='Include link to the relative')

    is_school = fields.Boolean(
        "School", help="Check this box to mark the school address")
    is_work = fields.Boolean(
        "Work", help="Check this box to mark the work address")


class DrugDoseUnits(ModelSQL, ModelView):
    'Drug Dose Unit'
    __name__ = 'gnuhealth.dose.unit'

    code = fields.Char('Code', required=True, translate=False)
    name = fields.Char('Unit', required=True, translate=True)
    desc = fields.Char('Description', translate=True)

    @classmethod
    def __setup__(cls):
        super(DrugDoseUnits, cls).__setup__()
        t = cls.__table__()

        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.name), 'The Unit must be unique !'),
            ('code_uniq', Unique(t, t.code), 'The Code must be unique !'),
        ]


class MedicationFrequency(ModelSQL, ModelView):
    'Medication Common Frequencies'
    __name__ = 'gnuhealth.medication.dosage'

    name = fields.Char(
        'Frequency', required=True, translate=True,
        help='Common frequency name')
    code = fields.Char(
        'Code', required=True,
        help='Dosage Code,for example: SNOMED 229798009 = 3 times per day.'
             'Please use CAPITAL LETTERS and no spaces')
    abbreviation = fields.Char(
        'Abbreviation',
        help='Dosage abbreviation, such as tid in the US or tds in the UK')

    @classmethod
    def __setup__(cls):
        super(MedicationFrequency, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.name), 'The Unit must be unique !'),
            ('code_uniq', Unique(t, t.code), 'The CODE must be unique !'),
        ]


class DrugForm(ModelSQL, ModelView):
    'Drug Form'
    __name__ = 'gnuhealth.drug.form'

    name = fields.Char('Form', required=True, translate=True)
    code = fields.Char('Code', required=True,
                       help="Please use CAPITAL LETTERS and no spaces")

    @classmethod
    def __setup__(cls):
        super(DrugForm, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.name), 'The Unit must be unique !'),
            ('code_uniq', Unique(t, t.code), 'The CODE must be unique !'),
        ]


class DrugRoute(ModelSQL, ModelView):
    'Drug Administration Route'
    __name__ = 'gnuhealth.drug.route'

    name = fields.Char('Route', required=True, translate=True)
    code = fields.Char('Code', required=True,
                       help="Please use CAPITAL LETTERS and no spaces")

    @classmethod
    def __setup__(cls):
        super(DrugRoute, cls).__setup__()
        t = cls.__table__()

        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.name), 'The Name must be unique !'),
            ('code_uniq', Unique(t, t.code), 'The CODE must be unique !'),
        ]


class Occupation(ModelSQL, ModelView):
    'Occupation'
    __name__ = 'gnuhealth.occupation'

    name = fields.Char('Name', required=True, translate=True)
    code = fields.Char('Code', required=True,
                       help="Please use CAPITAL LETTERS and no spaces")

    @classmethod
    def __setup__(cls):
        super(Occupation, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.name), 'The Name must be unique !'),
            ('code_uniq', Unique(t, t.code), 'The CODE must be unique !'),
        ]


class Ethnicity(ModelSQL, ModelView):
    'Ethnicity'
    __name__ = 'gnuhealth.ethnicity'

    name = fields.Char('Name', required=True, translate=True)
    code = fields.Char('Code', required=True,
                       help="Please use CAPITAL LETTERS and no spaces")

    notes = fields.Char('Notes')

    @classmethod
    def __setup__(cls):
        super(Ethnicity, cls).__setup__()
        t = cls.__table__()

        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.name), 'The Name must be unique !'),
            ('code_uniq', Unique(t, t.code), 'The CODE must be unique !'),

        ]


class OperationalArea(ModelSQL, ModelView):
    'Operational Area'
    __name__ = 'gnuhealth.operational_area'

    name = fields.Char(
        'Name', required=True, help='Operational Area of the city or region')

    operational_sector = fields.One2Many(
        'gnuhealth.operational_sector', 'operational_area',
        'Operational Sector', readonly=True)

    info = fields.Text('Extra Information')

    @classmethod
    def __setup__(cls):
        super(OperationalArea, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('name_uniq', Unique(t, t.name),
                'The operational area must be unique !'),
        ]


class OperationalSector(ModelSQL, ModelView):
    'Operational Sector'
    __name__ = 'gnuhealth.operational_sector'

    name = fields.Char(
        'Op. Sector', required=True,
        help='Region included in an operational area')

    operational_area = fields.Many2One(
        'gnuhealth.operational_area', 'Operational Area')

    info = fields.Text('Extra Information')

    @classmethod
    def __setup__(cls):
        super(OperationalSector, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('name_uniq', Unique(t, t.name, t.operational_area),
                'The operational sector must be unique in each'
                ' operational area!'),
        ]


# HEALTH INSTITUTION
class HealthInstitution(ModelSQL, ModelView):
    'Health Institution'
    __name__ = 'gnuhealth.institution'

    party = fields.Many2One(
        'party.party', 'Institution',
        domain=[('is_institution', '=', True)],
        help='Party Associated to this Health Institution',
        required=True,
        states={'readonly': Bool(Eval('party'))})

    code = fields.Char('Code', required=True,
                       help="Institution code")

    picture = fields.Binary('Picture')

    institution_type = fields.Selection((
        (None, ''),
        ('doctor_office', 'Doctor office'),
        ('primary_care', 'Primary Care Center'),
        ('clinic', 'Clinic'),
        ('hospital', 'General Hospital'),
        ('specialized', 'Specialized Hospital'),
        ('nursing_home', 'Nursing Home'),
        ('hospice', 'Hospice'),
        ('rural', 'Rural facility'),
    ), 'Type', required=True, sort=False)

    beds = fields.Integer("Beds")

    operating_room = fields.Boolean(
        "Op. Room",
        help="Check this box if the institution "
        "has operating rooms")

    or_number = fields.Integer(
        "ORs", states={'invisible': Not(Bool(Eval('operating_room')))})

    public_level = fields.Selection((
        (None, ''),
        ('private', 'Private'),
        ('public', 'Public'),
        ('mixed', 'Private - State'),
    ), 'Public Level', required=True, sort=False)

    teaching = fields.Boolean("Teaching", help="Mark if this is a"
                              " teaching institution")
    heliport = fields.Boolean("Heliport")
    trauma_center = fields.Boolean("Trauma Center")
    trauma_level = fields.Selection((
        (None, ''),
        ('one', 'Level I'),
        ('two', 'Level II'),
        ('three', 'Level III'),
        ('four', 'Level IV'),
        ('five', 'Level V'),
    ), 'Trauma Level', sort=False,
        states={'invisible': Not(Bool(Eval('trauma_center')))})

    extra_info = fields.Text("Extra Info")

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

    @classmethod
    def __setup__(cls):
        super(HealthInstitution, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.party),
             'This Institution already exists !'),
            ('code_uniq', Unique(t, t.code), 'This CODE already exists !'),
        ]

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to party
        if (table_h.column_exist('name')
                and not table_h.column_exist('party')):
            table_h.column_rename('name', 'party')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class HealthInstitutionSpecialties(ModelSQL, ModelView):
    'Health Institution Specialties'
    __name__ = 'gnuhealth.institution.specialties'

    institution = fields.Many2One('gnuhealth.institution', 'Institution',
                                  required=True)
    specialty = fields.Many2One('gnuhealth.specialty', 'Specialty',
                                required=True)

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

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to institution
        if (table_h.column_exist('name')
                and not table_h.column_exist('institution')):
            table_h.column_rename('name', 'institution')

        super().__register__(module)
        table_h = cls.__table_handler__(module)

    @classmethod
    def __setup__(cls):
        super(HealthInstitutionSpecialties, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_sp_uniq', Unique(t, t.institution, t.specialty),
             'The Specialty already exists for this institution'),
        ]


class HealthInstitutionOperationalSector(ModelSQL, ModelView):
    'Operational Sectors covered by Institution'
    __name__ = 'gnuhealth.institution.operationalsector'

    institution = fields.Many2One('gnuhealth.institution', 'Institution',
                                  required=True)
    operational_sector = fields.Many2One('gnuhealth.operational_sector',
                                         'Operational Sector', required=True)

    @classmethod
    def __setup__(cls):
        super(HealthInstitutionOperationalSector, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_os_uniq', Unique(t, t.institution, t.operational_sector),
                'The Operational Sector already exists for this institution'),
        ]

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to institution
        if (table_h.column_exist('name')
                and not table_h.column_exist('institution')):
            table_h.column_rename('name', 'institution')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class HealthInstitutionO2M(ModelSQL, ModelView):
    'Health Institution'
    __name__ = 'gnuhealth.institution'

    # Add Specialties to the Health Institution
    specialties = fields.One2Many(
        'gnuhealth.institution.specialties',
        'institution', 'Specialties',
        help="Specialties Provided in this Health Institution")

    main_specialty = fields.Many2One(
        'gnuhealth.institution.specialties',
        'Specialty',
        domain=[('institution', '=', Eval('id'))],
        depends=['specialties', 'institution_type', 'id'],
        help="Choose the speciality in the case of Specialized Hospitals"
             " or where this center excels",

        # Allow to select the institution specialty only if the record already
        # exists
        states={'required': And(Eval('institution_type') == 'specialized',
                                Eval('id', 0) > 0),
                'readonly': Eval('id', 0) < 0})

    # Add Specialties to the Health Institution
    operational_sectors = fields.One2Many(
        'gnuhealth.institution.operationalsector',
        'institution', 'Operational Sector',
        help="Operational Sectors covered by this institution")


# HEALTH CENTER / HOSPITAL INFRASTRUCTURE
class HospitalBuilding(ModelSQL, ModelView):
    'Hospital Building'
    __name__ = 'gnuhealth.hospital.building'

    name = fields.Char(
        'Name', required=True,
        help='Name of the building within the institution')

    institution = fields.Many2One(
        'gnuhealth.institution', 'Institution', required=True,
        help='Health Institution of this building')

    code = fields.Char('Code', required=True)
    extra_info = fields.Text('Extra Info')

    @staticmethod
    def default_institution():
        return get_institution()

    @classmethod
    def __setup__(cls):
        super(HospitalBuilding, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.name, t.institution),
                'The Building name must be unique per Health'
                ' Center'),
            ('code_uniq', Unique(t, t.code, t.institution),
                'The Building code must be unique per Health'
                ' Center'),
        ]


class HospitalUnit(ModelSQL, ModelView):
    'Hospital Unit'
    __name__ = 'gnuhealth.hospital.unit'

    name = fields.Char(
        'Name', required=True,
        help='Name of the unit, eg Neonatal, Intensive Care, ...')

    institution = fields.Many2One(
        'gnuhealth.institution', 'Institution', required=True,
        help='Health Institution')

    code = fields.Char('Code', required=True)
    extra_info = fields.Text('Extra Info')

    @staticmethod
    def default_institution():
        return get_institution()

    @classmethod
    def __setup__(cls):
        super(HospitalUnit, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.name, t.institution),
                'The Unit NAME must be unique per Health'
                ' Center'),
            ('code_uniq', Unique(t, t.code, t.institution),
                'The Unit CODE must be unique per Health'
                ' Center'),
        ]


class HospitalOR(ModelSQL, ModelView):
    'Operating Room'
    __name__ = 'gnuhealth.hospital.or'

    name = fields.Char(
        'Name', required=True, help='Name of the Operating Room')

    institution = fields.Many2One(
        'gnuhealth.institution', 'Institution', required=True,
        help='Health Institution')

    building = fields.Many2One(
        'gnuhealth.hospital.building', 'Building',
        domain=[('institution', '=', Eval('institution'))],
        depends=['institution'])

    unit = fields.Many2One(
        'gnuhealth.hospital.unit', 'Unit',
        domain=[('institution', '=', Eval('institution'))],
        depends=['institution'])

    extra_info = fields.Text('Extra Info')

    timeslot = fields.Integer(
        "Time span", help="Default reservation"
        " time in minutes for this operating room."
        " It will be added automatically to the start time of the"
        " programmed surgery and the OR Schedule")

    state = fields.Selection((
        (None, ''),
        ('free', 'Free'),
        ('scheduled', 'Scheduled'),
        ('confirmed', 'Confirmed'),
        ('occupied', 'Occupied'),
        ('na', 'Not available'),
    ), 'Status', sort=False)

    @staticmethod
    def default_institution():
        return get_institution()

    @staticmethod
    def default_timeslot():
        return 60

    @staticmethod
    def default_state():
        return 'free'

    @classmethod
    def __setup__(cls):
        super(HospitalOR, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.name, t.institution),
                'The Operating Room Name must be unique per Health'
                ' Center'),
        ]


class HospitalWard(ModelSQL, ModelView):
    'Hospital Ward'
    __name__ = 'gnuhealth.hospital.ward'

    name = fields.Char('Name', required=True, help='Ward / Room code')

    institution = fields.Many2One(
        'gnuhealth.institution', 'Institution', required=True,
        help='Health Institution')

    building = fields.Many2One(
        'gnuhealth.hospital.building', 'Building',
        domain=[('institution', '=', Eval('institution'))],
        depends=['institution'])

    floor = fields.Integer('Floor Number')
    unit = fields.Many2One('gnuhealth.hospital.unit', 'Unit',
                           domain=[('institution', '=', Eval('institution'))],
                           depends=['institution'])

    private = fields.Boolean(
        'Private', help='Check this option for private room')

    bio_hazard = fields.Boolean(
        'Bio Hazard', help='Check this option if there is biological hazard')

    number_of_beds = fields.Integer(
        'Number of beds', help='Number of patients per ward')

    telephone = fields.Boolean('Telephone access')
    ac = fields.Boolean('Air Conditioning')
    private_bathroom = fields.Boolean('Private Bathroom')
    guest_sofa = fields.Boolean('Guest sofa-bed')
    tv = fields.Boolean('Television')
    internet = fields.Boolean('Internet Access')
    refrigerator = fields.Boolean('Refrigerator')
    microwave = fields.Boolean('Microwave')

    gender = fields.Selection((
        (None, ''),
        ('men', 'Men Ward'),
        ('women', 'Women Ward'),
        ('unisex', 'Unisex'),
    ), 'Gender', required=True, sort=False)

    state = fields.Selection((
        (None, ''),
        ('beds_available', 'Beds available'),
        ('full', 'Full'),
        ('na', 'Not available'),
    ), 'Status', sort=False)

    extra_info = fields.Text('Extra Info')

    @staticmethod
    def default_gender():
        return 'unisex'

    @staticmethod
    def default_number_of_beds():
        return 1

    @staticmethod
    def default_institution():
        return get_institution()

    @classmethod
    def __setup__(cls):
        super(HospitalWard, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.name, t.institution),
                'The Ward / Room Name must be unique per Health'
                ' Center'),
        ]


class HospitalBed(ModelSQL, ModelView):
    'Hospital Bed'
    __name__ = 'gnuhealth.hospital.bed'

    product = fields.Many2One(
        'product.product', 'Bed', required=True,
        domain=[('is_bed', '=', True)],
        help='Bed Number')

    institution = fields.Many2One(
        'gnuhealth.institution', 'Institution', required=True,
        help='Health Institution')

    ward = fields.Many2One(
        'gnuhealth.hospital.ward', 'Ward',
        domain=[('institution', '=', Eval('institution'))],
        depends=['institution'],
        help='Ward or room')

    bed_type = fields.Selection((
        (None, ''),
        ('gatch', 'Gatch Bed'),
        ('electric', 'Electric'),
        ('stretcher', 'Stretcher'),
        ('low', 'Low Bed'),
        ('low_air_loss', 'Low Air Loss'),
        ('circo_electric', 'Circo Electric'),
        ('clinitron', 'Clinitron'),
    ), 'Bed Type', required=True, sort=False)

    telephone_number = fields.Char(
        'Telephone Number', help='Telephone number / Extension')

    extra_info = fields.Text('Extra Info')

    state = fields.Selection((
        (None, ''),
        ('free', 'Free'),
        ('reserved', 'Reserved'),
        ('occupied', 'Occupied'),
        ('to_clean', 'Needs cleaning'),
        ('na', 'Not available'),
    ), 'Status', readonly=True, sort=False)

    @staticmethod
    def default_bed_type():
        return 'gatch'

    @staticmethod
    def default_state():
        return 'free'

    @staticmethod
    def default_institution():
        return get_institution()

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

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

    @classmethod
    def __setup__(cls):
        super(HospitalBed, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.product, t.institution),
                'The Bed must be unique per Health Center'),
        ]
        # Show fix button when is in state "needs cleaning" or "NA"
        cls._buttons.update({
            'fix_bed': {
                'invisible': Or(Equal(Eval('state'), 'free'),
                                Equal(Eval('state'), 'occupied'),
                                Equal(Eval('state'), 'reserved')
                                )},
        }),

    @classmethod
    @ModelView.button
    def fix_bed(cls, beds):
        cls.write(beds, {'state': 'free'})

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to product
        if (table_h.column_exist('name')
                and not table_h.column_exist('product')):
            table_h.column_rename('name', 'product')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class MedicalSpecialty(ModelSQL, ModelView):
    'Medical Specialty'
    __name__ = 'gnuhealth.specialty'

    name = fields.Char(
        'Specialty', required=True, translate=True,
        help='ie, Addiction Psychiatry')
    code = fields.Char('Code', required=True,
                       help='ie, ADP. Please use CAPITAL LETTERS '
                       'and no spaces')

    @classmethod
    def __setup__(cls):
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.name), 'The Specialty must be unique !'),
            ('code_uniq', Unique(t, t.code), 'The CODE must be unique !'),
        ]
        super(MedicalSpecialty, cls).__setup__()


class HealthProfessional(ModelSQL, ModelView):
    'Health Professional'
    __name__ = 'gnuhealth.healthprofessional'

    party = fields.Many2One(
        'party.party', 'Health Prof', required=True,
        domain=[
            ('is_healthprof', '=', True),
            ('is_person', '=', True),
        ],
        help="Health Professional related party")

    institution = fields.Many2One(
        'gnuhealth.institution', 'Institution',
        help='Main institution where she/he works')

    institution_unit = fields.Many2One(
        'gnuhealth.hospital.unit', 'Unit',
        domain=[('institution', '=', Eval('institution'))],
        depends=['institution'])

    code = fields.Char('LICENSE ID', help='License ID')

    specialties = fields.One2Many(
        'gnuhealth.hp_specialty', 'healthprof', 'Specialties')

    info = fields.Text('Extra info')

    puid = fields.Function(
        fields.Char('PUID', help="Person Unique Identifier"),
        'get_hp_puid', searcher='search_hp_puid')

    active = fields.Boolean('Active')

    """
    main_specialty = fields.Many2One(
        'gnuhealth.hp_specialty', 'Main Specialty',
        domain=[('healthprof', '=', Eval('id'))],
        states={'readonly': Eval('id', 0) < 0},
        depends=['id'])
    """
    main_specialty = fields.Function(fields.Many2One(
        'gnuhealth.hp_specialty', 'Main Specialty'),
        'get_main_specialty')

    @staticmethod
    def default_active():
        return True

    def get_hp_puid(self, name):
        return self.party.ref

    def get_main_specialty(self, name):
        if (len(self.specialties) == 1):
            return self.specialties[0].id
        if (len(self.specialties) > 1):
            for sp in self.specialties:
                if (sp.mainsp):
                    return sp

    @staticmethod
    def default_institution():
        return get_institution()

    @classmethod
    def validate(cls, healthprofs):
        super(HealthProfessional, cls).validate(healthprofs)
        for hp in healthprofs:
            hp.validate_specialties()

    def validate_specialties(self):
        counter = 0
        for sp in self.specialties:
            if (sp.mainsp):
                counter = counter + 1
            if counter > 1:
                raise DupMainSP(
                    gettext('health.msg_dup_mainsp')
                )

    @classmethod
    def search_hp_puid(cls, name, clause):
        res = []
        value = clause[2]
        res.append(('party.ref', clause[1], value))
        return res

    @classmethod
    def __setup__(cls):
        super(HealthProfessional, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('hp_uniq', Unique(t, t.party),
                'The health professional must be unique'),
            ('code_uniq', Unique(t, t.code),
                'The LICENSE ID must be unique'),
        ]

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to party
        if (table_h.column_exist('name')
                and not table_h.column_exist('party')):
            table_h.column_rename('name', 'party')

        super().__register__(module)
        table_h = cls.__table_handler__(module)

    def get_rec_name(self, name):
        if self.party:
            res = self.party.rec_name
        return res

    # Execute when creating a new record
    @classmethod
    def create(cls, vlist):
        vlist = [x.copy() for x in vlist]
        # Use None instead of '' to allow null values in code
        # yet enforcing the unique constraint on the license ID
        for values in vlist:
            if values.get('code') == '':
                values['code'] = None
        return super(HealthProfessional, cls).create(vlist)

    # Execute on update record
    @classmethod
    def write(cls, healthprofs, values):
        # Use None instead of '' to allow null values in code
        # yet enforcing the unique constraint on the license ID
        for healthprof in healthprofs:
            if values.get('code') == '':
                values['code'] = None
        return super(HealthProfessional, cls).write(healthprofs, values)

    # Allow to search by party attributes and license ID
    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('code',) + tuple(clause[1:]),
                ('party',) + tuple(clause[1:]),
                ]


class HealthProfessionalSpecialties(ModelSQL, ModelView):
    'Health Professional Specialties'
    __name__ = 'gnuhealth.hp_specialty'

    healthprof = fields.Many2One(
        'gnuhealth.healthprofessional',
        'Health Prof', required=True,
        help='Health Professional')

    specialty = fields.Many2One(
        'gnuhealth.specialty', 'Specialty', required=True,
        help='Specialty Code')

    mainsp = fields.Boolean(
        'Main', help="Set this field if this is the main specialty or the one"
        " used in this health institution")

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

    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('specialty',) + tuple(clause[1:]),
                ]

    @classmethod
    def __setup__(cls):
        super(HealthProfessionalSpecialties, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.healthprof, t.specialty),
             'This specialty is already assigned to the Health Professional'),
        ]

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to healthprof
        if (table_h.column_exist('name')
                and not table_h.column_exist('healthprof')):
            table_h.column_rename('name', 'healthprof')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class Family(ModelSQL, ModelView):
    'Family'
    __name__ = 'gnuhealth.family'

    name = fields.Char('Family', required=True, help='Family code')

    members = fields.One2Many(
        'gnuhealth.family_member', 'family', 'Family Members')

    info = fields.Text('Extra Information')

    @classmethod
    def __setup__(cls):
        super(Family, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.name),
             'The Family Code must be unique !'),
        ]


class FamilyMember(ModelSQL, ModelView):
    'Family Member'
    __name__ = 'gnuhealth.family_member'

    family = fields.Many2One(
        'gnuhealth.family', 'Family', required=True, readonly=True,
        help='Family code')

    party = fields.Many2One(
        'party.party', 'Party', required=True,
        domain=[('is_person', '=', True)],
        help='Family Member')

    role = fields.Char('Role', help='Father, Mother, sibbling...')

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to family
        if (table_h.column_exist('name')
                and not table_h.column_exist('family')):
            table_h.column_rename('name', 'family')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class FamilyDiseases(ModelSQL, ModelView):
    'Family History'
    __name__ = 'gnuhealth.patient.family.diseases'

    patient = fields.Many2One('gnuhealth.patient', 'Patient')
    disease = fields.Many2One(
        'gnuhealth.pathology', 'Condition', required=True)
    xory = fields.Selection([
        (None, ''),
        ('m', 'Maternal'),
        ('f', 'Paternal'),
        ('s', 'Sibling'),
    ], 'Maternal or Paternal')

    xory_str = xory.translated('xory')

    relative = fields.Selection([
        ('mother', 'Mother'),
        ('father', 'Father'),
        ('brother', 'Brother'),
        ('sister', 'Sister'),
        ('daughter', 'Daughter'),
        ('son', 'Son'),
        ('aunt', 'Aunt'),
        ('uncle', 'Uncle'),
        ('nephew', 'Nephew'),
        ('niece', 'Niece'),
        ('grandfather', 'Grandfather'),
        ('grandmother', 'Grandmother'),
        ('cousin', 'Cousin'),
    ], 'Relative',
        help='First degree = siblings, parents and children '
             '(sons/daughters)\n'
             'Second degree = Grandparents, aunts/uncles, nephews and nieces\n'
             'Third degree = Great-grandparents and first cousins',
        required=True)

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to patient
        if (table_h.column_exist('name')
                and not table_h.column_exist('disease')):
            table_h.column_rename('name', 'disease')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


# Use the template as in Product category.
class MedicamentCategory(tree(separator=' / '), ModelSQL, ModelView):
    'Medicament Category'
    __name__ = 'gnuhealth.medicament.category'

    name = fields.Char('Name', required=True, translate=True)

    parent = fields.Many2One(
        'gnuhealth.medicament.category', 'Parent')

    childs = fields.One2Many(
        'gnuhealth.medicament.category', 'parent', string='Children')

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

    def get_rec_name(self, name):
        if self.parent:
            return self.parent.get_rec_name(name) + ' / ' + self.name
        else:
            return self.name


class Medicament(ModelSQL, ModelView):
    'Medicament'
    __name__ = 'gnuhealth.medicament'

    product = fields.Many2One(
        'product.product', 'Product', required=True,
        domain=[('is_medicament', '=', True)],
        help='Product Name')

    active_component = fields.Char(
        'Active component', translate=True,
        help='Active Component')

    category = fields.Many2One(
        'gnuhealth.medicament.category', 'Category')

    therapeutic_action = fields.Char(
        'Therapeutic effect', help='Therapeutic action')

    composition = fields.Text('Composition', help='Components')
    indications = fields.Text('Indication', help='Indications')
    strength = fields.Float(
        'Strength',
        help='Amount of medication (eg, 250 mg) per dose')

    unit = fields.Many2One(
        'gnuhealth.dose.unit', 'dose unit',
        help='Unit of measure for the medication to be taken')

    route = fields.Many2One(
        'gnuhealth.drug.route', 'Administration Route',
        help='Drug administration route code.')

    form = fields.Many2One(
        'gnuhealth.drug.form', 'Form',
        help='Drug form, such as tablet, suspension, liquid ..')

    sol_conc = fields.Float(
        'Concentration',
        help='Solution drug concentration')

    sol_conc_unit = fields.Many2One(
        'gnuhealth.dose.unit', 'Unit',
        help='Unit of the drug concentration')

    sol_vol = fields.Float(
        'Volume',
        help='Solution concentration volume')

    sol_vol_unit = fields.Many2One(
        'gnuhealth.dose.unit', 'Unit',
        help='Unit of the solution volume')

    dosage = fields.Text('Dosage Instructions', help='Dosage / Indications')
    overdosage = fields.Text('Overdosage', help='Overdosage')
    pregnancy_warning = fields.Boolean(
        'Pregnancy Warning',
        help='The drug represents risk to pregnancy or lactancy')

    pregnancy = fields.Text(
        'Pregnancy and Lactancy', help='Warnings for Pregnant Women')

    pregnancy_category = fields.Selection([
        (None, ''),
        ('A', 'A'),
        ('B', 'B'),
        ('C', 'C'),
        ('D', 'D'),
        ('X', 'X'),
        ('N', 'N'),
    ], 'Pregnancy Category',
        help='** FDA Pregnancy Categories ***\n'
        'CATEGORY A :Adequate and well-controlled human studies have failed'
        ' to demonstrate a risk to the fetus in the first trimester of'
        ' pregnancy (and there is no evidence of risk in later'
        ' trimesters).\n\n'
        'CATEGORY B : Animal reproduction studies have failed todemonstrate a'
        ' risk to the fetus and there are no adequate and well-controlled'
        ' studies in pregnant women OR Animal studies have shown an adverse'
        ' effect, but adequate and well-controlled studies in pregnant women'
        ' have failed to demonstrate a risk to the fetus in any'
        ' trimester.\n\n'
        'CATEGORY C : Animal reproduction studies have shown an adverse'
        ' effect on the fetus and there are no adequate and well-controlled'
        ' studies in humans, but potential benefits may warrant use of the'
        ' drug in pregnant women despite potential risks. \n\n '
        'CATEGORY D : There is positive evidence of human fetal  risk based'
        ' on adverse reaction data from investigational or marketing'
        ' experience or studies in humans, but potential benefits may warrant'
        ' use of the drug in pregnant women despite potential risks.\n\n'
        'CATEGORY X : Studies in animals or humans have demonstrated fetal'
        ' abnormalities and/or there is positive evidence of human fetal risk'
        ' based on adverse reaction data from investigational or marketing'
        ' experience, and the risks involved in use of the drug in pregnant'
        ' women clearly outweigh potential benefits.\n\n'
        'CATEGORY N : Not yet classified')

    presentation = fields.Text('Presentation', help='Packaging')
    adverse_reaction = fields.Text('Adverse Reactions')
    storage = fields.Text('Storage Conditions')
    is_vaccine = fields.Boolean('Vaccine')
    notes = fields.Text('Extra Info')

    active = fields.Boolean('Active')

    @staticmethod
    def default_active():
        return True

    # Show the icon depending on the pregnancy category
    pregnancy_cat_icon = \
        fields.Function(fields.Char('Preg. Cat. Icon'), 'get_preg_cat_icon')

    def get_preg_cat_icon(self, name):
        if (self.pregnancy_category == 'X'):
            return 'gnuhealth-stop'
        if (self.pregnancy_category == 'D' or self.pregnancy_category == "C"):
            return 'gnuhealth-warning'

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

    # Allow to search by name, active component or category
    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('product',) + tuple(clause[1:]),
                ('active_component',) + tuple(clause[1:]),
                ('category',) + tuple(clause[1:]),
                ]

    @classmethod
    def check_xml_record(cls, records, values):
        return True

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to product
        if (table_h.column_exist('name')
                and not table_h.column_exist('product')):
            table_h.column_rename('name', 'product')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class ImmunizationScheduleDose(ModelSQL, ModelView):
    'Immunization Schedule Dose'
    __name__ = 'gnuhealth.immunization_schedule_dose'

    vaccine = fields.Many2One(
        'gnuhealth.immunization_schedule_line', 'Vaccine', required=True,
        help='Vaccine Name')
    dose_number = fields.Integer('Dose', required=True)
    age_dose = fields.Integer('Age', required=True)
    age_unit = fields.Selection([
        (None, ''),
        ('days', 'days'),
        ('weeks', 'weeks'),
        ('months', 'months'),
        ('years', 'years'),
    ], 'Time Unit', required=True)

    age_unit_str = age_unit.translated('age_unit')

    remarks = fields.Char('Remarks')

    sched = fields.Function(
        fields.Char('Schedule'), 'get_dose_schedule',
        searcher='search_dose_schedule')

    def get_dose_schedule(self, name):
        return self.vaccine.sched.sched

    @classmethod
    def search_dose_schedule(cls, name, clause):
        res = []
        value = clause[2]
        res.append(('vaccine.sched.sched', clause[1], value))
        return res

    @classmethod
    def __setup__(cls):
        super(ImmunizationScheduleDose, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('number_uniq', Unique(t, t.dose_number, t.vaccine),
                'The dose number must be unique for this vaccine'),
        ]
        cls._order.insert(0, ('vaccine', 'ASC'))
        cls._order.insert(1, ('dose_number', 'ASC'))


class ImmunizationScheduleLine(ModelSQL, ModelView):
    'Immunization Schedule Line'
    __name__ = 'gnuhealth.immunization_schedule_line'

    sched = fields.Many2One(
        'gnuhealth.immunization_schedule', 'Schedule', required=True,
        help='Schedule Name')

    vaccine = fields.Many2One(
        'gnuhealth.medicament', 'Vaccine', required=True,
        domain=[('is_vaccine', '=', True)],
        help='Vaccine Name')

    scope = fields.Selection([
        (None, ''),
        ('systematic', 'Systematic'),
        ('recommended', 'Recommended'),
        ('highrisk', 'Risk groups'),
    ], 'Scope', sort=False)

    scope_str = scope.translated('scope')

    remarks = fields.Char('Remarks')

    doses = fields.One2Many('gnuhealth.immunization_schedule_dose',
                            'vaccine', 'Doses')

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

    @staticmethod
    def default_scope():
        return 'systematic'


class ImmunizationSchedule(ModelSQL, ModelView):
    'Immunization Schedule'
    __name__ = 'gnuhealth.immunization_schedule'

    sched = fields.Char('Code',
                        help="Code for this immunization schedule",
                        required=True)

    country = fields.Many2One('country.country', 'Country')
    year = fields.Integer('Year')
    active = fields.Boolean('Active')

    vaccines = fields.One2Many('gnuhealth.immunization_schedule_line',
                               'sched', 'Vaccines')

    desc = fields.Char('Description',
                       help="Short Description for this immunization schedule",
                       required=True)

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

    @staticmethod
    def default_active():
        return True

    @classmethod
    def __setup__(cls):
        super(ImmunizationSchedule, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('sched_uniq', Unique(t, t.sched),
             'The schedule code must be unique'),
        ]


class PathologyCategory(tree(separator=' / '), ModelSQL, ModelView):
    'Disease Categories'
    __name__ = 'gnuhealth.pathology.category'

    name = fields.Char('Category Name', required=True, translate=True)
    parent = fields.Many2One(
        'gnuhealth.pathology.category', 'Parent Category')

    childs = fields.One2Many(
        'gnuhealth.pathology.category', 'parent', 'Children Category')

    @classmethod
    def validate(cls, categories):
        super(PathologyCategory, cls).validate(categories)

    def get_rec_name(self, name):
        if self.parent:
            return self.parent.get_rec_name(name) + ' / ' + self.name
        else:
            return self.name

    @classmethod
    def __setup__(cls):
        super(PathologyCategory, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('name_uniq', Unique(t, t.name),
             'The category name must be unique'),
        ]
        cls._order.insert(0, ('name', 'ASC'))


class PathologyGroup(ModelSQL, ModelView):
    'Pathology Groups'
    __name__ = 'gnuhealth.pathology.group'

    name = fields.Char(
        'Name', required=True, translate=True, help='Group name')

    code = fields.Char(
        'Code', required=True,
        help='for example MDG6 code will contain the Millennium Development'
        ' Goals # 6 diseases : Tuberculosis, Malaria and HIV/AIDS')

    desc = fields.Char(
        'Short Description', required=True, translate=True)
    info = fields.Text('Detailed information')

    members = fields.One2Many('gnuhealth.disease_group.members',
                              'disease_group', 'Members', readonly=True)

    @classmethod
    def __setup__(cls):
        super(PathologyGroup, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('code_uniq', Unique(t, t.code),
             'The Pathology Group code must be unique'),
        ]


class Pathology(ModelSQL, ModelView):
    'Health Conditions'
    __name__ = 'gnuhealth.pathology'

    name = fields.Char(
        'Name', required=True, translate=True, help='Health condition name')
    code = fields.Char(
        'Code', required=True,
        help='Specific Code for the Disease (eg, ICD-10)')
    category = fields.Many2One(
        'gnuhealth.pathology.category', 'Main Category',
        help='Select the main category for this disease This is usually'
        ' associated to the standard. For instance, the chapter on the ICD'
        ' will be the main category for de disease')

    groups = fields.One2Many(
        'gnuhealth.disease_group.members', 'disease',
        'Groups', help='Specify the groups this pathology belongs. Some'
        ' automated processes act upon the code of the group')

    chromosome = fields.Char('Affected Chromosome', help='chromosome number')
    protein = fields.Char(
        'Protein involved', help='Name of the protein(s) affected')
    gene = fields.Char('Gene', help='Name of the gene(s) affected')
    info = fields.Text('Extra Info')

    uri = fields.Char('URI', help="Resource / Entity URL address")

    active = fields.Boolean('Active')

    @staticmethod
    def default_active():
        return True

    # Include code + description in result
    def get_rec_name(self, name):
        return (self.code + ' : ' + self.name)

    # Search by the health condition code or the description
    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('code',) + tuple(clause[1:]),
                ('name',) + tuple(clause[1:]),
                ]

    @classmethod
    def __setup__(cls):
        super(Pathology, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('code_uniq', Unique(t, t.code),
             'The disease code must be unique'),
        ]


# DISEASE GROUP MEMBERS
class DiseaseMembers(ModelSQL, ModelView):
    'Disease group members'
    __name__ = 'gnuhealth.disease_group.members'

    disease = fields.Many2One('gnuhealth.pathology',
                              'Condition', readonly=True)
    disease_group = fields.Many2One(
        'gnuhealth.pathology.group', 'Group', required=True)

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to disease
        if (table_h.column_exist('name')
                and not table_h.column_exist('disease')):
            table_h.column_rename('name', 'disease')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class ProcedureCode(ModelSQL, ModelView):
    'Medical Procedures'
    __name__ = 'gnuhealth.procedure'

    name = fields.Char('Code', required=True)
    description = fields.Char('Long Text', translate=True)

    product = fields.Many2One(
        'product.product', 'Product',
        domain=[('type', '=', 'service')],
        help='Associated product to the procedure')

    # Include code + description in result
    def get_rec_name(self, name):
        return (self.name + ' : ' + self.description)

    # Search by the Procedure code or the description
    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('name',) + tuple(clause[1:]),
                ('description',) + tuple(clause[1:]),
                ]


# Add institution attribute AFTER registering the Health Institution
# Health Professionals and underlying conditions
class BirthCertExtraInfo (metaclass=PoolMeta):
    __name__ = 'gnuhealth.birth_certificate'

    STATES = {'readonly': Eval('state') == 'done'}

    institution = fields.Many2One(
        'gnuhealth.institution', 'Institution',
        states=STATES)

    signed_by = fields.Many2One(
        'gnuhealth.healthprofessional',
        'Certifier', readonly=True, help='Person who certifies this'
        ' birth document',
        states=STATES)

    certification_date = fields.DateTime('Signed on', readonly=True,
                                         states=STATES)

    @staticmethod
    def default_institution():
        return get_institution()

    @fields.depends('institution')
    def on_change_institution(self):
        country = None
        subdivision = None
        if (self.institution and self.institution.party.addresses[0].country):
            country = self.institution.party.addresses[0].country.id

        if (self.institution and self.institution.party.addresses[0].
                subdivision):
            subdivision = self.institution.party.addresses[0].subdivision.id

        self.country = country
        self.country_subdivision = subdivision

    @classmethod
    @ModelView.button
    def sign(cls, certificates):
        pool = Pool()
        Person = pool.get('party.party')
        party = []

        # Change the state of the birth certificate to "Signed"
        # and write the name of the certifying health professional

        signing_hp = get_health_professional()

        cls.write(certificates, {
            'state': 'signed',
            'signed_by': signing_hp,
            'certification_date': datetime.now()})

        party.append(certificates[0].party)

        Person.write(party, {
            'birth_certificate': certificates[0].id})


class DeathCertExtraInfo (metaclass=PoolMeta):
    __name__ = 'gnuhealth.death_certificate'

    STATES = {'readonly': Eval('state') == 'done'}

    institution = fields.Many2One(
        'gnuhealth.institution', 'Institution',
        states=STATES)

    signed_by = fields.Many2One(
        'gnuhealth.healthprofessional', 'Signed by', readonly=True,
        states={'invisible': Equal(Eval('state'), 'draft')},
        help="Health Professional that signed the death certificate")

    certification_date = fields.DateTime('Certified on', readonly=True)

    cod = fields.Many2One(
        'gnuhealth.pathology', 'Cause',
        required=True, help="Immediate Cause of Death",
        states=STATES)

    underlying_conditions = fields.One2Many(
        'gnuhealth.death_underlying_condition',
        'death_certificate', 'Underlying Conditions', help='Underlying'
        ' conditions that initiated the events resulting in death.'
        ' Please code them in sequential, chronological order',
        states=STATES)

    @staticmethod
    def default_institution():
        return get_institution()

    @staticmethod
    def default_state():
        return 'draft'

    @fields.depends('institution')
    def on_change_institution(self):
        country = None
        subdivision = None
        if (self.institution and self.institution.party.addresses[0].country):
            country = self.institution.party.addresses[0].country.id

        if (self.institution and self.institution.party.addresses[0].
                subdivision):
            subdivision = self.institution.party.addresses[0].subdivision.id

        self.country = country
        self.country_subdivision = subdivision

    @classmethod
    @ModelView.button
    def sign(cls, certificates):
        pool = Pool()
        Person = pool.get('party.party')

        # Change the state of the death certificate to "Signed"
        # and write the name of the certifying health professional

        # It also set the associated party attribute deceased to True.

        party = []

        signing_hp = get_health_professional()

        cls.write(certificates, {
            'state': 'signed',
            'signed_by': signing_hp,
            'certification_date': datetime.now()})

        party.append(certificates[0].party)

        Person.write(party, {
            'deceased': True,
            'death_certificate': certificates[0].id
        })


# UNDERLYING CONDITIONS THAT RESULT IN DEATH INCLUDED IN DEATH CERT.
class DeathUnderlyingCondition(ModelSQL, ModelView):
    'Underlying Conditions'
    __name__ = 'gnuhealth.death_underlying_condition'

    death_certificate = fields.Many2One(
        'gnuhealth.death_certificate', 'Certificate', readonly=True)

    condition = fields.Many2One(
        'gnuhealth.pathology', 'Condition', required=True)

    interval = fields.Integer('Interval', help='Approx Interval'
                              ' onset to death', required=True)

    unit_of_time = fields.Selection([
        (None, ''),
        ('minutes', 'minutes'),
        ('hours', 'hours'),
        ('days', 'days'),
        ('months', 'months'),
        ('years', 'years'),
    ], 'Unit', sort=False, required=True)

    unit_of_time_str = unit_of_time.translated('unit_of_time')


class InsurancePlan(ModelSQL, ModelView):
    'Insurance Plan'

    __name__ = 'gnuhealth.insurance.plan'

    product = fields.Many2One(
        'product.product', 'Plan', required=True,
        domain=[('is_insurance_plan', '=', True)],
        help='Insurance company plan')

    company = fields.Many2One(
        'party.party', 'Insurance Company', required=True,
        domain=[('is_insurance_company', '=', True)])

    is_default = fields.Boolean(
        'Default plan',
        help='Check if this is the default plan when assigning this insurance'
        ' company to a patient')

    notes = fields.Text('Extra info')

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

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to product
        if (table_h.column_exist('name')
                and not table_h.column_exist('product')):
            table_h.column_rename('name', 'product')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class Insurance(ModelSQL, ModelView):
    'Person Insurance'
    __name__ = 'gnuhealth.insurance'

    # Insurance associated to an individual

    party = fields.Many2One('party.party', 'Owner')
    number = fields.Char('Number', required=True)

    company = fields.Many2One(
        'party.party', 'Insurance Company',
        required=True,
        domain=[('is_insurance_company', '=', True)])

    member_since = fields.Date('Member since')
    member_exp = fields.Date('Expiration date')
    status = fields.Function(fields.Selection([
        (None, ''),
        ('valid', 'Valid'),
        ('grace', 'Grace'),
        ('expired', 'Expired'),
    ], 'Status'), 'check_insurance_status')

    category = fields.Char(
        'Category', help='Insurance company category')

    insurance_type = fields.Selection([
        (None, ''),
        ('state', 'State'),
        ('labour_union', 'Labour Union / Syndical'),
        ('private', 'Private'),
    ], 'Insurance Type')

    plan_id = fields.Many2One(
        'gnuhealth.insurance.plan', 'Plan',
        help='Insurance company plan',
        domain=[('company', '=', Eval('company'))],
        depends=['company'])

    notes = fields.Text('Extra Info')

    def check_insurance_status(self, name):
        if self.member_exp and (self.member_exp < date.today()):
            return 'expired'
        else:
            return 'valid'

    def get_rec_name(self, name):
        company_name = self.company and self.company.name or ''
        plan_name = self.plan_id and self.plan_id.rec_name or ''
        num = self.number
        return f"{company_name}: {plan_name} - {num} ({self.status})"

    @classmethod
    def __setup__(cls):
        super(Insurance, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('number_uniq', Unique(t, t.number, t.company),
             'The number must be unique per insurance company'),
        ]

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to party
        if (table_h.column_exist('name')
                and not table_h.column_exist('party')):
            table_h.column_rename('name', 'party')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class AlternativePersonID (ModelSQL, ModelView):
    'Alternative person ID'
    __name__ = 'gnuhealth.person_alternative_identification'

    party = fields.Many2One('party.party', 'Party', readonly=True)
    code = fields.Char('Code', required=True)
    alternative_id_type = fields.Selection(
        [
            (None, ''),
            ('country_id', 'Country of origin ID'),
            ('passport', 'Passport'),
            ('medical_record', 'Medical Record'),
            ('ghealth_federation', 'GNU Health Federation'),
            ('other', 'Other'),
        ], 'ID type', required=True, sort=False,)

    other_alternative_id_type = fields.Char(
        'Other ID type',
        help="Other Alternative ID type, "
        "user can customize an ID type "
        "when 'ID type' = 'other'."
    )

    expiration_date = fields.Date('Expiration date')

    comments = fields.Char('Comments')

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to party
        if (table_h.column_exist('name')
                and not table_h.column_exist('party')):
            table_h.column_rename('name', 'party')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class BirthCertificate (ModelSQL, ModelView):
    'Birth Certificate'
    __name__ = 'gnuhealth.birth_certificate'

    STATES = {'readonly': Eval('state') == 'done'}

    party = fields.Many2One(
        'party.party', 'Person',
        required=True, domain=[('is_person', '=', True), ],
        states={'readonly': Eval('id', 0) > 0})

    mother = fields.Many2One('party.party', 'Mother',
                             domain=[('is_person', '=', True), ],
                             states=STATES)

    father = fields.Many2One('party.party', 'Father',
                             domain=[('is_person', '=', True), ],
                             states=STATES)

    code = fields.Char('Code', required=True, states=STATES)

    dob = fields.Date('Date of Birth', required=True, states=STATES)

    observations = fields.Text('Observations', states=STATES)

    country = fields.Many2One('country.country', 'Country', required=True,
                              states=STATES)

    country_subdivision = fields.Many2One(
        'country.subdivision', 'Subdivision',
        domain=[('country', '=', Eval('country'))],
        depends=['country'],
        states=STATES)

    state = fields.Selection([
        (None, ''),
        ('draft', 'Draft'),
        ('signed', 'Signed'),
        ('done', 'Done'),
    ], 'State', readonly=True, sort=False)

    @staticmethod
    def default_state():
        return 'draft'

    @fields.depends('party')
    def on_change_with_dob(self):
        if (self.party and self.party.dob):
            dob = self.party.dob
            return dob

    @classmethod
    def __setup__(cls):
        super(BirthCertificate, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.party), 'Certificate already exists !'),
            ('code_uniq', Unique(t, t.code), 'Certificate already exists !'),
        ]

        cls._buttons.update({
            'sign': {'invisible': Or(Equal(Eval('state'), 'signed'),
                                     Equal(Eval('state'), 'done'))}
        })

    @classmethod
    def validate(cls, certificates):
        super(BirthCertificate, cls).validate(certificates)
        for certificate in certificates:
            certificate.validate_dob()

    def validate_dob(self):
        if (self.party.dob != self.dob):
            raise BirthCertDateMismatch(
                gettext('health.msg_birth_cert_date_mismatch')
            )

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to party
        if (table_h.column_exist('name')
                and not table_h.column_exist('party')):
            table_h.column_rename('name', 'party')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class DeathCertificate (ModelSQL, ModelView):
    'Death Certificate'
    __name__ = 'gnuhealth.death_certificate'

    STATES = {'readonly': Eval('state') == 'done'}

    party = fields.Many2One(
        'party.party', 'Person', required=True,
        domain=[('is_person', '=', True), ], states=STATES)

    code = fields.Char('Code', required=True, states=STATES)

    autopsy = fields.Boolean('Autopsy', help="Check this box "
                             "if autopsy has been done",
                             states=STATES)

    dod = fields.DateTime('Date', required=True,
                          help="Date and time of Death",
                          states=STATES)

    type_of_death = fields.Selection(
        [
            (None, ''),
            ('natural', 'Natural'),
            ('suicide', 'Suicide'),
            ('homicide', 'Homicide'),
            ('undetermined', 'Undetermined'),
            ('pending_investigation', 'Pending Investigation'),
        ], 'Type of death', required=True, sort=False,
        states=STATES)

    type_of_death_str = type_of_death.translated('type_of_death')

    place_of_death = fields.Selection(
        [
            (None, ''),
            ('home', 'Home'),
            ('work', 'Work'),
            ('public_place', 'Public place'),
            ('health_center', 'Health Center'),
        ], 'Place', required=True, sort=False,
        states=STATES)

    place_of_death_str = place_of_death.translated('place_of_death')

    operational_sector = fields.Many2One(
        'gnuhealth.operational_sector', 'Op. Sector',
        states=STATES)

    du = fields.Many2One(
        'gnuhealth.du', 'DU', help="Domiciliary Unit",
        states=STATES)

    place_details = fields.Char('Details',
                                states=STATES)

    country = fields.Many2One('country.country', 'Country', required=True,
                              states=STATES)

    country_subdivision = fields.Many2One(
        'country.subdivision', 'Subdivision',
        domain=[('country', '=', Eval('country'))],
        depends=['country'],
        states=STATES)

    age = fields.Function(fields.Char('Age'), 'get_age_at_death')

    observations = fields.Text('Observations',
                               states=STATES)

    state = fields.Selection([
        (None, ''),
        ('draft', 'Draft'),
        ('signed', 'Signed'),
        ('done', 'Done'),
    ], 'State', readonly=True, sort=False)

    @staticmethod
    def default_state():
        return 'draft'

    @classmethod
    def __setup__(cls):
        super(DeathCertificate, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.party), 'Certificate already exists !'),
            ('code_uniq', Unique(t, t.code), 'Certificate already exists !'),
        ]

        cls._buttons.update({
            'sign': {'invisible': Or(Equal(Eval('state'), 'signed'),
                                     Equal(Eval('state'), 'done'))}
        })

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to party
        if (table_h.column_exist('name')
                and not table_h.column_exist('party')):
            table_h.column_rename('name', 'party')

        super().__register__(module)
        table_h = cls.__table_handler__(module)

    def get_age_at_death(self, name):
        if (self.party.dob):
            delta = relativedelta(self.dod, self.party.dob)
            years_months_days = format_years_months_days(
                years=delta.years,
                months=delta.months,
                days=delta.days)
        else:
            years_months_days = None
        return years_months_days


class Product(ModelSQL, ModelView):
    'Product'
    __name__ = 'product.product'

    is_medicament = fields.Boolean(
        'Medicament', help='Check if the product is a medicament')
    is_medical_supply = fields.Boolean(
        'Medical Supply', help='Check if the product is a medical supply')
    is_vaccine = fields.Boolean(
        'Vaccine', help='Check if the product is a vaccine')
    is_bed = fields.Boolean(
        'Bed', help='Check if the product is a bed on the gnuhealth.center')
    is_insurance_plan = fields.Boolean(
        'Insurance Plan', help='Check if the product is an insurance plan')

    @classmethod
    def check_xml_record(cls, records, values):
        return True


# PATIENT GENERAL INFORMATION
class PatientData(ModelSQL, ModelView):
    'Patient related information'
    __name__ = 'gnuhealth.patient'

    @fields.depends('diseases', 'critical_summary')
    def on_change_diseases(self):
        ''' Update the patient critical information and "Focus on"
            fields when updating the diseases
        '''
        for disease in self.diseases:
            if (disease.pathology):
                for member in disease.pathology.groups:
                    if (member.disease_group.name == "ALLERGIC"):
                        ''' Set "Focus on allergy" if the health condition
                            is a member of the ALLERGIC disease group'''
                        self.crit_allergic = True

                    ''' Update patient critical information'''
                    self.critical_summary = \
                        self.patient_critical_summary(name=None)

    def patient_critical_summary(self, name):
        # Patient Critical Information Summary
        # The information will be shown in the front page

        allergies = []
        other_conditions = []
        conditions = []
        for disease in self.diseases:
            if (disease.pathology):
                for member in disease.pathology.groups:
                    '''Retrieve patient allergies'''
                    if (member.disease_group.name == "ALLERGIC"):
                        if disease.pathology.name not in conditions:
                            allergies.append(disease.pathology.rec_name)
                            conditions.append(disease.pathology.rec_name)

                '''Retrieve patient other relevant conditions '''
                '''Chronic and active'''
                if (disease.status == "c" or disease.is_active):
                    if disease.pathology.name not in conditions:
                        other_conditions.append(disease.pathology.rec_name)

        return "\n".join((set(allergies + other_conditions)))

    party = fields.Many2One(
        'party.party', 'Patient', required=True,
        domain=[
            ('is_patient', '=', True),
            ('is_person', '=', True),
        ],
        states={'readonly': Eval('id', 0) > 0},
        help="Person associated to this patient")

    lastname = fields.Function(
        fields.Char('Lastname'), 'get_patient_lastname',
        searcher='search_patient_lastname')

    puid = fields.Function(
        fields.Char('PUID', help="Person Unique Identifier"),
        'get_patient_puid', searcher='search_patient_puid')

    family = fields.Many2One(
        'gnuhealth.family', 'Family', help='Family Code')

    current_insurance = fields.Many2One(
        'gnuhealth.insurance', 'Insurance',
        domain=[('party', '=', Eval('party'))],
        depends=['party'],
        help='Insurance information. You may choose from the different'
        ' insurances belonging to the patient')

    insurances = fields.Function(
        fields.One2Many(
            'gnuhealth.insurance', 'party', 'Insurances',
            help="Insurance Plans associated to this party"), 'get_insurances')

    current_address = fields.Many2One(
        'party.address', 'Temp. Addr',
        domain=[('party', '=', Eval('party'))],
        depends=['party'],
        help="Use this address for temporary contact information. "
        "For example, if the patient is on vacation, you can put "
        "the hotel address. In the case of a Domiciliary Unit, "
        "just link it to the name of the contact in the address form.")
    primary_care_doctor = fields.Many2One(
        'gnuhealth.healthprofessional',
        'GP', help='Current General Practitioner / Family Doctor')

    # Removed in 2.0 . PHOTO It's now a functional field
    # Retrieves the information from the party.

    photo = fields.Function(fields.Binary('Picture'), 'get_patient_photo')

    # Removed in 2.0 . DOB It's now a functional field
    # Retrieves the information from the party.
    #    dob = fields.Date('DoB', help='Date of Birth')

    dob = fields.Function(fields.Date('DoB'), 'get_patient_dob')

    age = fields.Function(fields.Char('Age'), 'get_patient_age')

    gender = fields.Function(fields.Selection([
        (None, ''),
        ('m', 'Male'),
        ('f', 'Female'),
        ('nb', 'Non-binary'),
        ('other', 'Other'),
        ('nd', 'Non disclosed'),
        ('u', 'Unknown'),
        ('f-m', 'Female -> Male'),
        ('m-f', 'Male -> Female'),
    ], 'Gender'), 'get_patient_gender')

    gender_str = gender.translated('gender')

    biological_sex = fields.Selection([
        (None, ''),
        ('m', 'Male'),
        ('f', 'Female'),
    ], 'Biological Sex',
        help="Biological sex. By defaults it takes the value"
             " from the neonatal information")

    # Removed in 2.0 . MARITAL STATUS It's now a functional field
    # Retrieves the information from the party.

    marital_status = fields.Function(
        fields.Selection([
            (None, ''),
            ('s', 'Single'),
            ('m', 'Married'),
            ('c', 'Concubinage'),
            ('w', 'Widowed'),
            ('d', 'Divorced'),
            ('x', 'Separated'),
        ], 'Marital Status', sort=False, help="Marital Status"),
        'get_patient_marital_status')

    blood_type = fields.Selection([
        (None, ''),
        ('A', 'A'),
        ('B', 'B'),
        ('AB', 'AB'),
        ('O', 'O'),
    ], 'Blood Type', sort=False)

    rh = fields.Selection([
        (None, ''),
        ('+', '+'),
        ('-', '-'),
    ], 'Rh')

    # Used in report template.
    def get_report_blood_type(self):
        blood_type = self.blood_type
        rh = self.rh
        if blood_type and rh:
            return blood_type + ' ' + rh
        elif blood_type:
            return blood_type
        else:
            return ''

    hb = fields.Selection([
        (None, ''),
        ('aa', 'AA'),
        ('as', 'AS'),
        ('ss', 'SS'),
        ('sc', 'SC'),
        ('cc', 'CC'),
        ('athal', 'A-THAL'),
        ('bthal', 'B-THAL'),
    ], 'Hb',
        help="Clinically relevant Hemoglobin types\n"
             "AA = Normal Hemoglobin\n"
             "AS = Sickle Cell Trait\n"
             "SS = Sickle Cell Anemia\n"
             "AC = Sickle Cell Hemoglobin C Disease\n"
             "CC = Hemoglobin C Disease\n"
             "A-THAL = A Thalassemia groups\n"
             "B-THAL = B Thalassemia groups\n"
    )

    vaccinations = fields.One2Many(
        'gnuhealth.vaccination', 'patient', 'Vaccinations', readonly=True)
    medications = fields.One2Many(
        'gnuhealth.patient.medication', 'patient', 'Medications')

    diseases = fields.One2Many(
        'gnuhealth.patient.disease', 'patient',
        'Conditions')

    critical_summary = fields.Function(fields.Text(
        'Important health conditions related to this patient',
        help='Automated summary of patient important health conditions '
        'and other critical information'),
        'patient_critical_summary')

    critical_info = fields.Text(
        'Free text information not included in the automatic summary',
        help='Write any important information on the patient\'s condition,'
        ' surgeries, allergies, ...')

    general_info = fields.Text(
        'General Information',
        help='General information about the patient')

    deceased = fields.Function(
        fields.Boolean('Deceased'),
        'check_is_alive', searcher='search_patient_deceased')

    dod = fields.Function(fields.DateTime(
        'Date of Death',
        states={
            'invisible': Not(Bool(Eval('deceased'))),
        },
        depends=['deceased']), 'get_dod')

    cod = fields.Function(fields.Many2One(
        'gnuhealth.pathology', 'Cause of Death',
        states={
            'invisible': Not(Bool(Eval('deceased'))),
        },
        depends=['deceased']), 'get_cod')

    childbearing_age = fields.Function(
        fields.Boolean('Potential for Childbearing'), 'get_childbearing_age')

    appointments = fields.One2Many(
        'gnuhealth.appointment', 'patient', 'Appointments')

    active = fields.Boolean('Active')

    # General key information about the patient, independent
    # from coding systems
    crit_allergic = fields.Boolean(
        'Allergic',
        help="Known serious allergic reactions")
    crit_dbt = fields.Boolean('DBT', help="Diabetes")
    crit_hbp = fields.Boolean(
        'HBP',
        help="High blood pressure (hypertension)")
    crit_cardio = fields.Boolean(
        'Cardiovascular',
        help="Cardiovascular disease, excluding HBP, that has "
             " its own field.")
    crit_mental = fields.Boolean(
        'Mental',
        help="Mental Health")
    crit_nutrition = fields.Boolean(
        'Nutrition', help="Issues on nutrition, malnourhisment."
        " Including obesity, famine and eating disorders")
    crit_cancer = fields.Boolean('Cancer', help="Cancer")
    crit_immuno = fields.Boolean(
        'Immunity', help="Immunocompromised or autoimmune disorders")
    crit_cognitive = fields.Boolean(
        'Cognitive', help="Cognitive issues")
    crit_social = fields.Boolean(
        'Social',
        help="There are serious socio-familiar issues, such as"
             " physical and social barriers, drug addiction, violence "
             " and education")

    family_history = fields.One2Many('gnuhealth.patient.family.diseases',
                                     'patient', 'Family History')

    @staticmethod
    def default_active():
        return True

    @classmethod
    def __setup__(cls):
        super(PatientData, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_uniq', Unique(t, t.party), 'The Patient already exists !'),
        ]
        cls._order.insert(0, ('party', 'ASC'))

    def get_patient_dob(self, name):
        return self.party.dob

    def get_insurances(self, name):
        if (self.party):
            return self.party.insurance

    def get_patient_gender(self, name):
        gender = self.party.gender
        sex = self.biological_sex
        if sex:
            if (gender != sex and (gender in ['f', 'm'])):
                res = sex + '-' + gender
            else:
                res = gender
        else:
            res = gender
        return res

    def get_patient_photo(self, name):
        return self.party.photo

    def get_patient_puid(self, name):
        return self.party.ref

    def get_patient_marital_status(self, name):
        return self.party.marital_status

    def check_is_alive(self, name):
        return self.party.deceased

    def get_patient_age(self, name):
        return self.party.age

    def get_childbearing_age(self, name):
        return compute_age_from_dates(
            self.dob, self.deceased, self.dod, self.gender, name, None)

    def get_dod(self, name):
        if (self.deceased):
            return self.party.death_certificate.dod

    def get_cod(self, name):
        if (self.deceased):
            return self.party.death_certificate.cod.id

    # Show the gender upon entering the individual
    @fields.depends('party')
    def on_change_party(self):
        if (self.party):
            self.gender = self.party.gender
            self.age = self.party.age

    @classmethod
    def search_patient_puid(cls, name, clause):
        res = []
        value = clause[2]
        res.append(('party.ref', clause[1], value))
        return res

    def get_patient_lastname(self, name):
        return self.party.lastname

    @classmethod
    def search_patient_lastname(cls, name, clause):
        res = []
        value = clause[2]
        res.append(('party.lastname', clause[1], value))
        return res

    @classmethod
    def search_patient_deceased(cls, name, clause):
        res = []
        value = clause[2]
        res.append(('party.deceased', clause[1], value))
        return res

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

    # Search by the patient name, lastname or PUID
    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('puid',) + tuple(clause[1:]),
                ('party',) + tuple(clause[1:]),
                ('lastname',) + tuple(clause[1:]),
                ]

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to party
        if (table_h.column_exist('name')
                and not table_h.column_exist('party')):
            table_h.column_rename('name', 'party')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


# PATIENT CONDITIONS INFORMATION
class PatientDiseaseInfo(ModelSQL, ModelView):
    'Patient Conditions History'
    __name__ = 'gnuhealth.patient.disease'

    patient = fields.Many2One('gnuhealth.patient', 'Patient')

    pathology = fields.Many2One(
        'gnuhealth.pathology', 'Condition', required=True, help='Condition')

    disease_severity = fields.Selection([
        (None, ''),
        ('1_mi', 'Mild'),
        ('2_mo', 'Moderate'),
        ('3_sv', 'Severe'),
    ], 'Severity', sort=False)

    disease_severity_str = disease_severity.translated('disease_severity')

    is_on_treatment = fields.Boolean('Current', help="Currently on treatment")
    is_infectious = fields.Boolean(
        'Infectious',
        help='Check if the patient has an infectious / transmissible disease')

    short_comment = fields.Char(
        'Remarks',
        help='Brief, one-line remark of the disease. Longer description will'
        ' go on the Extra info field')

    healthprof = fields.Many2One(
        'gnuhealth.healthprofessional',
        'Health Prof',
        help='Health Professional who treated or diagnosed the patient')

    diagnosed_date = fields.Date('Dx date')
    healed_date = fields.Date('Healed')
    is_active = fields.Boolean('Active',
                               help="Check this box if the disease is active")

    age = fields.Integer(
        'Years',
        help='Estimated age of the diagnosis')

    age_str = fields.Function(fields.Char(
        'Age at dx',
        help='Patient age at the moment of the diagnosis, '
        'in most situations, this value is derived from patient evalution.'),
        'patient_age_at_dx',)

    est_dodx = fields.Boolean('Est', help="Estimated date of diagnosis"
                              "from referred years")

    pregnancy_warning = fields.Boolean('Pregnancy warning')
    weeks_of_pregnancy = fields.Integer('Contracted in pregnancy week #')
    is_allergy = fields.Boolean('Allergic Disease')
    allergy_type = fields.Selection([
        (None, ''),
        ('da', 'Drug Allergy'),
        ('fa', 'Food Allergy'),
        ('ma', 'Misc Allergy'),
        ('mc', 'Misc Contraindication'),
    ], 'Allergy type', sort=False)
    pcs_code = fields.Many2One(
        'gnuhealth.procedure', 'Code',
        help='Procedure code')
    treatment_description = fields.Char(
        'Description', help="Short description of the treatment")
    date_start_treatment = fields.Date('Start', help='Start of treatment date')
    date_stop_treatment = fields.Date('End', help='End of treatment date')
    status = fields.Selection([
        (None, ''),
        ('a', 'acute'),
        ('c', 'chronic'),
        ('u', 'unchanged'),
        ('h', 'healed'),
        ('i', 'improving'),
        ('w', 'worsening'),
    ], 'Status', sort=False)

    status_str = status.translated('status')

    extra_info = fields.Text('Extra Info')

    related_evaluations = fields.One2Many(
        'gnuhealth.patient.evaluation', 'related_condition',
        'Related Evaluations', readonly=True)

    institution = fields.Many2One('gnuhealth.institution', 'Institution')

    @staticmethod
    def default_institution():
        return get_institution()

    @staticmethod
    def default_is_active():
        return True

    @classmethod
    def validate(cls, diseases):
        super(PatientDiseaseInfo, cls).validate(diseases)
        for disease in diseases:
            disease.validate_disease_period()
            disease.validate_treatment_dates()

    def validate_disease_period(self):
        if (self.healed_date and self.diagnosed_date):
            if (self.healed_date < self.diagnosed_date):
                raise DateHealedBeforeDx(
                    gettext('health.msg_healed_before_dx',
                            healed_date=self.healed_date,
                            diagnosed_date=self.diagnosed_date,
                            )
                )

    def validate_treatment_dates(self):
        if (self.date_stop_treatment and self.date_start_treatment):
            if (self.date_stop_treatment < self.date_start_treatment):
                raise EndTreatmentDateBeforeStart(
                    gettext('health.msg_end_treatment_before_start',
                            date_stop_treatment=self.date_stop_treatment,
                            date_start_treatment=self.date_start_treatment,
                            )
                )

    # Show warning on infectious disease
    infectious_disease_icon = \
        fields.Function(fields.Char('Infect'), 'get_infect_disease_icon')

    def get_infect_disease_icon(self, name):
        if (self.is_infectious):
            return 'gnuhealth-warning'

    @staticmethod
    def default_healthprof():
        return get_health_professional()

    @staticmethod
    def default_diagnosed_date():
        return date.today()

    @fields.depends('diagnosed_date', 'age_str', 'patient',
                    '_parent_patient.dob')
    def on_change_diagnosed_date(self):
        if (self.patient):
            self.age_str = compute_age_from_dates(
                self.patient.dob, None, None, None, 'age',
                self.diagnosed_date)
            self.est_dodx = False

    @fields.depends('age', 'patient', '_parent_patient.dob', 'age_str')
    def on_change_age(self):
        if (self.age and self.patient.dob):
            self.est_dodx = True
            self.diagnosed_date = self.patient.dob + \
                relativedelta(years=self.age)
            self.age_str = compute_age_from_dates(
                self.patient.dob, None, None, None, 'age',
                self.diagnosed_date)
            self.age = None

    @fields.depends('age', 'patient', '_parent_patient.dob', 'diagnosed_date')
    def on_change_with_age_str(self):
        if (self.patient):
            if (self.diagnosed_date and self.patient.dob):
                return compute_age_from_dates(
                    self.patient.dob, None, None, None, 'age',
                    self.diagnosed_date)

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

    def patient_age_at_dx(self, name):
        if (self.patient.dob and self.diagnosed_date):
            return compute_age_from_dates(
                self.patient.dob, None, None, None, 'age',
                self.diagnosed_date)

    @classmethod
    def create_health_condition_pol(cls, condition_info):
        """ Adds an entry in the person Page of Life
            related to this person health condition
        """
        Pol = Pool().get('gnuhealth.pol')
        pol = []

        def patient_age_at_dx():
            age_at_dx = ''
            if condition_info.age_str:
                age_at_dx = condition_info.age_str
            elif condition_info.age:
                age_at_dx = format_years_months_days(
                    years=condition_info.age, months=0, days=0)
            elif (condition_info.patient.dob and
                  condition_info.diagnosed_date):
                age_at_dx = compute_age_from_dates(
                    condition_info.patient.dob, None, None, None,
                    'age', condition_info.diagnosed_date)
            return age_at_dx

        vals = {
            'page': str(uuid4()),
            'person': condition_info.patient.party.id,
            'age': patient_age_at_dx(),
            'federation_account':
                condition_info.patient.party.federation_account,
            'page_type': 'medical',
            'medical_context': 'health_condition',
            'relevance': 'important',
            'summary': condition_info.short_comment,
            'info': condition_info.extra_info,
            'author': condition_info.healthprof and
            condition_info.healthprof.party.rec_name,
            'node': condition_info.institution and
            condition_info.institution.party.name
        }
        if (condition_info.pathology):
            vals['health_condition'] = condition_info.pathology
            vals['health_condition_text'] = condition_info.pathology.rec_name
            vals['health_condition_code'] = condition_info.pathology.code

        pol.append(vals)
        Pol.create(pol)

    @classmethod
    def create(cls, vlist):

        # Execute first the creation of PoL
        health_condition_info = super(PatientDiseaseInfo, cls).create(vlist)

        cls.create_health_condition_pol(health_condition_info[0])

        return health_condition_info

    # Search by the patient name, lastname or PUID
    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('patient',) + tuple(clause[1:]),
                ('pathology',) + tuple(clause[1:]),
                ('healthprof',) + tuple(clause[1:]),
                ]

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to patient
        if (table_h.column_exist('name')
                and not table_h.column_exist('patient')):
            table_h.column_rename('name', 'patient')

        super().__register__(module)
        table_h = cls.__table_handler__(module)

    @classmethod
    def __setup__(cls):
        super(PatientDiseaseInfo, cls).__setup__()
        cls._order.insert(0, ('is_active', 'DESC'))
        cls._order.insert(1, ('disease_severity', 'DESC'))
        cls._order.insert(2, ('is_infectious', 'DESC'))
        cls._order.insert(3, ('diagnosed_date', 'DESC'))


# PATIENT APPOINTMENT
class Appointment(ModelSQL, ModelView):
    'Patient Appointments'
    __name__ = 'gnuhealth.appointment'

    name = fields.Char(
        'APPT ID',
        help='Appointment ID',
        readonly=True)

    healthprof = fields.Many2One(
        'gnuhealth.healthprofessional', 'Health Prof',
        help='Health Professional')

    patient = fields.Many2One(
        'gnuhealth.patient', 'Patient',
        help='Patient Name',
        states={'required': (Eval('state') != 'free')})

    appointment_date = fields.DateTime(
        'Start', help="Appintmont start date and time.")

    checked_in_date = fields.DateTime('Checked-in Time')

    institution = fields.Many2One(
        'gnuhealth.institution', 'Institution',
        help='Health Care Institution')

    speciality = fields.Many2One(
        'gnuhealth.specialty', 'Specialty',
        help='Medical Specialty / Sector')

    state = fields.Selection([
        (None, ''),
        ('free', 'Free'),
        ('confirmed', 'Confirmed'),
        ('checked_in', 'Checked in'),
        ('done', 'Done'),
        ('user_cancelled', 'Cancelled by patient'),
        ('center_cancelled', 'Cancelled by Health Center'),
        ('no_show', 'No show')
    ], 'State', sort=False)

    urgency = fields.Selection([
        (None, ''),
        ('a', 'Normal'),
        ('b', 'Urgent'),
        ('c', 'Medical Emergency'),
    ], 'Urgency', sort=False)

    comments = fields.Text('Comments')

    appointment_type = fields.Selection([
        (None, ''),
        ('outpatient', 'Outpatient'),
        ('telemedicine', 'Telemedicine'),
        ('homecare', 'Home Care'),
        ('inpatient', 'Inpatient'),
    ], 'Type', sort=False)

    visit_type = fields.Selection([
        (None, ''),
        ('new', 'New health condition'),
        ('followup', 'Followup'),
        ('well_child', 'Well Child visit'),
        ('well_woman', 'Well Woman visit'),
        ('well_man', 'Well Man visit'),
    ], 'Visit', sort=False)

    consultations = fields.Many2One(
        'product.product', 'Consultation Services',
        domain=[('type', '=', 'service')],
        help='Consultation Services')

    insurance = fields.Many2One(
        'gnuhealth.insurance', 'Insurance',
        domain=[('party', '=', Eval('rel_party'))],
        help="Insurance plan used in this procedure")

    rel_party = fields.Function(
        fields.Integer('Party ID'), 'get_party')

    @fields.depends('patient', '_parent_patient.party')
    def on_change_patient(self):
        if self.patient:
            self.rel_party = int(self.patient.party.id)
            self.state = 'confirmed'

    @fields.depends('patient', '_parent_patient.party')
    def get_party(self, name):
        if self.patient:
            return int(self.patient.party.id)

    # Verify Insurance status

    @fields.depends(methods=['_notify_insurance_problem'])
    def on_change_notify(self):
        notifications = super().on_change_notify()
        notifications.extend(self._notify_insurance_problem())
        return notifications

    @fields.depends('insurance')
    def _notify_insurance_problem(self):
        if self.insurance:
            if self.insurance.status == 'expired':
                yield ('warning', gettext('health.msg_insurance_invalid'))

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

        cls._buttons.update({
            'check_in': {
                'invisible': Not(Equal(Eval('state'), 'confirmed'))}})

        cls._buttons.update({
            'miss_out': {
                'invisible': Not(Equal(Eval('state'), 'confirmed'))}})

        # Do not cache default_key as it depends on time
        cls.__rpc__['default_get'].cache = None

    @classmethod
    @ModelView.button
    def check_in(cls, appointments):
        cls.write(appointments, {
            'state': 'checked_in'})

    @classmethod
    @ModelView.button
    def miss_out(cls, appointments):
        cls.write(appointments, {
            'state': 'no_show'})

    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('name',) + tuple(clause[1:]),
                ('patient',) + tuple(clause[1:]),
                ]

    @classmethod
    def generate_code(cls, **pattern):
        Config = Pool().get('gnuhealth.sequences')
        config = Config(1)
        sequence = config.get_multivalue(
            'appointment_sequence', **pattern)
        if sequence:
            return sequence.get()

    @classmethod
    def create(cls, vlist):
        vlist = [x.copy() for x in vlist]
        for values in vlist:
            if values['state'] == 'confirmed' and not values.get('name'):
                values['name'] = cls.generate_code()
        return super(Appointment, cls).create(vlist)

    @classmethod
    def write(cls, appointments, values):
        for appointment in appointments:
            # When an appointment's state is changed from 'free' to
            # 'confirmed', make sure appointment code is generated.
            if values.get('state') == 'confirmed' and not values.get('name'):
                values['name'] = cls.generate_code()
            # Update the checked-in time only if unset
            if values.get('state') == 'checked_in' \
                    and values.get('checked_in_date') is None:
                values['checked_in_date'] = datetime.now()

        return super(Appointment, cls).write(appointments, values)

    @classmethod
    def copy(cls, appointments, default=None):
        if default is None:
            default = {}
        default = default.copy()
        default['name'] = None
        default['appointment_date'] = cls.default_appointment_date()
        default['state'] = cls.default_state()
        return super(Appointment, cls).copy(appointments, default=default)

    @staticmethod
    def default_healthprof():
        return get_health_professional(required=False)

    @staticmethod
    def default_urgency():
        return 'a'

    @staticmethod
    def default_appointment_date():
        return datetime.now()

    @staticmethod
    def default_appointment_type():
        return 'outpatient'

    @staticmethod
    def default_state():
        return 'confirmed'

    @staticmethod
    def default_institution():
        return get_institution()

    @staticmethod
    def default_visit_type():
        return 'new'

    @fields.depends('healthprof')
    def on_change_with_speciality(self):
        # Return the Current / Main speciality of the Health Professional
        # if this speciality has been specified in the HP record.
        if (self.healthprof and self.healthprof.main_specialty):
            specialty = self.healthprof.main_specialty.specialty.id
            return specialty

    @staticmethod
    def default_speciality():
        # This method will assign the Main specialty to the appointment
        # if there is a health professional associated to the login user
        # as a default value.
        # It will be overwritten if the health professional is modified in
        # this view, the on_change_with will take effect.

        # Retrieve the health professional Main specialty, if assigned

        hp_party_id = get_health_professional(required=False)
        hp_main_specialty = None

        if hp_party_id:
            # Retrieve the health professional Main specialty, if assigned
            health_professional_obj = Pool().get(
                'gnuhealth.healthprofessional')
            health_professional = health_professional_obj.search(
                [('id', '=', hp_party_id)], limit=1)[0]
            hp_main_specialty = health_professional.main_specialty

            if hp_main_specialty:
                return hp_main_specialty.specialty.id

        if hp_main_specialty:
            return hp_main_specialty.specialty.id

    def get_rec_name(self, name):
        name = self.name
        healthprof = (self.healthprof
                      and self.healthprof.rec_name
                      or '')
        appointment_date = str(self.appointment_date)
        if name:
            return name
        else:
            # Let report do not show error when state=free
            return healthprof + "-" + appointment_date


class AppointmentReport(ModelSQL, ModelView):
    'Appointment Report'
    __name__ = 'gnuhealth.appointment.report'

    ref = fields.Char('PUID')
    patient = fields.Many2One('gnuhealth.patient', 'Patient')
    healthprof = fields.Many2One('gnuhealth.healthprofessional', 'Health Prof')
    age = fields.Function(fields.Char('Age'), 'get_patient_age')
    gender = fields.Selection([
        (None, ''),
        ('m', 'Male'),
        ('f', 'Female')], 'Gender')
    address = fields.Function(fields.Char('Address'), 'get_address')
    insurance = fields.Function(fields.Char('Insurance'), 'get_insurance')
    appointment_date = fields.Date(
        'Start', help='Appointment start date.')
    appointment_date_time = fields.DateTime(
        'Start', help="Appointment start time.")
    diagnosis = fields.Function(
        fields.Many2One(
            'gnuhealth.pathology',
            'Main Condition'), 'get_diagnosis')

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

    @classmethod
    def table_query(cls):
        pool = Pool()
        appointment = pool.get('gnuhealth.appointment').__table__()
        party = pool.get('party.party').__table__()
        patient = pool.get('gnuhealth.patient').__table__()
        join1 = Join(appointment, patient)
        join1.condition = join1.right.id == appointment.patient
        join2 = Join(join1, party)
        join2.condition = join2.right.id == join1.right.party
        where = Literal(True)
        if Transaction().context.get('date_start'):
            where &= (appointment.appointment_date >=
                      Transaction().context['date_start'])
        if Transaction().context.get('date_end'):
            where &= (appointment.appointment_date <
                      Transaction().context['date_end'] + timedelta(days=1))
        if Transaction().context.get('healthprof'):
            where &= \
                appointment.healthprof == Transaction().context['healthprof']

        return join2.select(
            appointment.id,
            appointment.create_uid,
            appointment.create_date,
            appointment.write_uid,
            appointment.write_date,
            join2.right.ref,
            join1.right.id.as_('patient'),
            join2.right.gender,
            appointment.appointment_date,
            appointment.appointment_date.as_('appointment_date_time'),
            appointment.healthprof,
            where=where)

    def get_address(self, name):
        res = ''
        if self.patient.party.addresses:
            res = self.patient.party.addresses[0].full_address
        return res

    def get_insurance(self, name):
        res = ''
        if self.patient.current_insurance:
            res = self.patient.current_insurance.company.name
        return res

    def get_diagnosis(self, name):
        Evaluation = Pool().get('gnuhealth.patient.evaluation')

        res = None
        evaluations = Evaluation.search([
            ('appointment', '=', self.id)
        ])
        if evaluations:
            evaluation = evaluations[0]
            if evaluation.diagnosis:
                res = evaluation.diagnosis.id
        return res

    def get_patient_age(self, name):
        return self.patient.party.age


class OpenAppointmentReportStart(ModelView):
    'Open Appointment Report'
    __name__ = 'gnuhealth.appointment.report.open.start'
    date_start = fields.Date('Date Start', required=True)
    date_end = fields.Date('Date End', required=True)
    healthprof = fields.Many2One('gnuhealth.healthprofessional', 'Health Prof')

    @staticmethod
    def default_date_start():
        return datetime.now()

    @staticmethod
    def default_date_end():
        return datetime.now()

    @staticmethod
    def default_healthprof():
        return get_health_professional()

    @classmethod
    def __setup__(cls):
        super(OpenAppointmentReportStart, cls).__setup__()

        # Do not cache default_key as it depends on time
        cls.__rpc__['default_get'].cache = None


class OpenAppointmentReport(Wizard):
    'Open Appointment Report'
    __name__ = 'gnuhealth.appointment.report.open'

    start = StateView(
        'gnuhealth.appointment.report.open.start',
        'health.appointments_report_open_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Open', 'open_', 'tryton-ok', default=True),
        ])
    open_ = StateAction('health.act_appointments_report_view_tree')

    def do_open_(self, action):
        health_prof = ''
        if self.start.healthprof:
            health_prof = self.start.healthprof.id

        action['pyson_context'] = PYSONEncoder().encode({
            'date_start': self.start.date_start,
            'date_end': self.start.date_end,
            'healthprof': health_prof,
        })

        # Show action name depending on whether is an specific hp
        # or for all professionals

        if health_prof:
            action['name'] += ' - %s' % (self.start.healthprof.rec_name)
        else:
            action['name'] += ' - %s' % "All Health Professionals"

        return action, {}

    def transition_open_(self):
        return 'end'


# PATIENT MEDICATION TREATMENT
class PatientMedication(ModelSQL, ModelView):
    'Patient Medication'
    __name__ = 'gnuhealth.patient.medication'

    medicament = fields.Many2One(
        'gnuhealth.medicament', 'Medicament',
        required=True, help='Prescribed Medicament')

    indication = fields.Many2One(
        'gnuhealth.pathology', 'Indication',
        help='Choose a disease for this medicament from the disease list. It'
        ' can be an existing disease of the patient or a prophylactic.')

    patient = fields.Many2One(
        'gnuhealth.patient', 'Patient', readonly=True)

    healthprof = fields.Many2One(
        'gnuhealth.healthprofessional', 'Health Prof', readonly=True,
        help='Health Professional who prescribed or reviewed the medicament')

    institution = fields.Many2One(
        'gnuhealth.institution', 'Institution')

    is_active = fields.Boolean(
        'Active',
        help='Check if the patient is currently taking the medication')

    discontinued = fields.Boolean(
        'Discontinued')

    course_completed = fields.Boolean(
        'Course Completed')

    discontinued_reason = fields.Char(
        'Reason for discontinuation',
        states={
            'invisible': Not(Bool(Eval('discontinued'))),
            'required': Bool(Eval('discontinued')),
        },
        depends=['discontinued'],
        help='Short description for discontinuing the treatment',)

    adverse_reaction = fields.Text(
        'Adverse Reactions',
        help='Side effects or adverse reactions that the patient experienced')

    notes = fields.Text('Extra Info')

    start_treatment = fields.DateTime(
        'Start',
        help='Date of start of Treatment')

    end_treatment = fields.DateTime(
        'End', help='Date of end of Treatment')

    dose = fields.Float(
        'Dose',
        help='Amount of medication (eg, 250 mg) per dose')

    dose_unit = fields.Many2One(
        'gnuhealth.dose.unit', 'dose unit',
        help='Unit of measure for the medication to be taken')

    route = fields.Many2One(
        'gnuhealth.drug.route', 'Administration Route',
        help='Drug administration route code.')
    form = fields.Many2One(
        'gnuhealth.drug.form', 'Form',
        help='Drug form, such as tablet or gel')

    qty = fields.Integer(
        'x',
        help='Quantity of units (eg, 2 capsules) of the medicament')

    duration = fields.Integer(
        'Treatment duration',
        help='Period that the patient must take the medication. in minutes,'
        ' hours, days, months, years or indefinitely for chronic conditions')

    duration_period = fields.Selection([
        (None, ''),
        ('minutes', 'minutes'),
        ('hours', 'hours'),
        ('days', 'days'),
        ('months', 'months'),
        ('years', 'years'),
        ('indefinite', 'indefinite'),
    ], 'Treatment period', sort=False,
        help='Period that the patient must take the medication in minutes,'
        ' hours, days, months, years or indefinitely')

    common_dosage = fields.Many2One(
        'gnuhealth.medication.dosage', 'Frequency',
        help='Common / standard dosage frequency for this medicament')

    admin_times = fields.Char(
        'Admin hours',
        help='Suggested administration hours. For example, at 08:00, 13:00'
        ' and 18:00 can be encoded like 08 13 18')

    frequency = fields.Integer(
        'Frequency',
        help='Time in between doses the patient must wait (ie, for 1 pill'
        ' each 8 hours, put here 8 and select \"hours\" in the unit field')

    frequency_unit = fields.Selection([
        (None, ''),
        ('seconds', 'seconds'),
        ('minutes', 'minutes'),
        ('hours', 'hours'),
        ('days', 'days'),
        ('weeks', 'weeks'),
        ('wr', 'when required'),
    ], 'unit', sort=False)

    frequency_prn = fields.Boolean(
        'PRN', help='Use it as needed, pro re nata')

    infusion = fields.Boolean(
        'Infusion',
        help='Mark if the medication is in the form of infusion'
        ' Intravenous, Gastrostomy tube, nasogastric, etc...')
    infusion_rate = fields.Float(
        'Rate', states={'invisible': Not(Bool(Eval('infusion')))})

    infusion_rate_units = fields.Selection([
        (None, ''),
        ('mlhr', 'mL/hour'),
    ], 'Unit Rate',
        states={'invisible': Not(Bool(Eval('infusion')))},
        sort=False)

    prescription = fields.Many2One(
        'gnuhealth.prescription.order', 'Prescription', readonly=True,
        domain=[('patient', '=', Eval('patient'))],
        depends=['patient'],
        help='Related prescription')

    @classmethod
    def __setup__(cls):
        super(PatientMedication, cls).__setup__()
        cls._order.insert(0, ('is_active', 'DESC'))
        cls._order.insert(1, ('start_treatment', 'DESC'))

    @fields.depends('discontinued', 'course_completed')
    def on_change_with_is_active(self):
        return not (self.discontinued or self.course_completed)

    @fields.depends('is_active', 'course_completed')
    def on_change_with_discontinued(self):
        return not (self.is_active or self.course_completed)

    @fields.depends('is_active', 'discontinued')
    def on_change_with_course_completed(self):
        return not (self.is_active or self.discontinued)

    @staticmethod
    def default_is_active():
        return True

    @staticmethod
    def default_frequency_unit():
        return 'hours'

    @staticmethod
    def default_duration_period():
        return 'days'

    @staticmethod
    def default_qty():
        return 1

    @staticmethod
    def default_institution():
        return get_institution()

    @staticmethod
    def default_healthprof():
        return get_health_professional()

    @classmethod
    def validate(cls, medications):
        super(PatientMedication, cls).validate(medications)
        for medication in medications:
            medication.validate_medication_dates()

    def validate_medication_dates(self):
        if self.end_treatment:
            if (self.end_treatment < self.start_treatment):
                raise MedEndDateBeforeStart(gettext(
                    'health.msg_med_end_date_before_start',
                    start_treatment=self.start_treatment,
                    end_treatment=self.end_treatment
                )
                )

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to patient
        if (table_h.column_exist('name')
                and not table_h.column_exist('patient')):
            table_h.column_rename('name', 'patient')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


# PATIENT VACCINATION INFORMATION
class PatientVaccination(ModelSQL, ModelView):
    'Patient Vaccination information'
    __name__ = 'gnuhealth.vaccination'

    patient = fields.Many2One('gnuhealth.patient', 'Patient', required=True)

    vaccine = fields.Many2One(
        'gnuhealth.medicament', 'Vaccine', required=True,
        domain=[('is_vaccine', '=', True)],
        help='Vaccine Name. Make sure that the vaccine has all the'
        ' proper information at product level. Information such as provider,'
        ' supplier code, tracking number, etc.. This  information must always'
        ' be present. If available, please copy / scan the vaccine leaflet'
        ' and attach it to this record')

    admin_route = fields.Selection([
        (None, ''),
        ('im', 'Intramuscular'),
        ('sc', 'Subcutaneous'),
        ('id', 'Intradermal'),
        ('nas', 'Intranasal'),
        ('po', 'Oral'),
    ], 'Route', sort=False)

    picture = fields.Binary('Label')

    # Deprecated
    # Since 2.8, we include the expiration date and lot number information
    # in the health_stock module
    vaccine_expiration_date = fields.Date('Expiration date')

    vaccine_lot = fields.Char(
        'Lot Number',
        help='Please check on the vaccine (product) production lot numberand'
        ' tracking number when available !')

    institution = fields.Many2One(
        'gnuhealth.institution', 'Institution',
        help='Medical Center where the patient is being or was vaccinated')

    date = fields.DateTime('Date')
    dose = fields.Integer('Dose #')
    next_dose_date = fields.DateTime('Next Dose')
    observations = fields.Text('Observations')

    institution = fields.Many2One('gnuhealth.institution', 'Institution')

    healthprof = fields.Many2One(
        'gnuhealth.healthprofessional', 'Health Prof', readonly=True,
        help="Health Professional who administered or reviewed "
        "the vaccine information.")

    signed_by = fields.Many2One(
        'gnuhealth.healthprofessional', 'Signed by', readonly=True,
        states={'invisible': Equal(Eval('state'), 'in_progress')},
        help="Health Professional that signed the vaccination document")

    amount = fields.Float(
        'Amount',
        help='Amount of vaccine administered, in mL . The dose per mL '
        '(eg, mcg, EL.U ..) can be found at the related medicament.')

    admin_site = fields.Selection([
        (None, ''),
        ('lvl', 'left vastus lateralis'),
        ('rvl', 'right vastus lateralis'),
        ('ld', 'left deltoid'),
        ('rd', 'right deltoid'),
        ('lalt', 'left anterolateral fat of thigh'),
        ('ralt', 'right anterolateral fat of thigh'),
        ('lpua', 'left posterolateral fat of upper arm'),
        ('rpua', 'right posterolateral fat of upper arm'),
        ('lfa', 'left fore arm'),
        ('rfa', 'right fore arm')], 'Admin Site')

    state = fields.Selection([
        (None, ''),
        ('in_progress', 'In Progress'),
        ('done', 'Done'),
    ], 'State', readonly=True)

    @staticmethod
    def default_institution():
        return get_institution()

    @staticmethod
    def default_healthprof():
        return get_health_professional()

    @staticmethod
    def default_state():
        return 'in_progress'

    @staticmethod
    def default_date():
        return datetime.now()

    @staticmethod
    def default_dose():
        return 1

    @classmethod
    @ModelView.button
    def sign(cls, vaccinations):
        # Change the state of the vaccination to "Done"
        # and write the name of the signing health professional
        signing_hp = get_health_professional()

        cls.write(vaccinations, {
            'state': 'done',
            'signed_by': signing_hp})

    @classmethod
    def write(cls, vaccinations, vals):
        # Don't allow to modify the record if the vaccination has been signed
        if vaccinations[0].state == 'done':
            raise CanNotModifyVaccination(
                gettext('health.can_not_modify_vaccination')
            )
        return super(PatientVaccination, cls).write(vaccinations, vals)

    @classmethod
    def validate(cls, vaccines):
        super(PatientVaccination, cls).validate(vaccines)
        for vaccine in vaccines:
            vaccine.validate_next_dose_date()

    def validate_next_dose_date(self):
        if (self.next_dose_date):
            if (self.next_dose_date < self.date):
                raise NextDoseBeforeFirst(
                    gettext('health.msg_next_dose_before_first')
                )

    @classmethod
    def __setup__(cls):
        super(PatientVaccination, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('dose_uniq', Unique(t, t.patient, t.vaccine, t.dose),
                'This vaccine dose has been given already to the patient'),
        ]

        cls._buttons.update({
            'sign': {'invisible': Equal(Eval('state'), 'done')}
        })

        # Do not cache default_key as it depends on time
        cls.__rpc__['default_get'].cache = None

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to patient
        if (table_h.column_exist('name')
                and not table_h.column_exist('patient')):
            table_h.column_rename('name', 'patient')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class PatientPrescriptionOrder(ModelSQL, ModelView):
    'Prescription Order'
    __name__ = 'gnuhealth.prescription.order'
    _rec_name = 'prescription_id'

    STATES = {'readonly': Not(Eval('state') == 'draft')}

    patient = fields.Many2One(
        'gnuhealth.patient', 'Patient', required=True, states=STATES)

    prescription_id = fields.Char(
        'ID', readonly=True,
        help='Type in the ID of this prescription')

    prescription_date = fields.DateTime(
        'Date', states=STATES,
        help='Prescription Date')

    user_id = fields.Many2One('res.user', 'Prescribing Doctor', readonly=True)

    pharmacy = fields.Many2One(
        'party.party', 'Pharmacy', domain=[('is_pharmacy', '=', True)],
        states={
            'readonly': (Eval('state') != 'draft') & Bool(Eval('pharmacy')),
        },
        depends=['state'])

    prescription_line = fields.One2Many(
        'gnuhealth.prescription.line', 'presc_order', 'Prescription line',
        states=STATES)

    notes = fields.Text('Prescription Notes', states=STATES)
    pregnancy_warning = fields.Boolean(
        'Pregnancy',
        help="This field is set when the patient is or might be pregnant")

    allergy_warning = fields.Boolean(
        'Allergy',
        help="This field is active when the patient is known to be allergic")

    prescription_warning_ack = fields.Boolean(
        'Verified', states=STATES,
        help="This verification field must be set to generate"
             " the prescription")

    healthprof = fields.Many2One(
        'gnuhealth.healthprofessional', 'Prescriber', readonly=True)

    report_prescription_date = fields.Function(
        fields.Date('Date', help="Prescription Date"),
        'get_report_prescription_date')

    report_prescription_time = fields.Function(fields.Time(
        'Prescription Time'), 'get_report_prescription_time')

    state = fields.Selection([
        ('draft', 'Draft'),
        ('done', 'Done'),
        ('validated', 'Validated'),
    ], 'State', readonly=True, sort=False, states=STATES)

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

        cls._buttons.update({
            'create_prescription': {'invisible': Equal(Eval('state'), 'done')}

        })

        # Do not cache default_key as it depends on time
        cls.__rpc__['default_get'].cache = None

    @classmethod
    def validate(cls, prescriptions):
        super(PatientPrescriptionOrder, cls).validate(prescriptions)
        for prescription in prescriptions:
            prescription.check_health_professional()
            prescription.check_prescription_warning()

    def check_health_professional(self):
        if not self.healthprof:
            raise NoAssociatedHealthProfessional(gettext(
                ('health.msg_no_associated_health_professional'))
            )

    def check_prescription_warning(self):
        if self.pregnancy_warning:
            raise DrugPregnancySafetyCheck(gettext(
                'health.msg_drug_pregnancy_safety_check')
            )
        if self.allergy_warning:
            raise DrugAllergySafetyCheck(gettext(
                'health.msg_drug_allergy_safety_check')
            )
        if not self.prescription_warning_ack:
            raise PrescriptionSafetyCheck(gettext(
                'health.msg_prescription_safety_check')
            )

    @staticmethod
    def default_healthprof():
        return get_health_professional()

    # Method that makes the doctor to acknowledge if there is any
    # warning in the prescription

    @fields.depends('patient')
    def on_change_patient(self):
        if self.patient:
            # Trigger the warning if the patient is at a childbearing age
            if (self.patient.childbearing_age):
                self.pregnancy_warning = True
                self.prescription_warning_ack = False

            # Trigger the warning if the patient is allergic
            if (self.patient.crit_allergic):
                self.allergy_warning = True
                self.prescription_warning_ack = False

    @staticmethod
    def default_prescription_date():
        return datetime.now()

    @staticmethod
    def default_user_id():
        User = Pool().get('res.user')
        user = User(Transaction().user)
        return int(user.id)

    @classmethod
    def default_state(cls):
        return 'draft'

    def get_report_prescription_date(self, name):
        Company = Pool().get('company.company')

        timezone = None
        company_id = Transaction().context.get('company')
        if company_id:
            company = Company(company_id)
            if company.timezone:
                timezone = pytz.timezone(company.timezone)

        dt = self.prescription_date
        return datetime.astimezone(dt.replace(tzinfo=pytz.utc),
                                   timezone).date()

    def get_report_prescription_time(self, name):
        Company = Pool().get('company.company')

        timezone = None
        company_id = Transaction().context.get('company')
        if company_id:
            company = Company(company_id)
            if company.timezone:
                timezone = pytz.timezone(company.timezone)

        dt = self.prescription_date
        return datetime.astimezone(dt.replace(tzinfo=pytz.utc),
                                   timezone).time()

    @classmethod
    def create_prescription_pol(cls, prescription_info):
        """ Adds an entry in the person Page of Life
            related to this person prescription
        """
        Pol = Pool().get('gnuhealth.pol')
        pol = []

        plines = ""
        for line in prescription_info.prescription_line:
            plines = plines + line.rec_name + "\n"

        vals = {
            'page': str(uuid4()),
            'person': prescription_info.patient.party.id,
            'page_date': prescription_info.prescription_date,
            'federation_account':
                prescription_info.patient.party.federation_account,
            'page_type': 'medical',
            'medical_context': 'prescription',
            'relevance': 'important',
            'info': plines,
            'author': prescription_info.healthprof and
                prescription_info.healthprof.party.rec_name,
        }

        pol.append(vals)
        Pol.create(pol)

    @classmethod
    def generate_code(cls, **pattern):
        Config = Pool().get('gnuhealth.sequences')
        config = Config(1)
        sequence = config.get_multivalue(
            'prescription_sequence', **pattern)
        if sequence:
            return sequence.get()

    @classmethod
    def create(cls, vlist):
        vlist = [x.copy() for x in vlist]
        for values in vlist:
            if not values.get('prescription_id'):
                values['prescription_id'] = cls.generate_code()
        return super(PatientPrescriptionOrder, cls).create(vlist)

    @classmethod
    def copy(cls, prescriptions, default=None):
        if default is None:
            default = {}
        default = default.copy()
        default['prescription_id'] = None
        default['prescription_date'] = cls.default_prescription_date()
        return super(PatientPrescriptionOrder, cls).copy(
            prescriptions, default=default)

    @classmethod
    @ModelView.button
    def create_prescription(cls, prescriptions):
        prescription = prescriptions[0]

        # Change the state of the prescription to "Done"

        cls.write(prescriptions, {
            'state': 'done', })

        # Create prescription PoL if the person has a federation account.
        if (prescription.patient.party.federation_account):
            cls.create_prescription_pol(prescription)

    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('patient',) + tuple(clause[1:]),
                ('prescription_id',) + tuple(clause[1:]),
                ]


# PRESCRIPTION LINE
class PrescriptionLine(ModelSQL, ModelView):
    'Prescription Line'
    __name__ = 'gnuhealth.prescription.line'

# Remove inherits to be compatible with Tryton 2.8
#    _inherits = {'gnuhealth.medication.template': 'template'}
#    template = fields.Many2One('gnuhealth.medication.template',
#        'Medication Template')

    presc_order = fields.Many2One(
        'gnuhealth.prescription.order', 'ID',
        help='Prescription ID')

    review = fields.DateTime('Valid Until', help="Until this date, the patient"
                             "usually can ask for a refill / "
                             "reorder of this medicament")

    quantity = fields.Integer(
        'Units',
        help="Number of units of the medicament."
        " Example : 30 capsules of amoxicillin")

    refills = fields.Integer('Refills #')
    allow_substitution = fields.Boolean('Allow substitution')

    short_comment = fields.Char(
        'Comment', help='Short comment on the specific drug')

    prnt = fields.Boolean(
        'Print',
        help='Check this box to print this line of the prescription.')

    add_to_history = fields.Boolean(
        'Hist',
        help='Include this medication in the patient medication history')

    medicament = fields.Many2One(
        'gnuhealth.medicament', 'Medicament',
        required=True, help='Prescribed Medicament')

    indication = fields.Many2One(
        'gnuhealth.pathology', 'Indication',
        help='Choose a disease for this medicament from the disease list. It'
        ' can be an existing disease of the patient or a prophylactic.')

    start_treatment = fields.DateTime(
        'Start',
        help='Date of start of Treatment')

    end_treatment = fields.DateTime(
        'End', help='Date of start of Treatment')

    dose = fields.Float(
        'Dose',
        help='Amount of medication (eg, 250 mg) per dose')

    dose_unit = fields.Many2One(
        'gnuhealth.dose.unit', 'dose unit',
        help='Unit of measure for the medication to be taken')

    route = fields.Many2One(
        'gnuhealth.drug.route', 'Administration Route',
        help='Drug administration route code.')

    form = fields.Many2One(
        'gnuhealth.drug.form', 'Form',
        help='Drug form, such as tablet or gel')

    qty = fields.Integer(
        'x',
        help='Quantity of units (eg, 2 capsules) of the medicament')

    common_dosage = fields.Many2One(
        'gnuhealth.medication.dosage', 'Frequency',
        help='Common / standard dosage frequency for this medicament')

    admin_times = fields.Char(
        'Admin hours',
        help='Suggested administration hours. For example, at 08:00, 13:00'
        ' and 18:00 can be encoded like 08 13 18')

    frequency = fields.Integer(
        'Frequency',
        help='Time in between doses the patient must wait (ie, for 1 pill'
        ' each 8 hours, put here 8 and select \"hours\" in the unit field')

    frequency_unit = fields.Selection([
        (None, ''),
        ('seconds', 'seconds'),
        ('minutes', 'minutes'),
        ('hours', 'hours'),
        ('days', 'days'),
        ('weeks', 'weeks'),
        ('wr', 'when required'),
    ], 'unit', sort=False)

    frequency_unit_str = frequency_unit.translated('frequency_unit')

    frequency_prn = fields.Boolean('PRN', help='Use it as needed, pro re nata')

    # Used by prescription_orders report template.
    def get_report_common_usage_str(self):
        common_dosage = self.common_dosage
        admin_times = self.admin_times
        if common_dosage and admin_times:
            return f'{common_dosage.name}\n({admin_times})'
        elif common_dosage:
            return common_dosage.name
        elif admin_times:
            return admin_times
        else:
            return ''

    # Used by prescription_orders report template.
    def get_report_specific_usage_str(self):
        string = ''
        if self.frequency_unit == 'wr':
            string = self.frequency_unit_str
        elif self.frequency_prn:
            string = gettext('health.msg_prescription_line_frequency_prn')
        elif self.frequency and self.frequency_unit:
            string = gettext('health.msg_prescription_line_frequency_and_unit',
                             frequency=str(self.frequency),
                             frequency_unit=self.frequency_unit_str)
        else:
            string = ''
        return string

    duration = fields.Integer(
        'Treatment duration',
        help='Period that the patient must take the medication. in minutes,'
        ' hours, days, months, years or indefinately')

    duration_period = fields.Selection([
        (None, ''),
        ('minutes', 'minutes'),
        ('hours', 'hours'),
        ('days', 'days'),
        ('months', 'months'),
        ('years', 'years'),
        ('indefinite', 'indefinite'),
    ], 'Treatment period', sort=False,
        help='Period that the patient must take the medication in minutes,'
        ' hours, days, months, years or indefinately')

    duration_period_str = duration_period.translated('duration_period')

    # Used by prescription_orders report template.
    def get_report_duration_str(self):
        string = ''
        if self.duration_period == 'indefinite':
            string = self.duration_period_str
        elif self.duration and self.duration_period:
            string = str(self.duration) + ' ' + self.duration_period_str
        else:
            string = ''
        return string

    infusion = fields.Boolean(
        'Infusion',
        help='Mark if the medication is in the form of infusion'
        ' Intravenous, Gastrostomy tube, nasogastric, etc...')
    infusion_rate = fields.Float(
        'Rate', states={'invisible': Not(Bool(Eval('infusion')))})

    infusion_rate_units = fields.Selection([
        (None, ''),
        ('mlhr', 'mL/hour'),
    ], 'Unit Rate',
        states={'invisible': Not(Bool(Eval('infusion')))},
        sort=False)

    def get_rec_name(self, name):
        dose = ""
        if (self.dose):
            dose_unit = ""
            dose = str(self.dose)
            if self.dose_unit:
                dose_unit = self.dose_unit.rec_name
            dose = dose + dose_unit
        return str(self.medicament.rec_name + " " + dose)

    @staticmethod
    def default_qty():
        return 1

    @staticmethod
    def default_duration_period():
        return 'days'

    @staticmethod
    def default_frequency_unit():
        return 'hours'

    @staticmethod
    def default_quantity():
        return 1

    @staticmethod
    def default_prnt():
        return True

    @staticmethod
    def default_start_treatment():
        return datetime.now()

    @fields.depends('medicament')
    def on_change_medicament(self):
        if self.medicament:
            self.dose = self.medicament.strength
            self.dose_unit = self.medicament.unit
            self.form = self.medicament.form
            self.route = self.medicament.route

    @classmethod
    def create(cls, vlist):
        vlist = [x.copy() for x in vlist]

        for values in vlist:
            if values.get('add_to_history'):

                Medication = Pool().get('gnuhealth.patient.medication')
                med = []

                # Retrieve the patient ID from the prescription
                Prescs = Pool().get('gnuhealth.prescription.order')
                patient = Prescs.browse([values['presc_order']])[0].patient.id

                medicament = values['medicament']
                indication = values['indication']
                start_treatment = values.get('start_treatment')
                prescription = values['presc_order']

                values = {
                    'patient': patient,
                    'medicament': medicament,
                    'indication': indication,
                    'start_treatment': start_treatment,
                    'prescription': prescription
                }

                # Add the medicament from the prescription
                med.append(values)
                Medication.create(med)

        return super(PrescriptionLine, cls).create(vlist)

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to presc_order
        if (table_h.column_exist('name')
                and not table_h.column_exist('presc_order')):
            table_h.column_rename('name', 'presc_order')

        super().__register__(module)
        table_h = cls.__table_handler__(module)

    @classmethod
    def __setup__(cls):
        super(PrescriptionLine, cls).__setup__()

        # Do not cache default_key as it depends on time
        cls.__rpc__['default_get'].cache = None


class PatientEvaluation(ModelSQL, ModelView, MultiValueMixin):
    'Patient Evaluation'
    __name__ = 'gnuhealth.patient.evaluation'

    STATES = {'readonly': Eval('state') == 'signed'}

    def patient_age_at_evaluation(self, name):
        if (self.patient.party.dob and self.evaluation_start):
            return compute_age_from_dates(
                self.patient.party.dob, None, None, None, 'age',
                self.evaluation_start.date())

    def evaluation_duration(self, name):
        if (self.evaluation_endtime and self.evaluation_start):
            return self.evaluation_endtime - self.evaluation_start

    def get_wait_time(self, name):
        # Compute wait time between checked-in and start of evaluation
        if self.appointment:
            if self.appointment.checked_in_date:
                if self.appointment.checked_in_date < self.evaluation_start:
                    return self.evaluation_start - \
                        self.appointment.checked_in_date

    code = fields.Char(
        'Code',
        help="Unique code that identifies the evaluation")

    patient = fields.Many2One(
        'gnuhealth.patient', 'Patient', required=True, states=STATES)

    appointment = fields.Many2One(
        'gnuhealth.appointment', 'Appointment',
        domain=[('patient', '=', Eval('patient'))], depends=['patient'],
        help='Enter or select the date / ID of the appointment related to'
        ' this evaluation', states=STATES)

    related_condition = fields.Many2One(
        'gnuhealth.patient.disease', 'Related condition',
        domain=[('patient', '=', Eval('patient'))], depends=['patient'],
        help="Related condition related to this follow-up evaluation",
        states={'readonly': (Eval('visit_type') != 'followup')})

    evaluation_start = fields.DateTime('Start', required=True, states=STATES)
    evaluation_endtime = fields.DateTime('End', states=STATES)

    evaluation_length = fields.Function(
        fields.TimeDelta(
            'Evaluation length',
            help="Duration of the evaluation"),
        'evaluation_duration')

    wait_time = fields.Function(
        fields.TimeDelta('Patient wait time',
                         help="How long the patient waited"),
        'get_wait_time')

    state = fields.Selection([
        (None, ''),
        ('in_progress', 'In progress'),
        ('done', 'Done'),
        ('signed', 'Signed'),
    ], 'State', readonly=True, sort=False)

    next_evaluation = fields.Many2One(
        'gnuhealth.appointment',
        'Next Appointment', domain=[('patient', '=', Eval('patient'))],
        depends=['patient'], states=STATES)

    user_id = fields.Many2One('res.user', 'Last Changed by', readonly=True)
    healthprof = fields.Many2One(
        'gnuhealth.healthprofessional', 'Initiated by',
        help="Health professional that initiates the evaluation."
        "This health professional might or might not be the same that"
        " signs and finishes the evaluation."
        "The evaluation remains in progress state until it is signed"
        ", when it becomes read-only", readonly=True)

    signed_by = fields.Many2One(
        'gnuhealth.healthprofessional', 'Signed by', readonly=True,
        states={'invisible': Equal(Eval('state'), 'in_progress')},
        help="Health Professional that finished the patient evaluation")

    specialty = fields.Many2One(
        'gnuhealth.specialty', 'Specialty',
        states=STATES)

    visit_type = fields.Selection([
        (None, ''),
        ('new', 'New health condition'),
        ('followup', 'Followup'),
        ('well_child', 'Well Child visit'),
        ('well_woman', 'Well Woman visit'),
        ('well_man', 'Well Man visit'),
    ], 'Visit', sort=False,
        states=STATES)

    urgency = fields.Selection([
        (None, ''),
        ('a', 'Normal'),
        ('b', 'Urgent'),
        ('c', 'Medical Emergency'),
    ], 'Urgency', sort=False,
        states=STATES)

    computed_age = fields.Function(fields.Char(
        'Age',
        help="Computed patient age at the moment of the evaluation"),
        'patient_age_at_evaluation')

    gender = fields.Function(fields.Selection([
        (None, ''),
        ('m', 'Male'),
        ('f', 'Female'),
        ('nb', 'Non-binary'),
        ('other', 'Other'),
        ('nd', 'Non disclosed'),
        ('u', 'Unknown'),
        ('f-m', 'Female -> Male'),
        ('m-f', 'Male -> Female'),
    ], 'Gender'), 'get_patient_gender', searcher='search_patient_gender')

    information_source = fields.Char(
        'Source', help="Source of "
        "Information, eg : Self, relative, friend ...",
        states=STATES)

    reliable_info = fields.Boolean(
        'Reliable', help="Uncheck this option"
        "if the information provided by the source seems not reliable",
        states=STATES)

    # This method is used in report templates, for example:
    # patient_evaluation.fodt
    def get_report_reliable_info(self):
        if self.reliable_info:
            return gettext(
                'health.msg_patient_evaluation_reliable_info_is_true')
        else:
            return gettext(
                'health.msg_patient_evaluation_reliable_info_is_false')

    derived_from = fields.Many2One(
        'gnuhealth.healthprofessional', 'Derived from',
        help='Health Professional who derived the case',
        states=STATES)

    derived_to = fields.Many2One(
        'gnuhealth.healthprofessional', 'Derived to',
        help='Health Professional to derive the case',
        states=STATES)

    evaluation_type = fields.Selection([
        (None, ''),
        ('outpatient', 'Outpatient'),
        ('homecare', 'Home Care'),
        ('telemedicine', 'Telemedicine'),
        ('inpatient', 'Inpatient'),
    ], 'Type', sort=False,
        states=STATES)

    chief_complaint = fields.Char('Chief Complaint', help='Chief Complaint',
                                  states=STATES)
    notes_complaint = fields.Text('Complaint details', states=STATES)
    present_illness = fields.Text('Present Illness', states=STATES)
    evaluation_summary = fields.Text('Clinical and physical', states=STATES)

    glycemia = fields.Float(
        'Glycemia',
        help='Blood glucose level (mg/dL)', states=STATES)

    hba1c = fields.Float(
        'HbA1c',
        help='Last Glycated Hemoglobin HbA1c level(mmol/mol)',
        states=STATES)

    cholesterol_total = fields.Integer(
        'Last Cholesterol',
        help='Last cholesterol reading (mg/dL)',
        states=STATES)

    hdl = fields.Integer(
        'HDL',
        help='Last HDL Cholesterol reading (mg/dL)',
        states=STATES)

    ldl = fields.Integer(
        'LDL',
        help='Last LDL Cholesterol reading (mg/dL)',
        states=STATES)

    tag = fields.Integer(
        'TAGs',
        help='Last Triglicerides level, (mg/dL)',
        states=STATES)

    systolic = fields.Integer(
        'Systolic Pressure',
        help='Systolic pressure (mmHg)',
        states=STATES)

    diastolic = fields.Integer(
        'Diastolic Pressure',
        help='Diastolic pressure (mmHg)',
        states=STATES)

    bpm = fields.Integer(
        'Heart Rate',
        help='Heart rate (beats per minute)',
        states=STATES)

    respiratory_rate = fields.Integer(
        'Respiratory Rate',
        help='Respiratory rate expressed in breaths per minute',
        states=STATES)

    osat = fields.Integer(
        'Oxygen Saturation',
        help='Arterial oxygen saturation expressed as a percentage',
        states=STATES)

    malnutrition = fields.Boolean(
        'Malnutrition',
        help='Check this box if the patient show signs of malnutrition. If'
        ' associated  to a disease, please encode the correspondent disease'
        ' on the patient disease history. For example, Moderate'
        ' protein-energy malnutrition, E44.0 in ICD-10 encoding',
        states=STATES)

    dehydration = fields.Boolean(
        'Dehydration',
        help='Check this box if the patient show signs of dehydration. If'
        ' associated  to a disease, please encode the  correspondent disease'
        ' on the patient disease history. For example, Volume Depletion, E86'
        ' in ICD-10 encoding',
        states=STATES)

    temperature = fields.Float(
        'Temperature',
        help='Temperature in celcius',
        states=STATES)

    weight = fields.Float('Weight', digits=(3, 2), help='Weight in kilos',
                          states=STATES)

    height = fields.Float('Height', digits=(3, 1),
                          help='Height in centimeters', states=STATES)

    bmi = fields.Float(
        'BMI', digits=(2, 2),
        help='Body mass index',
        states=STATES)

    head_circumference = fields.Float(
        'Head',
        help='Head circumference in centimeters',
        states=STATES)

    abdominal_circ = fields.Float('Waist', digits=(3, 1),
                                  help='Waist circumference in centimeters',
                                  states=STATES)

    hip = fields.Float('Hip', digits=(3, 1),
                       help='Hip circumference in centimeters',
                       states=STATES)

    whr = fields.Float(
        'WHR', digits=(2, 2), help='Waist to hip ratio . Reference values:\n'
        'Men : < 0.9 Normal // 0.9 - 0.99 Overweight // > 1 Obesity \n'
        'Women : < 0.8 Normal // 0.8 - 0.84 Overweight // > 0.85 Obesity',
        states=STATES)

    # Include Body composition metrics

    body_water = fields.Float("Water", help="Total body water %")
    body_fat = fields.Float("Fat", help="Fat composition %")
    body_muscle = fields.Float("Muscle", help="Skeletal muscle %")
    body_bone = fields.Float("Bone", help="Body bone composition %")

    # DEPRECATION NOTE : SIGNS AND SYMPTOMS FIELDS TO BE REMOVED IN 1.6 .
    # NOW WE USE A O2M OBJECT TO MAKE IT MORE SCALABLE, CLEARER AND FUNCTIONAL
    # TO WORK WITH THE CLINICAL FINDINGS OF THE PATIENT
    loc = fields.Integer(
        'Glasgow',
        help='Level of Consciousness - on Glasgow Coma Scale :  < 9 severe -'
        ' 9-12 Moderate, > 13 minor',
        states=STATES)
    loc_eyes = fields.Selection([
        ('1', 'Does not Open Eyes'),
        ('2', 'Opens eyes in response to painful stimuli'),
        ('3', 'Opens eyes in response to voice'),
        ('4', 'Opens eyes spontaneously'),
    ], 'Glasgow - Eyes', sort=False,
        states=STATES)
    loc_verbal = fields.Selection([
        ('1', 'Makes no sounds'),
        ('2', 'Incomprehensible sounds'),
        ('3', 'Utters inappropriate words'),
        ('4', 'Confused, disoriented'),
        ('5', 'Oriented, converses normally'),
    ], 'Glasgow - Verbal', sort=False,
        states=STATES)
    loc_motor = fields.Selection([
        ('1', 'Makes no movement'),
        ('2', 'Extension to painful stimuli - decerebrate response -'),
        ('3', 'Abnormal flexion to painful stimuli (decorticate response)'),
        ('4', 'Flexion / Withdrawal to painful stimuli'),
        ('5', 'Localizes painful stimuli'),
        ('6', 'Obeys commands'),
    ], 'Glasgow - Motor', sort=False,
        states=STATES)

    tremor = fields.Boolean(
        'Tremor',
        help='If associated  to a disease, please encode it on the patient'
        ' disease history',
        states=STATES)

    violent = fields.Boolean(
        'Violent Behaviour',
        help='Check this box if the patient is aggressive or violent at the'
        ' moment',
        states=STATES)

    mood = fields.Selection([
        (None, ''),
        ('n', 'Normal'),
        ('s', 'Sad'),
        ('f', 'Fear'),
        ('r', 'Rage'),
        ('h', 'Happy'),
        ('d', 'Disgust'),
        ('e', 'Euphoria'),
        ('fl', 'Flat'),
    ], 'Mood', sort=False,
        states=STATES)

    orientation = fields.Boolean(
        'Orientation',
        help='Check this box if the patient is disoriented in time and/or'
        ' space',
        states=STATES)

    memory = fields.Boolean(
        'Memory',
        help='Check this box if the patient has problems in short or long'
        ' term memory',
        states=STATES)

    knowledge_current_events = fields.Boolean(
        'Knowledge of Current Events',
        help='Check this box if the patient can not respond to public'
        ' notorious events',
        states=STATES)

    judgment = fields.Boolean(
        'Judgment',
        help='Check this box if the patient can not interpret basic scenario'
        ' solutions',
        states=STATES)

    abstraction = fields.Boolean(
        'Abstraction',
        help='Check this box if the patient presents abnormalities in'
        ' abstract reasoning',
        states=STATES)

    vocabulary = fields.Boolean(
        'Vocabulary',
        help='Check this box if the patient lacks basic intelectual capacity,'
        ' when she/he can not describe elementary objects',
        states=STATES)

    calculation_ability = fields.Boolean(
        'Calculation Ability',
        help='Check this box if the patient can not do simple arithmetic'
        ' problems',
        states=STATES)

    object_recognition = fields.Boolean(
        'Object Recognition',
        help='Check this box if the patient suffers from any sort of gnosia'
        ' disorders, such as agnosia, prosopagnosia ...',
        states=STATES)

    praxis = fields.Boolean(
        'Praxis',
        help='Check this box if the patient is unable to make voluntary'
        'movements',
        states=STATES)

    diagnosis = fields.Many2One(
        'gnuhealth.pathology', 'Main Condition',
        help='Presumptive Diagnosis. If no diagnosis can be made'
        ', encode the main sign or symptom.',
        states=STATES)

    secondary_conditions = fields.One2Many(
        'gnuhealth.secondary_condition',
        'evaluation', 'Other Conditions', help='Other '
        ' conditions found on the patient in the context'
        ' of this evaluation',
        states=STATES)

    diagnostic_hypothesis = fields.One2Many(
        'gnuhealth.diagnostic_hypothesis',
        'evaluation', 'Hypotheses / DDx', help='Other Diagnostic Hypotheses /'
        ' Differential Diagnosis (DDx)',
        states=STATES)

    signs_and_symptoms = fields.One2Many(
        'gnuhealth.signs_and_symptoms',
        'evaluation', 'Signs and Symptoms', help='Enter the Signs and Symptoms'
        ' for the patient in this evaluation.',
        states=STATES)

    psychological_assessment = fields.Text("Psychological Assessment",
                                           states=STATES)

    info_diagnosis = fields.Text(
        'Diagnostic Judgement', help="Diagnostic judgement",
        states=STATES)

    directions = fields.Text('Therapeutic Plan', states=STATES)

    procedures = fields.One2Many(
        'gnuhealth.patient.procedure', 'reference', 'Procedures',
        domain=[
            ('patient', '=', Eval('patient')),
            ('ctx', '=', 'evaluation'),
            # ('pdate', '=', Eval('evaluation_start')),
            ('insurance', '=', Eval('insurance')),
        ],
        depends=['patient'],
        help='Procedures done in the evaluation')

    # Deprecated in 5.0. Use the "procedures" attribute
    actions = fields.One2Many(
        'gnuhealth.directions', 'evaluation', 'Procedures',
        help='Procedures done in the evaluation',
        states=STATES)

    notes = fields.Text('Notes', states=STATES)

    discharge_reason = fields.Selection([
        (None, ''),
        ('home', 'Home / Selfcare'),
        ('transfer', 'Transferred to another institution'),
        ('against_advice', 'Left against medical advice'),
        ('death', 'Death')],
        'Discharge Reason', required=True, sort=False,
        states={'invisible': Equal(Eval('state'), 'in_progress'),
                'readonly': Eval('state') == 'signed'},
        help="Reason for patient discharge")
    discharge_reason_str = discharge_reason.translated('discharge_reason')

    institution = fields.Many2One('gnuhealth.institution', 'Institution',
                                  states=STATES)

    report_evaluation_date = fields.Function(fields.Date(
        'Evaluation Date'), 'get_report_evaluation_date')
    report_evaluation_time = fields.Function(fields.Time(
        'Evaluation Time'), 'get_report_evaluation_time')

    insurance = fields.Many2One(
        'gnuhealth.insurance', 'Insurance',
        domain=[('party', '=', Eval('rel_party'))],
        help="Insurance plan used in this procedure")

    rel_party = fields.Function(fields.Integer('Party ID'), 'get_party')

    def get_party(self, name):
        if (self.patient):
            return int(self.patient.party.id)

    @staticmethod
    def default_procedures():
        """When creating a new patient medical evaluation / encounter,
           GNU Health checks if there is one code set on the
           gnuhealth.procedures.config model, and use it as the first line
           on the procedure list.
           The user can remove it or use another in that medical evaluation
           context.
        """
        ProceduresConfig = Pool().get('gnuhealth.procedures.config')(1)
        if (ProceduresConfig and ProceduresConfig.medical_evaluation):
            medical_procedure = int(ProceduresConfig.medical_evaluation)
            return [{'procedure': medical_procedure}]
        else:
            return []

    @staticmethod
    def default_institution():
        return get_institution()

    @staticmethod
    def default_discharge_reason():
        return 'home'

    def get_patient_gender(self, name):
        return (self.patient and self.patient.gender)

    @classmethod
    def search_patient_gender(cls, name, clause):
        res = []
        value = clause[2]
        res.append(('patient.party.gender', clause[1], value))
        return res

    @classmethod
    def validate(cls, evaluations):
        super(PatientEvaluation, cls).validate(evaluations)
        for evaluation in evaluations:
            evaluation.validate_evaluation_period()
            evaluation.check_health_professional()

    def validate_evaluation_period(self):
        if (self.evaluation_endtime and self.evaluation_start):
            if (self.evaluation_endtime < self.evaluation_start):
                raise EvaluationEndBeforeStart(gettext(
                    'health.msg_end_evaluation_time_before_start',
                    evaluation_start=self.evaluation_start,
                    evaluation_endtime=self.evaluation_endtime,
                )
                )

    def check_health_professional(self):
        if not self.healthprof:
            raise NoAssociatedHealthProfessional(gettext(
                ('health.msg_no_associated_health_professional'))
            )

    @staticmethod
    def default_healthprof():
        return get_health_professional()

    @staticmethod
    def default_loc_eyes():
        return '4'

    @staticmethod
    def default_loc_verbal():
        return '5'

    @staticmethod
    def default_loc_motor():
        return '6'

    @staticmethod
    def default_loc():
        return 15

    @staticmethod
    def default_evaluation_type():
        return 'outpatient'

    @staticmethod
    def default_state():
        return 'in_progress'

    @fields.depends('weight', 'height')
    def on_change_with_bmi(self):
        if self.height and self.weight:
            if (self.height > 0):
                return round(self.weight / ((self.height / 100) ** 2), 2)
            return 0

    @fields.depends('weight', 'height', 'bmi')
    def on_change_bmi(self):
        if self.height and self.weight:
            if (self.height > 0):
                self.bmi = round(self.weight / ((self.height / 100) ** 2), 2)
        elif (self.height and not self.weight):
            self.weight = round((((self.height / 100) ** 2) * self.bmi), 2)

        elif (self.weight and not self.height):
            self.height = round(((self.weight / self.bmi)**(0.5) * 100), 2)

    @fields.depends('loc_verbal', 'loc_motor', 'loc_eyes')
    def on_change_with_loc(self):
        return int(self.loc_motor) + int(self.loc_eyes) + int(self.loc_verbal)

    # Show the gender and age upon entering the patient
    # These two are function fields (don't exist at DB level)
    @fields.depends('patient')
    def on_change_patient(self):
        if self.patient:
            self.computed_age = self.patient.age
            self.gender = self.patient.gender
        else:
            self.computed_age = None
            self.gender = None

    @staticmethod
    def default_information_source():
        return gettext(
            'health.msg_patient_evaluation_default_information_source')

    @staticmethod
    def default_reliable_info():
        return True

    @staticmethod
    def default_evaluation_start():
        return datetime.now()

    # Calculate the WH ratio
    @fields.depends('abdominal_circ', 'hip', 'whr')
    def on_change_with_whr(self):
        waist = self.abdominal_circ
        hip = self.hip
        if waist and hip:
            if (hip > 0):
                whr = round((waist / hip), 2)
            else:
                whr = 0
            return whr

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

    def get_report_evaluation_date(self, name):
        Company = Pool().get('company.company')

        timezone = None
        company_id = Transaction().context.get('company')
        if company_id:
            company = Company(company_id)
            if company.timezone:
                timezone = pytz.timezone(company.timezone)

        dt = self.evaluation_start
        return datetime.astimezone(
            dt.replace(tzinfo=pytz.utc), timezone).date()

    def get_report_evaluation_time(self, name):
        Company = Pool().get('company.company')

        timezone = None
        company_id = Transaction().context.get('company')
        if company_id:
            company = Company(company_id)
            if company.timezone:
                timezone = pytz.timezone(company.timezone)

        dt = self.evaluation_start
        return datetime.astimezone(
            dt.replace(tzinfo=pytz.utc), timezone).time()

    @classmethod
    def __setup__(cls):
        super(PatientEvaluation, cls).__setup__()

        t = cls.__table__()
        cls._sql_constraints = [
            ('code_unique', Unique(t, t.code),
                'The evaluation code must be unique !'),
        ]

        cls._order.insert(0, ('evaluation_start', 'DESC'))

        cls._buttons.update({
            'end_evaluation': {'invisible': Or(Equal(Eval('state'), 'signed'),
                                               Equal(Eval('state'), 'done'))}
        })

        """
        We should be able to update the patient disease history
        from evaluations, even if these are signed.
        In addition, well-person visits can
        include new health conditions, detected during the
        evaluation.
        """
        cls._buttons.update({
            'update_patient_disease_info': {
                'invisible': Or(Equal(Eval('visit_type'), 'followup'),
                                Equal(Eval('state'), 'in_progress'),
                                Not(Eval('diagnosis')))},
        })

        # Do not cache default_key as it depends on time
        cls.__rpc__['default_get'].cache = None

    @classmethod
    def generate_code(cls, **pattern):
        Config = Pool().get('gnuhealth.sequences')
        config = Config(1)
        sequence = config.get_multivalue(
            'patient_evaluation_sequence', **pattern)
        if sequence:
            return sequence.get()

    @classmethod
    def create(cls, vlist):
        vlist = [x.copy() for x in vlist]
        for values in vlist:
            if not values.get('code'):
                values['code'] = cls.generate_code()

        return super(PatientEvaluation, cls).create(vlist)

    # End the evaluation and discharge the patient
    @classmethod
    @ModelView.button
    def end_evaluation(cls, evaluations):
        pool = Pool()
        Appointment = pool.get('gnuhealth.appointment')

        evaluation_id = evaluations[0]

        patient_app = []

        # Change the state of the evaluation to "Done"
        signing_hp = get_health_professional()

        cls.write(evaluations, {
            'state': 'done',
            'signed_by': signing_hp,
            'evaluation_endtime': datetime.now()
        })

        # If there is an appointment associated to this evaluation
        # set it to state "Done"

        if evaluations[0].appointment:
            patient_app.append(evaluations[0].appointment)
            Appointment.write(patient_app, {
                'state': 'done',
            })

        # Create an entry in the page of life
        # It will create the entry at the moment of
        # discharging the patient
        # The patient needs to have a federation account
        if (evaluation_id.patient.party.federation_account):
            cls.create_evaluation_pol(evaluation_id)

    @classmethod
    def create_evaluation_pol(cls, evaluation):
        """ Adds an entry in the person Page of Life
            related to this medical evaluation.
        """
        Pol = Pool().get('gnuhealth.pol')
        pol = []

        # Create a dictionary with vital signs and other measurements
        measurements = {}
        bp = {}
        if evaluation.systolic:
            bp['systolic'] = evaluation.systolic
        if evaluation.diastolic:
            bp['diastolic'] = evaluation.diastolic

        if bp:
            measurements['bp'] = bp
        if evaluation.temperature:
            measurements['t'] = evaluation.temperature
        if evaluation.bpm:
            measurements['hr'] = evaluation.bpm
        if evaluation.respiratory_rate:
            measurements['rr'] = evaluation.respiratory_rate
        if evaluation.osat:
            measurements['osat'] = evaluation.osat
        if evaluation.glycemia:
            measurements['bg'] = evaluation.glycemia
        if evaluation.weight:
            measurements['wt'] = evaluation.weight
        if evaluation.height:
            measurements['ht'] = evaluation.height
        if evaluation.bmi:
            measurements['bmi'] = evaluation.bmi
        if evaluation.head_circumference:
            measurements['hc'] = evaluation.head_circumference

        assessment = (evaluation.diagnosis and
                      evaluation.diagnosis.rec_name) or ''

        measures = str(measurements)
        # Summarize the encounter note taking as SOAP
        soap = \
            f"S: {evaluation.chief_complaint}\n--\n"
        f"{evaluation.present_illness}\n"
        f"O: {evaluation.evaluation_summary}\n"
        f"        {measures}\n"
        f"A: {assessment}\n"
        f"P: {evaluation.directions}"

        vals = {
            'page': str(uuid4()),
            'person': evaluation.patient.party.id,
            'page_date': evaluation.evaluation_start,
            'age': evaluation.computed_age,
            'federation_account': evaluation.patient.party.federation_account,
            'page_type': 'medical',
            'medical_context': 'encounter',
            'relevance': 'important',
            'summary': evaluation.chief_complaint,
            'info': soap,
            'measurements': measures,
            'author': evaluation.healthprof.party.rec_name,
            'author_acct': evaluation.healthprof.party.federation_account,
            'node': evaluation.institution and
            evaluation.institution.party.name or ''
        }
        if (evaluation.diagnosis):
            vals['health_condition_text'] = evaluation.diagnosis.rec_name
            vals['health_condition_code'] = evaluation.diagnosis.code

        pol.append(vals)
        Pol.create(pol)

    @classmethod
    @ModelView.button_action('health.update_patient_disease_info')
    def update_patient_disease_info(cls, evaluations):
        pass

    # Search by the health condition code or the description
    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('patient',) + tuple(clause[1:]),
                ('code',) + tuple(clause[1:]),
                ]


# PATIENT PROCEDURES
class PatientProcedure(ModelSQL, ModelView):
    'Procedures done on patients'
    __name__ = 'gnuhealth.patient.procedure'

    procedure = fields.Many2One(
        'gnuhealth.procedure', 'Procedure', required=True)

    patient = fields.Many2One('gnuhealth.patient', 'Patient', required=True)

    rel_party = fields.Function(fields.Integer('Party ID'), 'get_party')

    insurance = fields.Many2One(
        'gnuhealth.insurance', 'Insurance',
        domain=[('party', '=', Eval('rel_party'))],
        help="Insurance plan used in this procedure")

    ctx = fields.Selection([
        (None, ''),
        ('evaluation', 'Medical Evaluation'),
        ('ambulatory', 'Ambulatory Care'),
        ('rounding', 'Nursing Rounding'),
        ('surgery', 'Surgery'),
    ], 'Context', sort=False)

    reference = fields.Reference(
        'Origin', 'get_origin', readonly=True)

    pdate = fields.DateTime('Date')

    comments = fields.Char('Comments')

    @fields.depends('patient')
    def on_change_with_rel_party(self):
        if self.patient:
            return self.patient.party.id

    def get_party(self, name):
        if (self.patient):
            return self.patient.party.id

    @staticmethod
    def default_pdate():
        return datetime.now()

    @classmethod
    def _get_origin(cls):
        'List of Model References. New packages will add to it'
        return [('gnuhealth.patient.evaluation')]

    @classmethod
    def get_origin(cls):
        Model = Pool().get('ir.model')
        get_name = Model.get_name
        models = cls._get_origin()
        return [(None, '')] + [(m, get_name(m)) for m in models]

    # Allow to search by patient, procedure or context
    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('procedure',) + tuple(clause[1:]),
                ('patient',) + tuple(clause[1:]),
                ('ctx',) + tuple(clause[1:]),
                ]

    @classmethod
    def __setup__(cls):
        super(PatientProcedure, cls).__setup__()

        # Do not cache default_key as it depends on time
        cls.__rpc__['default_get'].cache = None


# PATIENT EVALUATION DIRECTIONS
# Obsoleted in 5.0 by PatientProcedure class
class Directions(ModelSQL, ModelView):
    'Patient Directions'
    __name__ = 'gnuhealth.directions'

    evaluation = fields.Many2One(
        'gnuhealth.patient.evaluation', 'Evaluation', readonly=True)

    procedure = fields.Many2One(
        'gnuhealth.procedure', 'Procedure', required=True)

    comments = fields.Char('Comments')

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to evaluation
        if (table_h.column_exist('name')
                and not table_h.column_exist('evaluation')):
            table_h.column_rename('name', 'evaluation')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


# SECONDARY CONDITIONS ASSOCIATED TO THE PATIENT IN THE EVALUATION
class SecondaryCondition(ModelSQL, ModelView):
    'Secondary Conditions'
    __name__ = 'gnuhealth.secondary_condition'

    evaluation = fields.Many2One(
        'gnuhealth.patient.evaluation', 'Evaluation', readonly=True)

    pathology = fields.Many2One(
        'gnuhealth.pathology', 'Pathology', required=True)

    comments = fields.Char('Comments')


# PATIENT EVALUATION OTHER DIAGNOSTIC HYPOTHESES
class DiagnosticHypothesis(ModelSQL, ModelView):
    'Other Diagnostic Hypothesis'
    __name__ = 'gnuhealth.diagnostic_hypothesis'

    evaluation = fields.Many2One(
        'gnuhealth.patient.evaluation', 'Evaluation', readonly=True)

    pathology = fields.Many2One(
        'gnuhealth.pathology', 'Pathology', required=True)

    comments = fields.Char('Comments')


# PATIENT EVALUATION CLINICAL FINDINGS (SIGNS AND SYMPTOMS)
class SignsAndSymptoms(ModelSQL, ModelView):
    'Evaluation Signs and Symptoms'
    __name__ = 'gnuhealth.signs_and_symptoms'

    evaluation = fields.Many2One(
        'gnuhealth.patient.evaluation', 'Evaluation', readonly=True)

    sign_or_symptom = fields.Selection([
        (None, ''),
        ('sign', 'Sign'),
        ('symptom', 'Symptom')],
        'Subjective / Objective', required=True)

    sign_or_symptom_str = sign_or_symptom.translated('sign_or_symptom')

    clinical = fields.Many2One(
        'gnuhealth.pathology', 'Sign or Symptom',
        required=True)

    comments = fields.Char('Comments')


# ECG
class PatientECG(ModelSQL, ModelView):
    'Patient ECG'
    __name__ = 'gnuhealth.patient.ecg'

    patient = fields.Many2One('gnuhealth.patient',
                              'Patient', required=True)

    ecg_date = fields.DateTime('Date', required=True)
    images = fields.One2Many('ir.attachment', 'resource', 'Images')
    lead = fields.Selection([
        (None, ''),
        ('i', 'I'),
        ('ii', 'II'),
        ('iii', 'III'),
        ('avf', 'aVF'),
        ('avr', 'aVR'),
        ('avl', 'aVL'),
        ('v1', 'V1'),
        ('v2', 'V2'),
        ('v3', 'V3'),
        ('v4', 'V4'),
        ('v5', 'V5'),
        ('v6', 'V6')],
        'Lead', sort=False)

    axis = fields.Selection([
        (None, ''),
        ('normal', 'Normal Axis'),
        ('left', 'Left deviation'),
        ('right', 'Right deviation'),
        ('extreme_right', 'Extreme right deviation')],
        'Axis', sort=False, required=True)

    rate = fields.Integer('Rate', required=True)

    rhythm = fields.Selection([
        (None, ''),
        ('regular', 'Regular'),
        ('irregular', 'Irregular')],
        'Rhythm', sort=False, required=True)

    pacemaker = fields.Selection([
        (None, ''),
        ('sa', 'Sinus Node'),
        ('av', 'Atrioventricular'),
        ('pk', 'Purkinje')
    ],
        'Pacemaker', sort=False, required=True)

    pr = fields.Integer('PR', help="Duration of PR interval in milliseconds")
    qrs = fields.Integer('QRS',
                         help="Duration of QRS interval in milliseconds")
    qt = fields.Integer('QT', help="Duration of QT interval in milliseconds")
    st_segment = fields.Selection([
        (None, ''),
        ('normal', 'Normal'),
        ('depressed', 'Depressed'),
        ('elevated', 'Elevated')],
        'ST Segment', sort=False, required=True)

    twave_inversion = fields.Boolean('T wave inversion')

    interpretation = fields.Text('Interpretation', required=True)
    ecg_strip = fields.Binary('ECG Strip')

    healthprof = fields.Many2One(
        'gnuhealth.healthprofessional',
        'Health Prof', readonly=True,
        help='Health Professional who performed the ECG')

    institution = fields.Many2One('gnuhealth.institution', 'Institution')

    # Default ECG date
    @staticmethod
    def default_ecg_date():
        return datetime.now()

    @staticmethod
    def default_institution():
        return get_institution()

    @staticmethod
    def default_healthprof():
        return get_health_professional()

    @classmethod
    def validate(cls, ecgs):
        super(PatientECG, cls).validate(ecgs)
        for ecg in ecgs:
            ecg.check_health_professional()

    def check_health_professional(self):
        if not self.healthprof:
            raise NoAssociatedHealthProfessional(gettext(
                ('health.msg_no_associated_health_professional'))
            )

    # Return the ECG Interpretation with main components
    def get_rec_name(self, name):
        if self.patient:
            res = str(self.interpretation) + ' // Rate ' + str(self.rate)
        return res

    @classmethod
    def __setup__(cls):
        super(PatientECG, cls).__setup__()
        # Do not cache default_key as it depends on time
        cls.__rpc__['default_get'].cache = None
        cls._order.insert(0, ('ecg_date', 'DESC'))

    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                ('patient',) + tuple(clause[1:]),
                ]

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: rename name to patient
        if (table_h.column_exist('name')
                and not table_h.column_exist('patient')):
            table_h.column_rename('name', 'patient')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class ProductTemplate(metaclass=PoolMeta):
    __name__ = 'product.template'

    """
    Allow to change the values from the product templates
    coming from XML files
    """
    @classmethod
    def check_xml_record(cls, records, values):
        return True


class Commands(ModelSQL, ModelView):
    'GNU Health Commands'
    __name__ = "gnuhealth.command"

    name = fields.Char('Command')
    label = fields.Char('Label')
    internal = fields.Boolean('Internal', help="Internal, Kernel commands")
    model = fields.Char('Model')
    domain = fields.Char('Domain')
    description = fields.Char("Description")

    @staticmethod
    def sysinfo():
        # Get Server side information
        info = ''
        os_header = "\n-- Operating System / Distribution --\n"
        uname = platform.uname()
        pversion = f"Python version: {str(platform.python_version())}\n"
        # Get OS version and related info.
        os_info = ''
        with open('/etc/os-release') as f:
            os_release = f.readlines()
            for line in os_release:
                os_info = os_info + str(line)
        # Get OS user, GH version and Tryton version
        gnuhealth_os_user = f"GNU Health user: {os.environ['USER']}\n"
        gnuhealth_version = f"GNU Health Server version:" \
            f"{importlib.metadata.version('gnuhealth')}\n"

        tryton_version = \
            f"Tryton server: {importlib.metadata.version('trytond')}\n"

        info = f"{info} {gnuhealth_version} {tryton_version}" \
            f"{gnuhealth_os_user}\n{pversion}\n" \
            f"{os_header}\n{os_info}\n" \
            f"Platform / Kernel Info: {str(uname)}\n"

        return info

    @classmethod
    def __setup__(cls):
        super(Commands, cls).__setup__()
        cls.__rpc__.update({
            'sysinfo': RPC(check_access=False),
        })


class Modules(metaclass=PoolMeta):
    __name__ = 'ir.module'

    # Add the module description field
    description = fields.Char("Description")


class Help(ModelSQL, ModelView):
    'GNU Health Help'
    __name__ = 'gnuhealth.help'

    ''' This model contains the documentation for the
        GNU Health Health and Hospital Management System.
        It can be invoked from the command line or by
        menu (Help).
    '''

    name = fields.Char("Code", required=True, help="Unique help code")
    description = fields.Char(
        "Description",
        help="Short description", required=True)
    category = fields.Char("Category")
    packages = fields.Char("Packages")
    keywords = fields.Char("Keywords")
    documentation = fields.Text("Documentation")

    @classmethod
    def __setup__(cls):
        super(Help, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('code_unique', Unique(t, t.name),
             'The help code already exists')
        ]

    @classmethod
    def __register__(cls, module):
        table_h = cls.__table_handler__(module)

        # Migration from 4.4: drop field category
        if (table_h.column_exist('package')):
            table_h.drop_column('package')

        super().__register__(module)
        table_h = cls.__table_handler__(module)


class OnlineDocument(ModelSQL, ModelView):
    'Health Online Document'
    __name__ = 'gnuhealth.online_document'

    name = fields.Char(
        'Name', required=True, translate=True,
        help='Health Online document name.')

    uri = fields.Char(
        'URI', required=True, translate=False,
        help="Health online document URL address.")

    @classmethod
    def __setup__(cls):
        super(OnlineDocument, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('code_unique', Unique(t, t.name),
             'The online document name already exists')
        ]


class ProceduresConfig(ModelSingleton, ModelSQL, ModelView):
    'Medical Procedures Config'
    __name__ = 'gnuhealth.procedures.config'

    medical_evaluation = fields.Many2One(
        'gnuhealth.procedure', 'Medical Evaluation',
        help="The code use for the procedure related to a medical"
             " encounter. If it exist, it will add up to the "
             "procedure list in the context of the medical evaluation"
    )

    ambulatory_care = fields.Many2One(
        'gnuhealth.procedure', 'Ambulatory care',
        help="The code use for the procedure related to an ambulatory"
             " care session. If it exist, it will add up to the "
             "procedure list in the context of ambulatory care"
    )
