#!/usr/bin/env python

# SPDX-FileCopyrightText: 2008-2025 Luis Falcón <falcon@gnuhealth.org>
# SPDX-FileCopyrightText: 2011-2025 GNU Solidario <health@gnusolidario.org>

# SPDX-License-Identifier: GPL-3.0-or-later

#########################################################################
#   Hospital Management Information System (HMIS) component of the      #
#                       GNU Health project                              #
#                   https://www.gnuhealth.org                           #
#########################################################################
#                     HEALTH INPATIENT package                          #
#                 health_inpatient.py: Main module                      #
#########################################################################
from datetime import datetime
from trytond.model import ModelView, ModelSQL, fields, Unique
from trytond.transaction import Transaction
from trytond.tools import grouped_slice, reduce_ids
from sql import Literal
from trytond.pool import Pool
from trytond.pyson import Eval, Not, Bool, And, Equal, Or
from trytond.i18n import gettext
from trytond.modules.health.core import get_health_professional, \
    get_institution

import pytz

from .exceptions import (
    NoAssociatedHealthProfessional,
    DischargeReasonNeeded, DischargeBeforeAdmission,
    BedIsNotAvailable, NeedTimeZone,
    AdmissionMustBeToday, SpecialMealNeeds
)


__all__ = [
    'DietTherapeutic', 'InpatientRegistration',
    'BedTransfer', 'Appointment', 'PatientEvaluation', 'PatientData',
    'InpatientMedication', 'InpatientMedicationAdminTimes',
    'InpatientMedicationLog', 'InpatientDiet', 'InpatientMeal',
    'InpatientMealOrder', 'InpatientMealOrderItem', 'ECG',
    'PatientRounding', 'RoundingProcedure']

# Therapeutic Diet types


class DietTherapeutic (ModelSQL, ModelView):
    'Diet Therapy'
    __name__ = "gnuhealth.diet.therapeutic"

    name = fields.Char('Diet type', required=True, translate=True)
    code = fields.Char('Code', required=True)
    description = fields.Text('Indications', required=True, translate=True)

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

        t = cls.__table__()
        cls._sql_constraints = [
            ('code_unique', Unique(t, t.code),
                'The Diet code already exists'),
        ]


class InpatientRegistration(ModelSQL, ModelView):
    'Patient admission History'
    __name__ = 'gnuhealth.inpatient.registration'

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

    name = fields.Char('Registration Code', readonly=True)
    patient = fields.Many2One(
        'gnuhealth.patient', 'Patient',
        required=True, states=STATES)
    admission_type = fields.Selection([
        (None, ''),
        ('routine', 'Routine'),
        ('maternity', 'Maternity'),
        ('elective', 'Elective'),
        ('urgent', 'Urgent'),
        ('emergency', 'Emergency'),
    ], 'Admission type', required=True, states=STATES)
    hospitalization_date = fields.DateTime(
        'Hospitalization date',
        required=True, states=STATES)
    discharge_date = fields.DateTime(
        'Expected Discharge Date', required=True,
        states=STATES)
    attending_physician = fields.Many2One(
        'gnuhealth.healthprofessional',
        'Attending Physician', states=STATES)
    operating_physician = fields.Many2One(
        'gnuhealth.healthprofessional',
        'Operating Physician', states=STATES)
    admission_reason = fields.Many2One(
        'gnuhealth.pathology',
        'Reason for Admission', help="Reason for Admission", states=STATES)
    bed = fields.Many2One(
        'gnuhealth.hospital.bed', 'Hospital Bed',
        states={
            'required': Not(Bool(Eval('name'))),
            'readonly': Or(
                Eval('state') == 'done',
                Eval('state') == 'finished',
                Bool(Eval('name')),
            )
        },
        depends=['name'])
    nursing_plan = fields.Text('Nursing Plan', states=STATES)
    medications = fields.One2Many(
        'gnuhealth.inpatient.medication', 'registration',
        'Medications', states=STATES)
    therapeutic_diets = fields.One2Many(
        'gnuhealth.inpatient.diet', 'registration',
        'Meals / Diet Program', states=STATES)

    nutrition_notes = fields.Text(
        'Nutrition notes / directions', states=STATES)
    discharge_plan = fields.Text(
        'Discharge Plan', states=STATES)
    info = fields.Text('Notes', states=STATES)
    state = fields.Selection((
        (None, ''),
        ('free', 'free'),
        ('cancelled', 'cancelled'),
        ('confirmed', 'confirmed'),
        ('hospitalized', 'hospitalized'),
        ('done', 'Discharged - needs cleaning'),
        ('finished', 'Finished'),
    ), 'Status', readonly=True)

    bed_transfers = fields.One2Many(
        'gnuhealth.bed.transfer', 'registration',
        'Transfer History', readonly=True)

    discharged_by = fields.Many2One(
        'gnuhealth.healthprofessional', 'Discharged by', readonly=True,
        help="Health Professional that discharged the patient")

    discharge_reason = fields.Selection([
        (None, ''),
        ('home', 'Home / Selfcare'),
        ('transfer', 'Transferred to another institution'),
        ('death', 'Death'),
        ('against_advice', 'Left against medical advice')],
        'Discharge Reason',
        states={'readonly': Not(Equal(Eval('state'), 'hospitalized'))},
        help="Reason for patient discharge")

    discharge_dx = fields.Many2One(
        'gnuhealth.pathology',
        'Discharge Dx', help="Code for Discharge Diagnosis",
        states={'readonly': Not(Equal(Eval('state'), 'hospitalized'))})

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

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

    @staticmethod
    def default_institution():
        return get_institution()

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

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

    @classmethod
    def __setup__(cls):
        super(InpatientRegistration, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('name_unique', Unique(t, t.name),
                'The Registration code already exists'),
        ]

        cls._buttons.update({
            'confirmed': {
                'invisible': And(
                    Not(Equal(Eval('state'), 'free')),
                    Not(Equal(Eval('state'), 'cancelled'))),
            },
            'cancel': {
                'invisible': Not(Equal(Eval('state'), 'confirmed')),
            },
            'admission': {
                'invisible': Not(Equal(Eval('state'), 'confirmed')),
            },
            'discharge': {
                'invisible': Not(Equal(Eval('state'), 'hospitalized')),
            },
            'bedclean': {
                'invisible': Not(Equal(Eval('state'), 'done')),
            },
        })

    # Method to check for availability and make the hospital bed reservation
    # Checks that there are not overlapping dates and status of the bed / room
    # is not confirmed, hospitalized or done but requiring cleaning ('done')
    @classmethod
    @ModelView.button
    def confirmed(cls, registrations):
        pool = Pool()
        Bed = pool.get('gnuhealth.hospital.bed')
        cursor = Transaction().connection.cursor()
        table = cls.__table__()

        beds = []
        for registration in registrations:
            if (registration.discharge_date.date() <
                    registration.hospitalization_date.date()):
                raise DischargeBeforeAdmission(
                    gettext('health_inpatient.msg_discharge_befor_admission'))
            cursor.execute(*table.select(
                table.id,
                where=((
                    (table.hospitalization_date <=
                     registration.hospitalization_date) &
                    (table.discharge_date >=
                     registration.hospitalization_date)
                ) |
                    ((table.hospitalization_date
                      <= registration.discharge_date)
                        & (table.discharge_date
                           >= registration.discharge_date)) |
                    ((table.hospitalization_date
                      >= registration.hospitalization_date)
                     & (table.discharge_date
                        <= registration.discharge_date)))
                & table.state.in_(['confirmed', 'hospitalized', 'done'])
                & (table.bed == registration.bed.id)))

            if cursor.fetchone():
                raise BedIsNotAvailable(
                    gettext('health_inpatient.msg_bed_is_not_available'))
            beds.append(registration.bed)
        cls.write(registrations, {'state': 'confirmed'})
        Bed.write(beds, {'state': 'reserved'})

    @classmethod
    @ModelView.button
    def discharge(cls, registrations):
        registration_id = registrations[0]
        Bed = Pool().get('gnuhealth.hospital.bed')

        signing_hp = get_health_professional()
        if not signing_hp:
            raise NoAssociatedHealthProfessional(
                gettext('health_inpatient.'
                        'msg_no_associated_health_professional'))

        cls.write(registrations, {
            'state': 'done', 'discharged_by': signing_hp})

        Bed.write([registration_id.bed], {'state': 'to_clean'})

    @classmethod
    @ModelView.button
    def bedclean(cls, registrations):
        registration_id = registrations[0]
        Bed = Pool().get('gnuhealth.hospital.bed')

        cls.write(registrations, {'state': 'finished'})

        Bed.write([registration_id.bed], {'state': 'free'})

    @classmethod
    @ModelView.button
    def cancel(cls, registrations):
        registration_id = registrations[0]
        Bed = Pool().get('gnuhealth.hospital.bed')

        cls.write(registrations, {'state': 'cancelled'})
        Bed.write([registration_id.bed], {'state': 'free'})

    @classmethod
    @ModelView.button
    def admission(cls, registrations):
        registration_id = registrations[0]
        Bed = Pool().get('gnuhealth.hospital.bed')

        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 = datetime.today()
                dt_local = datetime.astimezone(
                    dt.replace(tzinfo=pytz.utc), timezone)

                if (registration_id.hospitalization_date.date() !=
                        dt_local.date()):
                    raise AdmissionMustBeToday(
                        gettext('health_inpatient.'
                                'msg_admission_must_be_today'))
                else:
                    cls.write(registrations, {'state': 'hospitalized'})
                    Bed.write([registration_id.bed], {'state': 'occupied'})

            else:
                raise NeedTimeZone(
                    gettext('health_inpatient.msg_need_timezone'))

    @classmethod
    def generate_code(cls, **pattern):
        Config = Pool().get('gnuhealth.sequences')
        config = Config(1)
        sequence = config.get_multivalue(
            'inpatient_registration_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('name'):
                values['name'] = cls.generate_code()
        return super(InpatientRegistration, cls).create(vlist)

    @staticmethod
    def default_state():
        return 'free'

    @classmethod
    def validate(cls, registrations):
        super(InpatientRegistration, cls).validate(registrations)
        for registration in registrations:
            registration.check_discharge_context()

    def check_discharge_context(self):
        if ((not self.discharge_reason or not self.discharge_dx
            or not self.admission_reason)
                and self.state == 'done'):
            raise DischargeReasonNeeded(
                gettext('health_inpatient.msg_discharge_reason_needed'))

    # Format Registration ID : Patient : Bed
    def get_rec_name(self, name):
        if self.patient:
            return f'{self.name}:{self.bed.rec_name}:{self.patient.rec_name}'
        else:
            return self.name

    # Allow searching by the hospitalization code, patient name
    # or bed number

    @classmethod
    def search_rec_name(cls, name, clause):
        field = None
        # Search by Registration Code ID or Patient
        for field in ('name', 'patient', 'bed'):
            registrations = cls.search([(field,) + tuple(clause[1:])], limit=1)
            if registrations:
                break
        if registrations:
            return [(field,) + tuple(clause[1:])]
        return [(cls._rec_name,) + tuple(clause[1:])]


class BedTransfer(ModelSQL, ModelView):
    'Bed transfers'
    __name__ = 'gnuhealth.bed.transfer'

    registration = fields.Many2One(
        'gnuhealth.inpatient.registration',
        'Registration Code')
    transfer_date = fields.DateTime('Date')
    bed_from = fields.Many2One('gnuhealth.hospital.bed', 'From')
    bed_to = fields.Many2One('gnuhealth.hospital.bed', 'To')
    reason = fields.Char('Reason')

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

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

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


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

    inpatient_registration_code = fields.Many2One(
        'gnuhealth.inpatient.registration', 'Inpatient',
        help="Inpatient Registration, "
        "please enter the patient hospitalization code.")


class PatientEvaluation(ModelSQL, ModelView):
    __name__ = 'gnuhealth.patient.evaluation'

    inpatient_registration_code = fields.Many2One(
        'gnuhealth.inpatient.registration', 'IPC',
        help="Inpatient Code, "
        "please enter the patient hospitalization code")


class ECG(ModelSQL, ModelView):
    __name__ = 'gnuhealth.patient.ecg'

    inpatient_registration_code = fields.Many2One(
        'gnuhealth.inpatient.registration', 'Inpatient',
        help="Inpatient Registration, "
        "please enter the patient hospitalization code")


class PatientData(ModelSQL, ModelView):
    'Inherit patient model and add the patient status to the patient.'
    __name__ = 'gnuhealth.patient'

    patient_status = fields.Function(
        fields.Boolean(
            'Hospitalized',
            help="Show the hospitalization status of the patient"),
        getter='get_patient_status', searcher='search_patient_status')

    @classmethod
    def get_patient_status(cls, patients, name):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        Registration = pool.get('gnuhealth.inpatient.registration')
        registration = Registration.__table__()

        # Will store statuses {patient: True/False, ...}
        ids = list(map(int, patients))
        result = dict.fromkeys(ids, False)

        for sub_ids in grouped_slice(ids):
            # SQL expression for relevant patient ids
            clause_ids = reduce_ids(registration.id, sub_ids)

            # Hospitalized patient ids
            query = registration.select(
                registration.patient, Literal(True),
                where=(registration.state == 'hospitalized') & clause_ids,
                group_by=registration.patient)

            # Update dictionary of patient ids with True statuses
            cursor.execute(*query)
            result.update(cursor.fetchall())

        return result

    @classmethod
    def search_patient_status(cls, name, clause):
        p = Pool().get('gnuhealth.inpatient.registration')
        table = p.__table__()
        pat = cls.__table__()
        _, operator, value = clause

        # Validate operator and value
        if operator not in ['=', '!=']:
            raise ValueError('Wrong operator: %s' % operator)
        if value is not True and value is not False:
            raise ValueError('Wrong value: %s' % value)

        # Find hospitalized patient ids
        j = pat.join(table, condition=pat.id == table.patient)
        s = j.select(pat.id, where=table.state == 'hospitalized')

        # Choose domain operator
        if (operator == '=' and value) or (operator == '!=' and not value):
            d = 'in'
        else:
            d = 'not in'

        return [('id', d, s)]


class InpatientMedication (ModelSQL, ModelView):
    'Inpatient Medication'
    __name__ = 'gnuhealth.inpatient.medication'

    registration = fields.Many2One(
        'gnuhealth.inpatient.registration',
        'Registration Code')
    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', required=True)
    end_treatment = fields.DateTime('End', help='Date of start of Treatment')
    dose = fields.Float(
        'Dose',
        help='Amount of medication (eg, 250 mg) per dose', required=True)
    dose_unit = fields.Many2One(
        'gnuhealth.dose.unit', 'dose unit',
        required=True, help='Unit of measure for the medication to be taken')
    route = fields.Many2One(
        'gnuhealth.drug.route', 'Administration Route',
        required=True, help='Drug administration route code.')
    form = fields.Many2One(
        'gnuhealth.drug.form', 'Form', required=True,
        help='Drug form, such as tablet or gel')
    qty = fields.Integer(
        'x', required=True,
        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.One2Many(
        'gnuhealth.inpatient.medication.admin_time',
        'medication', "Admin times")
    log_history = fields.One2Many(
        'gnuhealth.inpatient.medication.log', 'medication',
        "Log History")
    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')

    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')

    @fields.depends('discontinued', 'course_completed')
    def on_change_with_is_active(self):
        is_active = True
        if (self.discontinued or self.course_completed):
            is_active = False
        return is_active

    @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

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

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

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


class InpatientMedicationAdminTimes (ModelSQL, ModelView):
    'Inpatient Medication Admin Times'
    __name__ = "gnuhealth.inpatient.medication.admin_time"

    medication = fields.Many2One(
        'gnuhealth.inpatient.medication', 'Medication')
    admin_time = fields.Time("Time")
    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')
    remarks = fields.Text(
        'Remarks',
        help='specific remarks for this dose')

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

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

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


class InpatientMedicationLog (ModelSQL, ModelView):
    'Inpatient Medication Log History'
    __name__ = "gnuhealth.inpatient.medication.log"

    medication = fields.Many2One(
        'gnuhealth.inpatient.medication', 'Medication')
    admin_time = fields.DateTime("Date", readonly=True)
    health_professional = fields.Many2One(
        'gnuhealth.healthprofessional',
        'Health Prof', readonly=True)
    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')
    remarks = fields.Text(
        'Remarks',
        help='specific remarks for this dose')

    @classmethod
    def validate(cls, records):
        super(InpatientMedicationLog, cls).validate(records)
        for record in records:
            record.check_health_professional()

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

    @staticmethod
    def default_health_professional():
        return get_health_professional()

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

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

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

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

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

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


class InpatientDiet (ModelSQL, ModelView):
    'Inpatient Diet'
    __name__ = "gnuhealth.inpatient.diet"

    registration = fields.Many2One(
        'gnuhealth.inpatient.registration',
        'Registration Code')
    diet = fields.Many2One('gnuhealth.diet.therapeutic', 'Diet', required=True)
    remarks = fields.Text(
        'Remarks / Directions',
        help='specific remarks for this diet / patient')

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

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

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


class InpatientMeal (ModelSQL, ModelView):
    'Inpatient Meal'
    __name__ = "gnuhealth.inpatient.meal"

    meal = fields.Many2One(
        'product.product', 'Meal', required=True,
        help='Meal')

    diet_therapeutic = fields.Many2One(
        'gnuhealth.diet.therapeutic',
        'Diet')

    diet_belief = fields.Many2One('gnuhealth.diet.belief', 'Belief')

    diet_vegetarian = fields.Many2One(
        'gnuhealth.vegetarian_types',
        'Vegetarian')

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

    @staticmethod
    def default_institution():
        return get_institution()

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

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

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

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


class InpatientMealOrderItem (ModelSQL, ModelView):
    'Inpatient Meal Item'
    __name__ = "gnuhealth.inpatient.meal.order.item"

    meal_order = fields.Many2One(
        'gnuhealth.inpatient.meal.order',
        'Meal Order')

    meal = fields.Many2One('gnuhealth.inpatient.meal', 'Meal')
    remarks = fields.Char('Remarks')

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

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

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


class InpatientMealOrder (ModelSQL, ModelView):
    'Inpatient Meal Order'
    __name__ = "gnuhealth.inpatient.meal.order"

    registration = fields.Many2One(
        'gnuhealth.inpatient.registration',
        'Registration Code', domain=[('state', '=', 'hospitalized')],
        required=True)

    mealtime = fields.Selection((
        (None, ''),
        ('breakfast', 'Breakfast'),
        ('lunch', 'Lunch'),
        ('dinner', 'Dinner'),
        ('snack', 'Snack'),
        ('special', 'Special order'),
    ), 'Meal time', required=True, sort=False)

    meal_item = fields.One2Many(
        'gnuhealth.inpatient.meal.order.item', 'meal_order',
        'Items')

    meal_order = fields.Char('Order', readonly=True)

    health_professional = fields.Many2One(
        'gnuhealth.healthprofessional',
        'Health Prof')

    remarks = fields.Text('Remarks')

    meal_warning = fields.Boolean(
        'Warning',
        help="The patient has special needs on meals")

    meal_warning_ack = fields.Boolean(
        'Ack',
        help="Check if you have verified the warnings on the"
        " patient meal items")

    order_date = fields.DateTime('Date', help='Order date', required=True)

    state = fields.Selection((
        (None, ''),
        ('draft', 'Draft'),
        ('cancelled', 'Cancelled'),
        ('ordered', 'Ordered'),
        ('processing', 'Processing'),
        ('done', 'Done'),
    ), 'Status', readonly=True)

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

    @staticmethod
    def default_state():
        return 'draft'

    @staticmethod
    def default_health_professional():
        return get_health_professional()

    @classmethod
    def generate_code(cls, **pattern):
        Config = Pool().get('gnuhealth.sequences')
        config = Config(1)
        sequence = config.get_multivalue(
            'inpatient_meal_order_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('meal_order'):
                values['meal_order'] = cls.generate_code()
        return super(InpatientMealOrder, cls).create(vlist)

    @classmethod
    def validate(cls, meal_orders):
        super(InpatientMealOrder, cls).validate(meal_orders)
        for meal_order in meal_orders:
            meal_order.check_meal_order_warning()
            meal_order.check_health_professional()

    def check_meal_order_warning(self):
        if not self.meal_warning_ack and self.meal_warning:
            raise SpecialMealNeeds(
                gettext('health_inpatient.msg_special_meal_needs'))

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

    @fields.depends('registration', '_parent_registration.patient')
    def on_change_registration(self):
        if self.registration:
            # Trigger the warning if the patient
            # has special needs on meals (religion / philosophy )
            if (self.registration.patient.vegetarian_type or
                    self.registration.patient.diet_belief):
                self.meal_warning = True
                self.meal_warning_ack = False

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

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

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

    @classmethod
    def __setup__(cls):
        super(InpatientMealOrder, cls).__setup__()
        cls._buttons.update({
            'cancel': {'invisible': Not(Equal(Eval('state'), 'ordered'))}
        })

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

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

        t = cls.__table__()
        cls._sql_constraints = [
            ('meal_order_uniq', Unique(t, t.meal_order),
                'The Meal Order code already exists'),
        ]

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

    @classmethod
    @ModelView.button
    def generate(cls, mealorders):
        cls.write(mealorders, {
            'state': 'ordered'})

    @classmethod
    @ModelView.button
    def cancel(cls, mealorders):
        cls.write(mealorders, {
            'state': 'cancelled'})

    @classmethod
    @ModelView.button
    def done(cls, mealorders):
        cls.write(mealorders, {
            'state': 'done'})


# Class : PatientRounding
# Assess the patient and evironment periodically
# Usually done by nurses

class PatientRounding(ModelSQL, ModelView):
    'Patient Rounding'
    __name__ = 'gnuhealth.patient.rounding'

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

    registration = fields.Many2One(
        'gnuhealth.inpatient.registration',
        'Registration Code', required=True, states=STATES)
    code = fields.Char('Code', readonly=True)
    health_professional = fields.Many2One(
        'gnuhealth.healthprofessional',
        'Health Prof', readonly=True)
    evaluation_start = fields.DateTime('Start', required=True, states=STATES)
    evaluation_end = fields.DateTime('End', readonly=True)

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

    environmental_assessment = fields.Char(
        'Environment', help="Environment"
        " assessment . State any disorder in the room.", states=STATES)

    weight = fields.Integer(
        'Weight',
        help="Measured weight, in kg", states=STATES)

    # The 6 P's of rounding
    pain = fields.Boolean(
        'Pain',
        help="Check if the patient is in pain", states=STATES)

    pain_level = fields.Integer(
        'Pain level',
        help="Enter the pain level, from 1 to 10.",
        states={'readonly': Eval('state') == 'done'})

    # Use by round_report template
    def get_report_pain_and_level(self):
        if self.pain and self.pain_level:
            return gettext('health_nursing.msg_report_pain_level',
                           pain_level=str(self.pain_level))
        elif self.pain:
            return gettext('health_nursing.msg_report_pain_yes')
        else:
            return gettext('health_nursing.msg_report_pain_no')

    potty = fields.Boolean(
        'Potty', help="Check if the patient needs to "
        "urinate / defecate", states=STATES)
    position = fields.Boolean(
        'Position', help="Check if the patient needs to "
        "be repositioned or is unconfortable", states=STATES)
    proximity = fields.Boolean(
        'Proximity', help="Check if personal items, "
        "water, alarm, ... are not in easy reach", states=STATES)
    pump = fields.Boolean(
        'Pumps', help="Check if there is any issues with "
        "the pumps - IVs ...", states=STATES)
    personal_needs = fields.Boolean(
        'Personal needs', help="Check if the "
        "patient requests anything", states=STATES)

    # Vital Signs
    systolic = fields.Integer('Systolic Pressure', states=STATES)
    diastolic = fields.Integer('Diastolic Pressure', states=STATES)
    bpm = fields.Integer(
        'Heart Rate',
        help='Heart rate expressed in 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='Oxygen Saturation(arterial).', states=STATES)
    temperature = fields.Float(
        'Temperature',
        help='Temperature in celsius', states=STATES)

    # Diuresis

    diuresis = fields.Integer('Diuresis', help="volume in ml", states=STATES)
    urinary_catheter = fields.Boolean('Urinary Catheter', states=STATES)

    # Glycemia
    glycemia = fields.Integer(
        'Glycemia', help='Blood Glucose level', states=STATES)

    depression = fields.Boolean(
        'Depression signs', help="Check this if the "
        "patient shows signs of depression", states=STATES)
    evolution = fields.Selection(
        [
            (None, ''),
            ('n', 'Status Quo'),
            ('i', 'Improving'),
            ('w', 'Worsening'),
        ], 'Evolution', help="Check your judgement of current "
        "patient condition", sort=False, states=STATES)

    evolution_str = evolution.translated('evolution')

    round_summary = fields.Text('Round Summary', states=STATES)

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

    warning = fields.Boolean(
        'Warning', help="Check this box to alert the "
        "supervisor about this patient rounding. A warning icon will be shown "
        "in the rounding list", states=STATES)
    warning_icon = fields.Function(
        fields.Char('Warning Icon'), 'get_warn_icon')

    rounding_procedures = fields.One2Many(
        'gnuhealth.patient.procedure', 'reference', 'Procedures',
        domain=[
            ('patient', '=', Eval('patient')),
            ('ctx', '=', 'rounding'),
            # ('pdate', '=', Eval('evaluation_start')),
        ],
        depends=['registration', 'patient'],
        help='Procedures done during the rounding')

    # Deprecated in 5.0 by rounding_procedures
    procedures = fields.One2Many(
        'gnuhealth.rounding_procedure', 'rounding',
        'Procedures', help="List of the procedures in this rounding. Please "
        "enter the first one as the main procedure", states=STATES)

    report_start_date = fields.Function(
        fields.Date('Start Date'),
        'get_report_start_date')
    report_start_time = fields.Function(
        fields.Time('Start Time'),
        'get_report_start_time')
    report_end_date = fields.Function(
        fields.Date('End Date'),
        'get_report_end_date')
    report_end_time = fields.Function(
        fields.Time('End Time'),
        'get_report_end_time')

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

    def get_patient(self, name):
        if (self.registration):
            return self.registration.patient

    # Show patient upon entering the registration
    @fields.depends('registration')
    def on_change_registration(self):
        if (self.registration):
            self.patient = self.registration.patient

    @staticmethod
    def default_health_professional():
        return get_health_professional()

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

    @staticmethod
    def default_state():
        return 'draft'

    @classmethod
    @ModelView.button
    def end_rounding(cls, roundings):
        # End the rounding

        # Change the state of the rounding to "Done"
        signing_hp = get_health_professional()

        cls.write(roundings, {
            'state': 'done',
            'signed_by': signing_hp,
            'evaluation_end': datetime.now()
        })

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

    def get_rec_name(self, name):
        if self.code:
            return f"{self.code}"

    @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(PatientRounding, cls).create(vlist)

    @classmethod
    def validate(cls, roundings):
        super(PatientRounding, cls).validate(roundings)
        for rounding in roundings:
            rounding.check_health_professional()

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

    def get_report_start_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_start_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()

    def get_report_end_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_end
        return datetime.astimezone(
            dt.replace(tzinfo=pytz.utc), timezone).date()

    def get_report_end_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_end
        return datetime.astimezone(
            dt.replace(tzinfo=pytz.utc), timezone).time()

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

    @classmethod
    def __setup__(cls):
        super(PatientRounding, cls).__setup__()
        cls._buttons.update({
            'end_rounding': {
                'invisible': ~Eval('state').in_(['draft']),
            }})

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

        # 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 registration
        if (table_h.column_exist('name')
                and not table_h.column_exist('registration')):
            table_h.column_rename('name', 'registration')

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


# Deprecated in GH 5.0 by general health.PatientProcedure
class RoundingProcedure(ModelSQL, ModelView):
    'Rounding - Procedure'
    __name__ = 'gnuhealth.rounding_procedure'

    rounding = fields.Many2One('gnuhealth.patient.rounding', 'Rounding')
    procedure = fields.Many2One(
        'gnuhealth.procedure', 'Code', required=True,
        help="Procedure Code, for example ICD-10-PCS Code 7-character string")
    notes = fields.Text('Notes')

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

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

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