Exemple #1
0
class patient(object):
    '''
    this class pulls information from the database into a python object.
    '''
    def __init__(self, sno):
        '''
        initiate the class with default variables, then load from database
        '''
        self.serialno = sno
        self.dbstate = None

        self.load_warnings = []

        # patient table atts
        self.courseno0 = None
        self.money0 = 0
        self.money1 = 0
        self.money2 = 0
        self.money3 = 0
        self.money4 = 0
        self.money5 = 0
        self.money6 = 0
        self.money7 = 0
        self.money8 = 0
        self.money9 = 0
        self.money10 = 0
        self.pd0 = None
        self.pd1 = None
        self.pd2 = None
        self.pd3 = None
        self.pd4 = None  # this field is no longer used (last treatment date)
        self.pd5 = None
        self.pd6 = None
        self.pd7 = None
        self.pd8 = None
        self.pd9 = None
        self.pd10 = None
        self.pd11 = None
        self.pd12 = None
        self.pd13 = None
        self.pd14 = None
        self.sname = ''
        self.fname = ''
        self.title = ''
        self.sex = ''
        self.dob = None
        self.addr1 = ''
        self.addr2 = ''
        self.addr3 = ''
        self.pcde = ''
        self.tel1 = ''
        self.tel2 = ''
        self.occup = ''
        self.nhsno = ''
        self.cnfd = None
        self.cset = ''
        self.dnt1 = 0
        self.dnt2 = 0
        self.ur8st = ''
        self.ur7st = ''
        self.ur6st = ''
        self.ur5st = ''
        self.ur4st = ''
        self.ur3st = ''
        self.ur2st = ''
        self.ur1st = ''
        self.ul1st = ''
        self.ul2st = ''
        self.ul3st = ''
        self.ul4st = ''
        self.ul5st = ''
        self.ul6st = ''
        self.ul7st = ''
        self.ul8st = ''
        self.ll8st = ''
        self.ll7st = ''
        self.ll6st = ''
        self.ll5st = ''
        self.ll4st = ''
        self.ll3st = ''
        self.ll2st = ''
        self.ll1st = ''
        self.lr1st = ''
        self.lr2st = ''
        self.lr3st = ''
        self.lr4st = ''
        self.lr5st = ''
        self.lr6st = ''
        self.lr7st = ''
        self.lr8st = ''
        self.dent0 = 0
        self.dent1 = 0
        self.dent2 = 0
        self.dent3 = 0
        self.billdate = None
        self.billct = 0
        self.billtype = None
        self.money11 = 0
        self.familyno = localsettings.last_family_no
        self.memo = ''
        self.town = ''
        self.county = ''
        self.mobile = ''
        self.fax = ''
        self.email1 = ''
        self.email2 = ''
        self.status = ''
        self.initaccept = 0
        self.lastreaccept = None
        self.lastclaim = None
        self.expiry = None
        self.cstatus = None
        self.transfer = 0
        self.pstatus = None

        self.estimates = []

        # from userdata
        self.plandata = PlanData(self.serialno)

        # NEIL'S STUFF####
        self.exemption = ""
        self.exempttext = ""
        self.bpe = []
        self.bpedate = nullDate
        self.chartdate = nullDate
        self.notes_dict = {}
        self.MEDALERT = False
        self.mh_chkdate = None
        self.mh_form_date = None
        self.HIDDENNOTES = []
        self.chartgrid = {}
        self._fee_table = None
        self.synopsis = ""
        self._n_family_members = None
        self._dayBookHistory = None
        self.treatment_course = None
        self.est_logger = None
        self._most_recent_daybook_entry = None
        self._first_note_date = None
        self._has_exam_booked = None
        self._previous_surnames = None
        self.monies_reset = False
        self._n_hyg_visits = None

        if self.serialno == 0:
            return

        #
        # now load stuff from the database ##
        #
        db = connect.connect()
        cursor = db.cursor()

        self.getSynopsis()

        cursor.execute(PATIENT_QUERY, (self.serialno, ))
        values = cursor.fetchall()

        if values == ():
            raise localsettings.PatientNotFoundError

        for i, att_ in enumerate(patient_query_atts):
            value = values[0][i]
            if value is not None:
                self.__dict__[att_] = value
            elif att_ == "familyno":
                self.familyno = 0

        query = '''select exemption, exempttext from exemptions
        where serialno=%s'''
        cursor.execute(query, (self.serialno, ))

        values = cursor.fetchall()

        for value in values:
            self.exemption, self.exempttext = value

        query = '''select bpedate, bpe from bpe where serialno=%s
        order by bpedate'''
        cursor.execute(query, (self.serialno, ))

        values = cursor.fetchall()

        for value in values:
            self.bpe.append(value)

        if self.courseno0 != 0:
            self.getEsts()

        self.treatment_course = TreatmentCourse(self.serialno, self.courseno0)

        self.getNotesTuple()

        cursor.execute(QUICK_MED_QUERY, (self.serialno, ))
        try:
            self.MEDALERT, self.mh_chkdate = cursor.fetchone()
        except TypeError:
            pass

        cursor.execute(MED_FORM_QUERY + " limit 1", (self.serialno, ))
        try:
            self.mh_form_date = cursor.fetchone()[0]
        except TypeError:
            pass
        cursor.close()

        # - load from plandata
        self.plandata.getFromDB()

        self.appt_prefs = ApptPrefs(self.serialno)

        self.updateChartgrid()

        self.take_snapshot()

    @property
    def appt_memo(self):
        return self.appt_prefs.note

    def set_appt_memo(self, memo):
        self.appt_prefs.note = memo

    @property
    def recall_active(self):
        return self.appt_prefs.recall_active

    @property
    def exam_due(self):
        return self.recall_active and self.recd < localsettings.currentDay()

    @property
    def recd(self):
        return self.appt_prefs.recdent

    @property
    def dayBookHistory(self):
        if self._dayBookHistory is None:
            db = connect.connect()
            cursor = db.cursor()
            query = 'select date, trtid, chart from daybook where serialno=%s'
            cursor.execute(query, (self.serialno, ))
            self._dayBookHistory = cursor.fetchall()
            cursor.close()
        return self._dayBookHistory

    @property
    def last_treatment_date(self):
        max_date = localsettings.currentDay()
        if self.treatment_course.cmp_txs != \
                self.dbstate.treatment_course.cmp_txs:
            return max_date
        if self._most_recent_daybook_entry is None:
            db = connect.connect()
            cursor = db.cursor()
            query = 'select max(date) from daybook where serialno=%s'
            if cursor.execute(query, (self.serialno, )):
                max_date = cursor.fetchone()[0]
            cursor.close()
            self._most_recent_daybook_entry = max_date
        return self._most_recent_daybook_entry

    @property
    def first_note_date(self):
        '''
        returns teh first date found in the patient notes
        '''
        if self._first_note_date is None:
            min_date = localsettings.currentDay()
            db = connect.connect()
            cursor = db.cursor()
            query = 'select min(ndate) from formatted_notes where serialno=%s'
            if cursor.execute(query, (self.serialno, )):
                min_date = cursor.fetchone()[0]
            cursor.close()
            self._first_note_date = min_date \
                if min_date else localsettings.currentDay()
        return self._first_note_date

    @property
    def n_hyg_visits(self):
        if self._n_hyg_visits is not None:
            pass
        elif not localsettings.hyg_ixs:
            self._n_hyg_visits = 0
        else:
            if len(localsettings.hyg_ixs) == 1:
                conditional = "="
                values = (self.serialno, localsettings.hyg_ixs[0])
            else:
                conditional = "in"
                values = (self.serialno, localsettings.hyg_ixs)
            self._n_hyg_visits = 0
            db = connect.connect()
            cursor = db.cursor()
            query = '''select count(*) from
            (select date from daybook where serialno=%%s and
            trtid %s %%s group by date) as t''' % conditional
            if cursor.execute(query, values):
                self._n_hyg_visits = cursor.fetchone()[0]
            cursor.close()
        return self._n_hyg_visits

    def forget_exam_booked(self):
        self._has_exam_booked = None

    @property
    def has_exam_booked(self):
        if self._has_exam_booked is None:
            db = connect.connect()
            cursor = db.cursor()
            cursor.execute(FUTURE_EXAM_QUERY, (self.serialno, ))
            self._has_exam_booked = bool(cursor.fetchone()[0])
            cursor.close()

        return self._has_exam_booked

    def __repr__(self):
        return "'Patient_class instance - serialno %d'" % self.serialno

    @property
    def address(self):
        '''
        a printable address
        '''
        address = ""
        for line in (self.addr1, self.addr2, self.addr3, self.town,
                     self.county, self.pcde):
            if line.strip(" ") != "":
                address += "%s\n" % line.strip(" ")
        return address

    def getAge(self, on_date=None):
        '''
        return the age in form (year(int), months(int), isToday(bool))
        '''
        if on_date is None:
            # use today
            on_date = localsettings.currentDay()

        try:
            nextbirthday = datetime.date(on_date.year, self.dob.month,
                                         self.dob.day)
        except ValueError:
            # catch leap years!!
            nextbirthday = datetime.date(on_date.year, self.dob.month,
                                         self.dob.day - 1)

        ageYears = on_date.year - self.dob.year

        if nextbirthday > on_date:
            ageYears -= 1
            months = (12 - self.dob.month) + on_date.month
        else:
            months = on_date.month - self.dob.month
        if self.dob.day > on_date.day:
            months -= 1

        isToday = nextbirthday == localsettings.currentDay()

        return (ageYears, months, isToday)

    @property
    def ageYears(self):
        return self.getAge()[0]

    @property
    def age_course_start(self):
        '''
        returns a tuple (year, months) for the patient at accd
        '''
        return self.getAge(self.treatment_course.accd)[:2]

    @property
    def under_6(self):
        '''
        returns a bool "is patient under 6?".
        '''
        return self.ageYears < 6

    @property
    def under_18(self):
        '''
        returns a bool "is patient under 18?".
        '''
        return self.ageYears < 18

    def forget_fee_table(self):
        self._fee_table = None

    @property
    def fee_table(self):
        '''
        logic to determine which feeTable should be used for standard items
        '''
        if self._fee_table is None:
            if self.treatment_course.accd is None:
                cse_accd = localsettings.currentDay()
            else:
                cse_accd = self.treatment_course.accd
            for table in reversed(localsettings.FEETABLES.tables.values()):
                LOGGER.debug(
                    "checking feescale %s to see if suitable a feetable",
                    table)

                start, end = table.startDate, table.endDate
                LOGGER.debug("categories, start, end = %s, %s, %s",
                             table.categories, start, end)
                if end is None:
                    end = localsettings.currentDay()

                if self.cset in table.categories and start <= cse_accd <= end:
                    self._fee_table = table

            if self._fee_table is None:
                # - no matching table found, use the default.
                LOGGER.warning("NO SUITABLE FEETABLE FOUND, RETURNING DEFAULT")
                self._fee_table = localsettings.FEETABLES.default_table

        return self._fee_table

    def getEsts(self):
        '''
        get estimate data
        '''
        self.estimates = db_estimates.get_ests(self.serialno, self.courseno0)
        self.est_logger = EstLogger(self.courseno0)

    def getSynopsis(self):
        '''
        the synopsis line is displayed on the clinical summary page
        '''
        db = connect.connect()
        cursor = db.cursor()
        try:
            if cursor.execute(SYNOPSIS_QUERY, (self.serialno, )):
                self.synopsis = cursor.fetchall()[-1][0]
        except connect.OperationalError:
            # - necessary because the column is missing is db schema 1.4
            LOGGER.warning("invalid schema for getSynopsis")

    @property
    def underTreatment(self):
        '''
        a boolean value stating whether the patient has a continuing treatment
        plan
        '''
        return (self.treatment_course is not None
                and self.treatment_course.underTreatment)

    @property
    def max_tx_courseno(self):
        '''
        a patient who has had many courses of treatment, this gets the
        latest
        '''
        return self.treatment_course.max_tx_courseno

    @property
    def newer_course_found(self):
        '''
        check for a newer course in the currtrtmt2 table than the one loaded
        at startup.
        '''
        return self.treatment_course.newer_course_found

    def getNotesTuple(self):
        '''
        connect and poll the formatted_notes table
        '''
        self.notes_dict = formatted_notes.get_notes_dict(self.serialno)

    def flipDec_Perm(self, tooth):
        '''
        switches a deciduous tooth to a permanent one,
        and viceVersa pass a variable like "ur5"
        '''
        quadrant = tooth[:2]
        pos = int(tooth[2]) - 1  # will be 0-7
        if quadrant == "ul":
            var = self.dent1
            pos = 7 - pos
        elif quadrant == "ur":
            var = self.dent0
        elif quadrant == "ll":
            var = self.dent2
        else:  # lr
            var = self.dent3
            pos = 7 - pos
        existing = dec_perm.fromSignedByte(var)
        if existing[pos] == "1":
            existing = existing[:pos] + "0" + existing[pos + 1:]
        else:
            existing = existing[:pos] + "1" + existing[pos + 1:]
        if quadrant == "ul":
            self.dent1 = dec_perm.toSignedByte(existing)
        elif quadrant == "ur":
            self.dent0 = dec_perm.toSignedByte(existing)
        elif quadrant == "ll":
            self.dent2 = dec_perm.toSignedByte(existing)
        else:  # lr
            self.dent3 = dec_perm.toSignedByte(existing)
        self.updateChartgrid()

    def updateChartgrid(self):
        '''
        a legacy issue with openmolar is the way teeth are saved as present
        is as 4 bytes (32 bits = 32 teeth). very frugal storage, but requires
        a fair deal of client computation :(
        '''
        grid = ""
        for quad in (self.dent1, self.dent0, self.dent3, self.dent2):
            grid += dec_perm.fromSignedByte(quad)
        for pos in mouth:
            if grid[mouth.index(pos)] == "0":
                self.chartgrid[pos] = pos
            else:
                self.chartgrid[pos] = decidmouth[mouth.index(pos)]

    def apply_fees(self):
        '''
        update the money owed.
        '''
        LOGGER.debug("Applying Fees")
        if "N" in self.cset:
            self.money0 = self.dbstate.money0 + self.fees_accrued
        else:
            self.money1 = self.dbstate.money1 + self.fees_accrued

    @property
    def fees(self):
        '''
        calculate what money is due.
        '''
        return int(self.money0 + self.money1 + self.money9 + self.money10 +
                   self.money11 - self.money2 - self.money3 - self.money8)

    @property
    def fees_accrued(self):
        '''
        what fees have changed since load.
        '''
        old_estimate_charges = 0
        if self.courseno0 == self.dbstate.courseno0:
            old_estimate_charges = self.dbstate.estimate_charges

        accrued_fees = self.estimate_charges - old_estimate_charges
        LOGGER.debug("fees_accrued = (new-existing) = %d - %d = %d",
                     self.estimate_charges, old_estimate_charges, accrued_fees)
        return accrued_fees

    @property
    def estimate_charges(self):
        '''
        charges for all completed treatments.
        '''
        charges = 0
        for est in self.estimates:
            if est.completed == 2:
                charges += est.ptfee
            elif est.completed == 1:
                charges += est.interim_pt_fee
        return charges

    @property
    def est_logger_text(self):
        '''
        a summary of the estimate for use in the est_logger_table
        est_logger is unconcerned whether treatment is completed etc..
        '''
        text = ""
        total, p_total = 0, 0
        for estimate in sorted(self.estimates):
            text += estimate.log_text
            total += estimate.fee
            p_total += estimate.ptfee
        text += "TOTAL ||  ||  ||  ||  ||  || %s || %s" % (total, p_total)
        return text

    def resetAllMonies(self):
        '''
        gets money1 and money 0 from apply_fees,
        then equalises money3 and money2 accordingly.
        zero's everything else
        money11 (bad debt) is left unaltered.
        '''
        self.dbstate.money0 = 0
        self.dbstate.money1 = 0
        self.monies_reset = True

        self.money0 = 0
        self.money1 = 0
        self.apply_fees()
        self.money9 = 0
        self.money10 = 0
        self.money2 = self.money0
        self.money3 = self.money1
        self.money8 = 0

    def nhs_claims(self, completed_only=True):
        '''
        nhs items from the estimates.
        if completed_only is False, then include planned items.
        '''
        claims = []
        for est in self.estimates:
            if est.csetype.startswith("N") and \
                    (not completed_only or est.completed == 2):
                claims.append(est)
        return claims

    def addHiddenNote(self,
                      ntype,
                      note="",
                      attempt_delete=False,
                      one_only=False):
        '''
        re-written for schema 1.9
        '''
        LOGGER.info(
            "addHiddenNote - ntype='%s',note='%s', attempt_delete='%s'", ntype,
            note, attempt_delete)

        HN = ()
        if ntype == "payment":
            HN = ("RECEIVED: ", note)
        elif ntype == "printed":
            HN = ("PRINTED: ", note)
        elif ntype == "exam":
            HN = ("TC: EXAM", note)
        elif ntype == "chart_treatment":
            HN = ("TC:", note)
        elif ntype == "perio_treatment":
            HN = ("TC: PERIO", note)
        elif ntype == "xray_treatment":
            HN = ("TC: XRAY", note)
        elif ntype == "treatment":
            HN = ("TC: OTHER", note)
        elif ntype == "mednotes":  # other treatment
            HN = ("UPDATED:Medical Notes", note)
        elif ntype == "close_course":
            HN = ("COURSE CLOSED", "=" * 10)
        elif ntype == "open_course":
            HN = ("COURSE OPENED", "= " * 5)
        elif ntype == "resume_course":
            HN = ("COURSE RE-OPENED", "= " * 5)
        elif ntype == "fee":
            HN = ("INTERIM: ", note)

        if not HN:
            LOGGER.warning("unable to add Hidden Note notetype '%s' not found",
                           ntype)
            return

        reversing_note = ("UNCOMPLETED", "{%s}" % note)

        if attempt_delete:
            try:
                self.HIDDENNOTES.remove(HN)
            except ValueError:
                LOGGER.debug("'%s' not in hiddenotes", HN)
                LOGGER.debug(self.HIDDENNOTES)
                self.HIDDENNOTES.append(reversing_note)
        else:
            try:
                self.HIDDENNOTES.remove(reversing_note)
            except ValueError:
                self.HIDDENNOTES.append(HN)

        if one_only:
            while self.HIDDENNOTES.count(HN) > 1:
                self.HIDDENNOTES.remove(HN)

    def clearHiddenNotes(self):
        '''
        reset self.HIDDENNOTES
        '''
        self.HIDDENNOTES = []

    def updateBilling(self, tone):
        '''
        update the last billdate and tone of invoice
        '''
        self.billdate = localsettings.currentDay()
        self.billct += 1
        self.billtype = tone

    def reset_billing(self):
        '''
        if patients account is now is order, reset all billing params
        '''
        if self.fees == 0:
            self.billdate = None
            self.billct = None
            self.billtype = None

    def treatmentOutstanding(self):
        '''
        does the patient have treatmentOutstanding?
        returns a boolean
        '''
        return (self.treatment_course
                and self.treatment_course.has_treatment_outstanding)

    def checkExemption(self):
        '''
        see if the patient's exemption requires removal.
        '''
        if (self.exemption == "S"
                and self.getAge(self.treatment_course.accd)[0] > 19):
            self.exemption = ""
            self.load_warnings.append(_("Student Exemption removed"))
        elif (self.exemption == "A"
              and self.getAge(self.treatment_course.accd)[0] > 18):
            self.exemption = ""
            self.load_warnings.append(_("Age Exemption removed"))
        else:
            return True

    @property
    def name_id(self):
        '''
        name and serialno
        '''
        return u"%s - %s" % (self.name, self.serialno)

    @property
    def name(self):
        '''
        patients name in a readable form
        '''
        return u"%s %s %s" % (self.title, self.fname, self.sname)

    @property
    def psn(self):
        '''
        previous surname
        '''
        try:
            return self.previous_surnames[0]
        except IndexError:
            return ""

    @property
    def previous_surnames(self):
        '''
        previous surnames are stored.
        ## TODO - check this is used.
        '''
        if self._previous_surnames is None:
            db = connect.connect()
            cursor = db.cursor()
            cursor.execute(PSN_QUERY, (self.serialno, ))
            self._previous_surnames = [s[0] for s in cursor.fetchall()]
            cursor.close()
        return self._previous_surnames

    @property
    def n_family_members(self):
        '''
        how many members are linked to this patient's familyno
        '''
        if self._n_family_members is None:
            db = connect.connect()
            cursor = db.cursor()
            cursor.execute(FAMILY_COUNT_QUERY, (self.familyno, ))
            self._n_family_members = cursor.fetchone()[0]

        return self._n_family_members

    @property
    def under_capitation(self):
        '''
        under capitation if regular NHS patient and under 18.
        '''
        if self.cset != "N":
            return False
        years, months = self.age_course_start

        return years < 17 or (years == 17 and months < 11)

    def new_tx_course(self, new_courseno):
        '''
        start a new treatment course
        '''
        self.courseno0 = new_courseno
        self.treatment_course = TreatmentCourse(self.serialno, new_courseno)

    @property
    def COPIED_ATTRIBUTES(self):
        '''
        these are what is copied over into pt.dbstate
        '''
        return (patient_query_atts + exemptionTableAtts + bpeTableAtts +
                clinical_memos +
                ("fees", "estimate_charges", "serialno", "estimates",
                 "appt_prefs", "treatment_course", "chartgrid"))

    @property
    def USER_CHANGEABLE_ATTRIBUTES(self):
        '''
        the attributes, common to pt and the object copy pt.db_state
        which is generated during take_snapshot
        used to determine whether the patient has been edited.
        '''
        for att_ in self.COPIED_ATTRIBUTES:
            # if att_ not in ("treatment_course", "estimates", "chartgrid"):
            yield att_

    @property
    def changes(self):
        '''
        what has changed since the patient was loaded
        '''
        changes = []
        for att_ in self.USER_CHANGEABLE_ATTRIBUTES:
            new_value = self.__dict__.get(att_, "")
            db_value = self.dbstate.__dict__.get(att_, "")
            if new_value != db_value:
                message = "Altered pt.%s" % att_.ljust(20)
                if att_ not in ("treatment_course", "estimates"):
                    message += (" ORIG = '%s' NEW = '%s'" %
                                (db_value, new_value))
                LOGGER.debug(message)
                changes.append(att_)
        return changes

    @property
    def has_changes(self):
        is_changed = self.changes != []
        self.lock_record_in_use() if is_changed else self.clear_lock()
        return is_changed

    def take_snapshot(self):
        '''
        create a snapshot of this class, copying all attributes that the
        user can change
        '''
        memo = {}
        cls = self.__class__
        snapshot = cls.__new__(cls)
        memo[id(self)] = snapshot
        for att_, val_ in self.__dict__.items():
            if att_ in self.COPIED_ATTRIBUTES:
                setattr(snapshot, att_, deepcopy(val_, memo))
        self.dbstate = snapshot

        LOGGER.debug("snapshot of %s taken" % self)

    @property
    def course_dentist(self):
        '''
        returns the course dentist for NHS and private courses, but the
        contracted dentist otherwise.
        this is used in the daybook for "work done for lists".
        '''
        if self.cset == "I":
            return self.dnt1
        if self.dnt2 not in (0, None):
            return self.dnt2
        return self.dnt1

    @property
    def has_new_course(self):
        '''
        if the initial state has no course, or a lower course number,
        this is true.
        '''
        if self.treatment_course and self.dbstate.treatment_course is None:
            return True
        return (self.treatment_course.courseno !=
                self.dbstate.treatment_course.courseno)

    @property
    def tx_hash_tups(self):
        '''
        a list of unique hashes of all treatment on the current treatment plan
        returns a tuple (unique hash, attribute, treatment)
        '''
        for hash_, att_, tx in self.treatment_course._get_tx_hashes():
            if re.match("[ul][lr][1-8]", att_):
                att_ = self.chartgrid.get(att_)
            yield hash_, att_, tx

    @property
    def completed_tx_hash_tups(self):
        for hash_, att_, tx in self.treatment_course.completed_tx_hash_tups:
            if re.match("[ul][lr][1-8]", att_):
                att_ = self.chartgrid.get(att_)
            yield hash_, att_, tx

    @property
    def completed_tx_hashes(self):
        return list(self.treatment_course.completed_tx_hashes)

    @property
    def planned_tx_hash_tups(self):
        return self.treatment_course.planned_tx_hash_tups

    @property
    def has_planned_perio_txs(self):
        for hash_, att_, tx in self.planned_tx_hash_tups:
            if att_ == "perio":
                return True
        return False

    def get_tx_from_hash(self, hash_):
        return self.treatment_course.get_tx_from_hash(hash_)

    def ests_from_hash(self, hash_):
        '''
        return all estimate items associated with a unique tx_hash
        '''
        for est in self.estimates:
            for tx_hash in est.tx_hashes:
                if tx_hash == hash_:
                    yield est

    @property
    def address_tuple(self):
        return (self.sname, self.addr1, self.addr2, self.addr3, self.town,
                self.county, self.pcde, self.tel1)

    def reload_mh_form_date(self):
        db = connect.connect()
        cursor = db.cursor()
        cursor.execute(MED_FORM_QUERY + " limit 1", (self.serialno, ))
        try:
            self.mh_form_date = cursor.fetchone()[0]
        except TypeError:
            self.mh_form_date = None
        cursor.close()

    def mh_form_dates(self):
        '''
        the dates that the  mh form has been signed off by the patient.
        '''
        db = connect.connect()
        cursor = db.cursor()
        cursor.execute(MED_FORM_QUERY, (self.serialno, ))
        for row in cursor.fetchall():
            yield row[0]
        cursor.close()

    def set_record_in_use(self):
        records_in_use.set_in_use(self.serialno)

    def lock_record_in_use(self):
        records_in_use.set_locked(self.serialno)

    def clear_lock(self):
        records_in_use.clear_lock(self.serialno)
Exemple #2
0
class patient(object):

    '''
    this class pulls information from the database into a python object.
    '''
    def __init__(self, sno):
        '''
        initiate the class with default variables, then load from database
        '''
        self.serialno = sno
        self.dbstate = None

        self.load_warnings = []

        # patient table atts
        self.courseno0 = None
        self.money0 = 0
        self.money1 = 0
        self.money2 = 0
        self.money3 = 0
        self.money4 = 0
        self.money5 = 0
        self.money6 = 0
        self.money7 = 0
        self.money8 = 0
        self.money9 = 0
        self.money10 = 0
        self.pd0 = None
        self.pd1 = None
        self.pd2 = None
        self.pd3 = None
        self.pd4 = None  # this field is no longer used (last treatment date)
        self.pd5 = None
        self.pd6 = None
        self.pd7 = None
        self.pd8 = None
        self.pd9 = None
        self.pd10 = None
        self.pd11 = None
        self.pd12 = None
        self.pd13 = None
        self.pd14 = None
        self.sname = ''
        self.fname = ''
        self.title = ''
        self.sex = ''
        self.dob = None
        self.addr1 = ''
        self.addr2 = ''
        self.addr3 = ''
        self.pcde = ''
        self.tel1 = ''
        self.tel2 = ''
        self.occup = ''
        self.nhsno = ''
        self.cnfd = None
        self.cset = ''
        self.dnt1 = 0
        self.dnt2 = 0
        self.ur8st = ''
        self.ur7st = ''
        self.ur6st = ''
        self.ur5st = ''
        self.ur4st = ''
        self.ur3st = ''
        self.ur2st = ''
        self.ur1st = ''
        self.ul1st = ''
        self.ul2st = ''
        self.ul3st = ''
        self.ul4st = ''
        self.ul5st = ''
        self.ul6st = ''
        self.ul7st = ''
        self.ul8st = ''
        self.ll8st = ''
        self.ll7st = ''
        self.ll6st = ''
        self.ll5st = ''
        self.ll4st = ''
        self.ll3st = ''
        self.ll2st = ''
        self.ll1st = ''
        self.lr1st = ''
        self.lr2st = ''
        self.lr3st = ''
        self.lr4st = ''
        self.lr5st = ''
        self.lr6st = ''
        self.lr7st = ''
        self.lr8st = ''
        self.dent0 = 0
        self.dent1 = 0
        self.dent2 = 0
        self.dent3 = 0
        self.billdate = None
        self.billct = 0
        self.billtype = None
        self.money11 = 0
        self.familyno = localsettings.last_family_no
        self.memo = ''
        self.town = ''
        self.county = ''
        self.mobile = ''
        self.fax = ''
        self.email1 = ''
        self.email2 = ''
        self.status = ''
        self.initaccept = 0
        self.lastreaccept = None
        self.lastclaim = None
        self.expiry = None
        self.cstatus = None
        self.transfer = 0
        self.pstatus = None

        self.estimates = []

        # from userdata
        self.plandata = PlanData(self.serialno)

        # NEIL'S STUFF####
        self.exemption = ""
        self.exempttext = ""
        self.bpe = []
        self.bpedate = nullDate
        self.chartdate = nullDate
        self.notes_dict = {}
        self.MEDALERT = False
        self.mh_chkdate = None
        self.mh_form_date = None
        self.HIDDENNOTES = []
        self.chartgrid = {}
        self._fee_table = None
        self.synopsis = ""
        self._n_family_members = None
        self._dayBookHistory = None
        self.treatment_course = None
        self.est_logger = None
        self._most_recent_daybook_entry = None
        self._first_note_date = None
        self._has_exam_booked = None
        self._previous_surnames = None
        self.monies_reset = False
        self._n_hyg_visits = None

        if self.serialno == 0:
            return

        #
        # now load stuff from the database ##
        #
        db = connect.connect()
        cursor = db.cursor()

        self.getSynopsis()

        cursor.execute(PATIENT_QUERY, (self.serialno,))
        values = cursor.fetchall()

        if values == ():
            raise localsettings.PatientNotFoundError

        for i, att_ in enumerate(patient_query_atts):
            value = values[0][i]
            if value is not None:
                self.__dict__[att_] = value
            elif att_ == "familyno":
                self.familyno = 0

        query = '''select exemption, exempttext from exemptions
        where serialno=%s'''
        cursor.execute(query, (self.serialno,))

        values = cursor.fetchall()

        for value in values:
            self.exemption, self.exempttext = value

        query = '''select bpedate, bpe from bpe where serialno=%s
        order by bpedate'''
        cursor.execute(query, (self.serialno,))

        values = cursor.fetchall()

        for value in values:
            self.bpe.append(value)

        if self.courseno0 != 0:
            self.getEsts()

        self.treatment_course = TreatmentCourse(
            self.serialno, self.courseno0)

        self.getNotesTuple()

        cursor.execute(QUICK_MED_QUERY, (self.serialno,))
        try:
            self.MEDALERT, self.mh_chkdate = cursor.fetchone()
        except TypeError:
            pass

        cursor.execute(MED_FORM_QUERY + " limit 1", (self.serialno,))
        try:
            self.mh_form_date = cursor.fetchone()[0]
        except TypeError:
            pass
        cursor.close()

        # - load from plandata
        self.plandata.getFromDB()

        self.appt_prefs = ApptPrefs(self.serialno)

        self.updateChartgrid()

        self.take_snapshot()

    @property
    def appt_memo(self):
        return self.appt_prefs.note

    def set_appt_memo(self, memo):
        self.appt_prefs.note = memo

    @property
    def recall_active(self):
        return self.appt_prefs.recall_active

    @property
    def exam_due(self):
        return self.recall_active and self.recd < localsettings.currentDay()

    @property
    def recd(self):
        return self.appt_prefs.recdent

    @property
    def dayBookHistory(self):
        if self._dayBookHistory is None:
            db = connect.connect()
            cursor = db.cursor()
            query = 'select date, trtid, chart from daybook where serialno=%s'
            cursor.execute(query, (self.serialno,))
            self._dayBookHistory = cursor.fetchall()
            cursor.close()
        return self._dayBookHistory

    @property
    def last_treatment_date(self):
        max_date = localsettings.currentDay()
        if self.treatment_course.cmp_txs != \
                self.dbstate.treatment_course.cmp_txs:
            return max_date
        if self._most_recent_daybook_entry is None:
            db = connect.connect()
            cursor = db.cursor()
            query = 'select max(date) from daybook where serialno=%s'
            if cursor.execute(query, (self.serialno,)):
                max_date = cursor.fetchone()[0]
            cursor.close()
            self._most_recent_daybook_entry = max_date
        return self._most_recent_daybook_entry

    @property
    def first_note_date(self):
        '''
        returns teh first date found in the patient notes
        '''
        if self._first_note_date is None:
            min_date = localsettings.currentDay()
            db = connect.connect()
            cursor = db.cursor()
            query = 'select min(ndate) from formatted_notes where serialno=%s'
            if cursor.execute(query, (self.serialno,)):
                min_date = cursor.fetchone()[0]
            cursor.close()
            self._first_note_date = min_date \
                if min_date else localsettings.currentDay()
        return self._first_note_date

    @property
    def n_hyg_visits(self):
        if self._n_hyg_visits is not None:
            pass
        elif not localsettings.hyg_ixs:
            self._n_hyg_visits = 0
        else:
            if len(localsettings.hyg_ixs) == 1:
                conditional = "="
                values = (self.serialno, localsettings.hyg_ixs[0])
            else:
                conditional = "in"
                values = (self.serialno, localsettings.hyg_ixs)
            self._n_hyg_visits = 0
            db = connect.connect()
            cursor = db.cursor()
            query = '''select count(*) from
            (select date from daybook where serialno=%%s and
            trtid %s %%s group by date) as t''' % conditional
            if cursor.execute(query, values):
                self._n_hyg_visits = cursor.fetchone()[0]
            cursor.close()
        return self._n_hyg_visits

    def forget_exam_booked(self):
        self._has_exam_booked = None

    @property
    def has_exam_booked(self):
        if self._has_exam_booked is None:
            db = connect.connect()
            cursor = db.cursor()
            cursor.execute(FUTURE_EXAM_QUERY, (self.serialno,))
            self._has_exam_booked = bool(cursor.fetchone()[0])
            cursor.close()

        return self._has_exam_booked

    def __repr__(self):
        return "'Patient_class instance - serialno %d'" % self.serialno

    @property
    def address(self):
        '''
        a printable address
        '''
        address = ""
        for line in (self.addr1, self.addr2, self.addr3,
                     self.town, self.county, self.pcde):
            if line.strip(" ") != "":
                address += "%s\n" % line.strip(" ")
        return address

    def getAge(self, on_date=None):
        '''
        return the age in form (year(int), months(int), isToday(bool))
        '''
        if on_date is None:
            # use today
            on_date = localsettings.currentDay()

        try:
            nextbirthday = datetime.date(on_date.year, self.dob.month,
                                         self.dob.day)
        except ValueError:
            # catch leap years!!
            nextbirthday = datetime.date(on_date.year, self.dob.month,
                                         self.dob.day - 1)

        ageYears = on_date.year - self.dob.year

        if nextbirthday > on_date:
            ageYears -= 1
            months = (12 - self.dob.month) + on_date.month
        else:
            months = on_date.month - self.dob.month
        if self.dob.day > on_date.day:
            months -= 1

        isToday = nextbirthday == localsettings.currentDay()

        return (ageYears, months, isToday)

    @property
    def ageYears(self):
        return self.getAge()[0]

    @property
    def age_course_start(self):
        '''
        returns a tuple (year, months) for the patient at accd
        '''
        return self.getAge(self.treatment_course.accd)[:2]

    @property
    def under_6(self):
        '''
        returns a bool "is patient under 6?".
        '''
        return self.ageYears < 6

    @property
    def under_18(self):
        '''
        returns a bool "is patient under 18?".
        '''
        return self.ageYears < 18

    def forget_fee_table(self):
        self._fee_table = None

    @property
    def fee_table(self):
        '''
        logic to determine which feeTable should be used for standard items
        '''
        if self._fee_table is None:
            if self.treatment_course.accd is None:
                cse_accd = localsettings.currentDay()
            else:
                cse_accd = self.treatment_course.accd
            for table in reversed(localsettings.FEETABLES.tables.values()):
                LOGGER.debug(
                    "checking feescale %s to see if suitable a feetable",
                    table)

                start, end = table.startDate, table.endDate
                LOGGER.debug("categories, start, end = %s, %s, %s",
                             table.categories, start, end)
                if end is None:
                    end = localsettings.currentDay()

                if self.cset in table.categories and start <= cse_accd <= end:
                    self._fee_table = table

            if self._fee_table is None:
                # - no matching table found, use the default.
                LOGGER.warning("NO SUITABLE FEETABLE FOUND, RETURNING DEFAULT")
                self._fee_table = localsettings.FEETABLES.default_table

        return self._fee_table

    def getEsts(self):
        '''
        get estimate data
        '''
        self.estimates = db_estimates.get_ests(self.serialno, self.courseno0)
        self.est_logger = EstLogger(self.courseno0)

    def getSynopsis(self):
        '''
        the synopsis line is displayed on the clinical summary page
        '''
        db = connect.connect()
        cursor = db.cursor()
        try:
            if cursor.execute(SYNOPSIS_QUERY, (self.serialno,)):
                self.synopsis = cursor.fetchall()[-1][0]
        except connect.OperationalError:
            # - necessary because the column is missing is db schema 1.4
            LOGGER.warning("invalid schema for getSynopsis")

    @property
    def underTreatment(self):
        '''
        a boolean value stating whether the patient has a continuing treatment
        plan
        '''
        return (self.treatment_course is not None and
                self.treatment_course.underTreatment)

    @property
    def max_tx_courseno(self):
        '''
        a patient who has had many courses of treatment, this gets the
        latest
        '''
        return self.treatment_course.max_tx_courseno

    @property
    def newer_course_found(self):
        '''
        check for a newer course in the currtrtmt2 table than the one loaded
        at startup.
        '''
        return self.treatment_course.newer_course_found

    def getNotesTuple(self):
        '''
        connect and poll the formatted_notes table
        '''
        self.notes_dict = formatted_notes.get_notes_dict(self.serialno)

    def flipDec_Perm(self, tooth):
        '''
        switches a deciduous tooth to a permanent one,
        and viceVersa pass a variable like "ur5"
        '''
        quadrant = tooth[:2]
        pos = int(tooth[2]) - 1                 # will be 0-7
        if quadrant == "ul":
            var = self.dent1
            pos = 7 - pos
        elif quadrant == "ur":
            var = self.dent0
        elif quadrant == "ll":
            var = self.dent2
        else:  # lr
            var = self.dent3
            pos = 7 - pos
        existing = dec_perm.fromSignedByte(var)
        if existing[pos] == "1":
            existing = existing[:pos] + "0" + existing[pos + 1:]
        else:
            existing = existing[:pos] + "1" + existing[pos + 1:]
        if quadrant == "ul":
            self.dent1 = dec_perm.toSignedByte(existing)
        elif quadrant == "ur":
            self.dent0 = dec_perm.toSignedByte(existing)
        elif quadrant == "ll":
            self.dent2 = dec_perm.toSignedByte(existing)
        else:  # lr
            self.dent3 = dec_perm.toSignedByte(existing)
        self.updateChartgrid()

    def updateChartgrid(self):
        '''
        a legacy issue with openmolar is the way teeth are saved as present
        is as 4 bytes (32 bits = 32 teeth). very frugal storage, but requires
        a fair deal of client computation :(
        '''
        grid = ""
        for quad in (self.dent1, self.dent0, self.dent3, self.dent2):
            grid += dec_perm.fromSignedByte(quad)
        for pos in mouth:
            if grid[mouth.index(pos)] == "0":
                self.chartgrid[pos] = pos
            else:
                self.chartgrid[pos] = decidmouth[mouth.index(pos)]

    def apply_fees(self):
        '''
        update the money owed.
        '''
        LOGGER.debug("Applying Fees")
        if "N" in self.cset:
            self.money0 = self.dbstate.money0 + self.fees_accrued
        else:
            self.money1 = self.dbstate.money1 + self.fees_accrued

    @property
    def fees(self):
        '''
        calculate what money is due.
        '''
        return int(self.money0 + self.money1 + self.money9 + self.money10 +
                   self.money11 - self.money2 - self.money3 - self.money8)

    @property
    def fees_accrued(self):
        '''
        what fees have changed since load.
        '''
        old_estimate_charges = 0
        if self.courseno0 == self.dbstate.courseno0:
            old_estimate_charges = self.dbstate.estimate_charges

        accrued_fees = self.estimate_charges - old_estimate_charges
        LOGGER.debug("fees_accrued = (new-existing) = %d - %d = %d",
                     self.estimate_charges, old_estimate_charges, accrued_fees)
        return accrued_fees

    @property
    def estimate_charges(self):
        '''
        charges for all completed treatments.
        '''
        charges = 0
        for est in self.estimates:
            if est.completed == 2:
                charges += est.ptfee
            elif est.completed == 1:
                charges += est.interim_pt_fee
        return charges

    @property
    def est_logger_text(self):
        '''
        a summary of the estimate for use in the est_logger_table
        est_logger is unconcerned whether treatment is completed etc..
        '''
        text = ""
        total, p_total = 0, 0
        for estimate in sorted(self.estimates):
            text += estimate.log_text
            total += estimate.fee
            p_total += estimate.ptfee
        text += "TOTAL ||  ||  ||  ||  ||  || %s || %s" % (total, p_total)
        return text

    def resetAllMonies(self):
        '''
        gets money1 and money 0 from apply_fees,
        then equalises money3 and money2 accordingly.
        zero's everything else
        money11 (bad debt) is left unaltered.
        '''
        self.dbstate.money0 = 0
        self.dbstate.money1 = 0
        self.monies_reset = True

        self.money0 = 0
        self.money1 = 0
        self.apply_fees()
        self.money9 = 0
        self.money10 = 0
        self.money2 = self.money0
        self.money3 = self.money1
        self.money8 = 0

    def nhs_claims(self, completed_only=True):
        '''
        nhs items from the estimates.
        if completed_only is False, then include planned items.
        '''
        claims = []
        for est in self.estimates:
            if est.csetype.startswith("N") and \
                    (not completed_only or est.completed == 2):
                claims.append(est)
        return claims

    def addHiddenNote(self, ntype, note="", attempt_delete=False,
                      one_only=False):
        '''
        re-written for schema 1.9
        '''
        LOGGER.info(
            "addHiddenNote - ntype='%s',note='%s', attempt_delete='%s'",
            ntype, note, attempt_delete
        )

        HN = ()
        if ntype == "payment":
            HN = ("RECEIVED: ", note)
        elif ntype == "printed":
            HN = ("PRINTED: ", note)
        elif ntype == "exam":
            HN = ("TC: EXAM", note)
        elif ntype == "chart_treatment":
            HN = ("TC:", note)
        elif ntype == "perio_treatment":
            HN = ("TC: PERIO", note)
        elif ntype == "xray_treatment":
            HN = ("TC: XRAY", note)
        elif ntype == "treatment":
            HN = ("TC: OTHER", note)
        elif ntype == "mednotes":  # other treatment
            HN = ("UPDATED:Medical Notes", note)
        elif ntype == "close_course":
            HN = ("COURSE CLOSED", "=" * 10)
        elif ntype == "open_course":
            HN = ("COURSE OPENED", "= " * 5)
        elif ntype == "resume_course":
            HN = ("COURSE RE-OPENED", "= " * 5)
        elif ntype == "fee":
            HN = ("INTERIM: ", note)

        if not HN:
            LOGGER.warning(
                "unable to add Hidden Note notetype '%s' not found", ntype)
            return

        reversing_note = ("UNCOMPLETED", "{%s}" % note)

        if attempt_delete:
            try:
                self.HIDDENNOTES.remove(HN)
            except ValueError:
                LOGGER.debug("'%s' not in hiddenotes", HN)
                LOGGER.debug(self.HIDDENNOTES)
                self.HIDDENNOTES.append(reversing_note)
        else:
            try:
                self.HIDDENNOTES.remove(reversing_note)
            except ValueError:
                self.HIDDENNOTES.append(HN)

        if one_only:
            while self.HIDDENNOTES.count(HN) > 1:
                self.HIDDENNOTES.remove(HN)

    def clearHiddenNotes(self):
        '''
        reset self.HIDDENNOTES
        '''
        self.HIDDENNOTES = []

    def updateBilling(self, tone):
        '''
        update the last billdate and tone of invoice
        '''
        self.billdate = localsettings.currentDay()
        self.billct += 1
        self.billtype = tone

    def reset_billing(self):
        '''
        if patients account is now is order, reset all billing params
        '''
        if self.fees == 0:
            self.billdate = None
            self.billct = None
            self.billtype = None

    def treatmentOutstanding(self):
        '''
        does the patient have treatmentOutstanding?
        returns a boolean
        '''
        return (self.treatment_course and
                self.treatment_course.has_treatment_outstanding)

    def checkExemption(self):
        '''
        see if the patient's exemption requires removal.
        '''
        if (self.exemption == "S" and
                self.getAge(self.treatment_course.accd)[0] > 19):
            self.exemption = ""
            self.load_warnings.append(_("Student Exemption removed"))
        elif (self.exemption == "A" and
              self.getAge(self.treatment_course.accd)[0] > 18):
            self.exemption = ""
            self.load_warnings.append(_("Age Exemption removed"))
        else:
            return True

    @property
    def name_id(self):
        '''
        name and serialno
        '''
        return u"%s - %s" % (self.name, self.serialno)

    @property
    def name(self):
        '''
        patients name in a readable form
        '''
        return u"%s %s %s" % (self.title, self.fname, self.sname)

    @property
    def psn(self):
        '''
        previous surname
        '''
        try:
            return self.previous_surnames[0]
        except IndexError:
            return ""

    @property
    def previous_surnames(self):
        '''
        previous surnames are stored.
        ## TODO - check this is used.
        '''
        if self._previous_surnames is None:
            db = connect.connect()
            cursor = db.cursor()
            cursor.execute(PSN_QUERY, (self.serialno,))
            self._previous_surnames = [s[0] for s in cursor.fetchall()]
            cursor.close()
        return self._previous_surnames

    @property
    def n_family_members(self):
        '''
        how many members are linked to this patient's familyno
        '''
        if self._n_family_members is None:
            db = connect.connect()
            cursor = db.cursor()
            cursor.execute(FAMILY_COUNT_QUERY, (self.familyno,))
            self._n_family_members = cursor.fetchone()[0]

        return self._n_family_members

    @property
    def under_capitation(self):
        '''
        under capitation if regular NHS patient and under 18.
        '''
        if self.cset != "N":
            return False
        years, months = self.age_course_start

        return years < 17 or (years == 17 and months < 11)

    def new_tx_course(self, new_courseno):
        '''
        start a new treatment course
        '''
        self.courseno0 = new_courseno
        self.treatment_course = TreatmentCourse(self.serialno, new_courseno)

    @property
    def COPIED_ATTRIBUTES(self):
        '''
        these are what is copied over into pt.dbstate
        '''
        return (patient_query_atts + exemptionTableAtts + bpeTableAtts +
                clinical_memos + ("fees", "estimate_charges", "serialno",
                                  "estimates", "appt_prefs",
                                  "treatment_course", "chartgrid"))

    @property
    def USER_CHANGEABLE_ATTRIBUTES(self):
        '''
        the attributes, common to pt and the object copy pt.db_state
        which is generated during take_snapshot
        used to determine whether the patient has been edited.
        '''
        for att_ in self.COPIED_ATTRIBUTES:
            # if att_ not in ("treatment_course", "estimates", "chartgrid"):
            yield att_

    @property
    def changes(self):
        '''
        what has changed since the patient was loaded
        '''
        changes = []
        for att_ in self.USER_CHANGEABLE_ATTRIBUTES:
            new_value = self.__dict__.get(att_, "")
            db_value = self.dbstate.__dict__.get(att_, "")
            if new_value != db_value:
                message = "Altered pt.%s" % att_.ljust(20)
                if att_ not in ("treatment_course", "estimates"):
                    message += (
                        " ORIG = '%s' NEW = '%s'" % (db_value, new_value))
                LOGGER.debug(message)
                changes.append(att_)
        return changes

    @property
    def has_changes(self):
        is_changed = self.changes != []
        self.lock_record_in_use() if is_changed else self.clear_lock()
        return is_changed

    def take_snapshot(self):
        '''
        create a snapshot of this class, copying all attributes that the
        user can change
        '''
        memo = {}
        cls = self.__class__
        snapshot = cls.__new__(cls)
        memo[id(self)] = snapshot
        for att_, val_ in self.__dict__.items():
            if att_ in self.COPIED_ATTRIBUTES:
                setattr(snapshot, att_, deepcopy(val_, memo))
        self.dbstate = snapshot

        LOGGER.debug("snapshot of %s taken" % self)

    @property
    def course_dentist(self):
        '''
        returns the course dentist for NHS and private courses, but the
        contracted dentist otherwise.
        this is used in the daybook for "work done for lists".
        '''
        if self.cset == "I":
            return self.dnt1
        if self.dnt2 not in (0, None):
            return self.dnt2
        return self.dnt1

    @property
    def has_new_course(self):
        '''
        if the initial state has no course, or a lower course number,
        this is true.
        '''
        if self.treatment_course and self.dbstate.treatment_course is None:
            return True
        return (self.treatment_course.courseno !=
                self.dbstate.treatment_course.courseno)

    @property
    def tx_hash_tups(self):
        '''
        a list of unique hashes of all treatment on the current treatment plan
        returns a tuple (unique hash, attribute, treatment)
        '''
        for hash_, att_, tx in self.treatment_course._get_tx_hashes():
            if re.match("[ul][lr][1-8]", att_):
                att_ = self.chartgrid.get(att_)
            yield hash_, att_, tx

    @property
    def completed_tx_hash_tups(self):
        for hash_, att_, tx in self.treatment_course.completed_tx_hash_tups:
            if re.match("[ul][lr][1-8]", att_):
                att_ = self.chartgrid.get(att_)
            yield hash_, att_, tx

    @property
    def completed_tx_hashes(self):
        return list(self.treatment_course.completed_tx_hashes)

    @property
    def planned_tx_hash_tups(self):
        return self.treatment_course.planned_tx_hash_tups

    @property
    def has_planned_perio_txs(self):
        for hash_, att_, tx in self.planned_tx_hash_tups:
            if att_ == "perio":
                return True
        return False

    def get_tx_from_hash(self, hash_):
        return self.treatment_course.get_tx_from_hash(hash_)

    def ests_from_hash(self, hash_):
        '''
        return all estimate items associated with a unique tx_hash
        '''
        for est in self.estimates:
            for tx_hash in est.tx_hashes:
                if tx_hash == hash_:
                    yield est

    @property
    def address_tuple(self):
        return (self.sname, self.addr1, self.addr2,
                self.addr3, self.town, self.county,
                self.pcde, self.tel1)

    def reload_mh_form_date(self):
        db = connect.connect()
        cursor = db.cursor()
        cursor.execute(MED_FORM_QUERY + " limit 1", (self.serialno,))
        try:
            self.mh_form_date = cursor.fetchone()[0]
        except TypeError:
            self.mh_form_date = None
        cursor.close()

    def mh_form_dates(self):
        '''
        the dates that the  mh form has been signed off by the patient.
        '''
        db = connect.connect()
        cursor = db.cursor()
        cursor.execute(MED_FORM_QUERY, (self.serialno,))
        for row in cursor.fetchall():
            yield row[0]
        cursor.close()

    def set_record_in_use(self):
        records_in_use.set_in_use(self.serialno)

    def lock_record_in_use(self):
        records_in_use.set_locked(self.serialno)

    def clear_lock(self):
        records_in_use.clear_lock(self.serialno)
Exemple #3
0
class patient(object):

    def __init__(self, sno):
        '''
        initiate the class with default variables, then load from database
        '''
        self.serialno = sno
        self.dbstate = None

        self.load_warnings = []

        # patient table atts
        self.courseno0 = None
        self.pf0 = 0
        self.pf1 = 0
        self.pf2 = 0
        self.pf3 = 0
        self.pf4 = 0
        self.pf5 = 0
        self.pf6 = 0
        self.pf7 = 0
        self.pf8 = 0
        self.pf9 = 0
        self.pf10 = 0
        self.pf11 = 0
        self.pf12 = 0
        self.pf14 = 0
        self.pf15 = 0
        self.pf16 = 0
        self.pf17 = 0
        self.pf18 = 0
        self.pf19 = 0
        self.money0 = 0
        self.money1 = 0
        self.money2 = 0
        self.money3 = 0
        self.money4 = 0
        self.money5 = 0
        self.money6 = 0
        self.money7 = 0
        self.money8 = 0
        self.money9 = 0
        self.money10 = 0
        self.pd0 = None
        self.pd1 = None
        self.pd2 = None
        self.pd3 = None
        self.pd4 = None
        self.pd5 = None
        self.pd6 = None
        self.pd7 = None
        self.pd8 = None
        self.pd9 = None
        self.pd10 = None
        self.pd11 = None
        self.pd12 = None
        self.pd13 = None
        self.pd14 = None
        self.sname = ''
        self.fname = ''
        self.title = ''
        self.sex = ''
        self.dob = None
        self.addr1 = ''
        self.addr2 = ''
        self.addr3 = ''
        self.pcde = ''
        self.tel1 = ''
        self.tel2 = ''
        self.occup = ''
        self.nhsno = ''
        self.cnfd = None
        self.psn = ''
        self.cset = ''
        self.dnt1 = 0
        self.dnt2 = 0
        self.courseno1 = 0
        self.ur8st = ''
        self.ur7st = ''
        self.ur6st = ''
        self.ur5st = ''
        self.ur4st = ''
        self.ur3st = ''
        self.ur2st = ''
        self.ur1st = ''
        self.ul1st = ''
        self.ul2st = ''
        self.ul3st = ''
        self.ul4st = ''
        self.ul5st = ''
        self.ul6st = ''
        self.ul7st = ''
        self.ul8st = ''
        self.ll8st = ''
        self.ll7st = ''
        self.ll6st = ''
        self.ll5st = ''
        self.ll4st = ''
        self.ll3st = ''
        self.ll2st = ''
        self.ll1st = ''
        self.lr1st = ''
        self.lr2st = ''
        self.lr3st = ''
        self.lr4st = ''
        self.lr5st = ''
        self.lr6st = ''
        self.lr7st = ''
        self.lr8st = ''
        self.dent0 = 0
        self.dent1 = 0
        self.dent2 = 0
        self.dent3 = 0
        self.dmask = "YYYYYYY"
        self.minstart = 0
        self.maxend = 0
        self.billdate = None
        self.billct = 0
        self.billtype = None
        self.pf20 = 0
        self.money11 = 0
        self.pf13 = 0
        self.familyno = localsettings.last_family_no
        self.memo = ''
        self.town = ''
        self.county = ''
        self.mobile = ''
        self.fax = ''
        self.email1 = ''
        self.email2 = ''
        self.status = ''
        self.source = ''
        self.enrolled = ''
        self.archived = None
        self.initaccept = 0
        self.lastreaccept = None
        self.lastclaim = None
        self.expiry = None
        self.cstatus = None
        self.transfer = 0
        self.pstatus = None
        self.courseno2 = 0

        # TABLE 'mnhist'#######
        self.chgdate = nullDate   # date 	YES 	 	None
        self.ix = 0  # tinyint(3) unsigned 	YES 	 	None
        self.note = ''  # varchar(60) 	YES 	 	None

        self.estimates = []

        # from userdata
        self.plandata = PlanData(self.serialno)

        # NEIL'S STUFF####
        self.exemption = ""
        self.exempttext = ""
        self.perioData = {}
        self.bpe = []
        self.bpedate = nullDate
        self.chartdate = nullDate
        self.notes_dict = {}
        self.MH = ()
        self.MEDALERT = False
        self.HIDDENNOTES = []
        self.chartgrid = {}
        self._fee_table = None
        self.synopsis = ""
        self._n_family_members = None
        self._dayBookHistory = None
        self.treatment_course = None
        self.est_logger = None

        if self.serialno == 0:
            return

        #
        # now load stuff from the database ##
        #
        db = connect.connect()
        cursor = db.cursor()

        self.getSynopsis()

        cursor.execute(PATIENT_QUERY, (self.serialno,))
        values = cursor.fetchall()

        if values == ():
            raise localsettings.PatientNotFoundError

        for i, att in enumerate(patientTableAtts):
            value = values[0][i]
            if value is not None:
                self.__dict__[att] = value
            elif att == "familyno":
                self.familyno = 0

        query = '''select exemption, exempttext from exemptions
        where serialno=%s'''
        cursor.execute(query, self.serialno)

        values = cursor.fetchall()

        for value in values:
            self.exemption, self.exempttext = value

        query = '''select bpedate, bpe from bpe where serialno=%s
        order by bpedate'''
        cursor.execute(query, self.serialno)

        values = cursor.fetchall()

        for value in values:
            self.bpe.append(value)

        if self.courseno0 != 0:
            self.getEsts()

        self.treatment_course = TreatmentCourse(
            self.serialno, self.courseno0)

        self.getNotesTuple()

        query = 'select chartdate,chartdata from perio where serialno=%s'
        cursor.execute(query, self.serialno)
        perioData = cursor.fetchall()

        for data in perioData:
            self.perioData[localsettings.formatDate(data[0])] = (
                perio.get_perioData(data[1]))
            #--perioData is
            #--a dictionary (keys=dates) of dictionaries with keys
            #--like "ur8" and containing 7 tuples of data

        query = 'select drnm,adrtel,curmed,oldmed,allerg,heart,lungs,' +\
            'liver,kidney,bleed,anaes,other,alert,chkdate from mednotes' +\
            ' where serialno=%s'
        cursor.execute(query, (self.serialno,))

        self.MH = cursor.fetchone()
        if self.MH is not None:
            self.MEDALERT = self.MH[12]

        cursor.close()
        # db.close()

        #-- load from plandata
        self.plandata.getFromDB()

        self.appt_prefs = ApptPrefs(self.serialno)

        self.updateChartgrid()

        self.take_snapshot()

    @property
    def appt_memo(self):
        return self.appt_prefs.note

    def set_appt_memo(self, memo):
        self.appt_prefs.note = memo

    @property
    def recall_active(self):
        return self.appt_prefs.recall_active

    @property
    def recd(self):
        return self.appt_prefs.recdent

    @property
    def dayBookHistory(self):
        if self._dayBookHistory is None:
            db = connect.connect()
            cursor = db.cursor()
            query = 'select date, trtid, chart from daybook where serialno=%s'
            cursor.execute(query, self.serialno)
            self._dayBookHistory = cursor.fetchall()
            cursor.close()
        return self._dayBookHistory

    def __repr__(self):
        return "'Patient_class instance - serialno %d'" % self.serialno

    @property
    def address(self):
        '''
        a printable address
        '''
        address = ""
        for line in (self.addr1, self.addr2, self.addr3,
                     self.town, self.county, self.pcde):
            if line.strip(" ") != "":
                address += "%s\n" % line.strip(" ")
        return address

    def getAge(self, on_date=None):
        '''
        return the age in form (year(int), months(int), isToday(bool))
        '''
        if on_date is None:
            # use today
            on_date = localsettings.currentDay()

        day = self.dob.day
        try:
            nextbirthday = datetime.date(on_date.year, self.dob.month,
                                         self.dob.day)
        except ValueError:
            # catch leap years!!
            nextbirthday = datetime.date(on_date.year, self.dob.month,
                                         self.dob.day - 1)

        ageYears = on_date.year - self.dob.year

        if nextbirthday > on_date:
            ageYears -= 1
            months = (12 - self.dob.month) + on_date.month
        else:
            months = on_date.month - self.dob.month
        if self.dob.day > on_date.day:
            months -= 1

        isToday = nextbirthday == localsettings.currentDay()

        return (ageYears, months, isToday)

    @property
    def ageYears(self):
        return self.getAge()[0]

    @property
    def age_course_start(self):
        '''
        returns a tuple (year, months) for the patient at accd
        '''
        return self.getAge(self.treatment_course.accd)[:2]

    @property
    def under_6(self):
        '''
        returns a bool "is patient under 6?".
        '''
        return self.ageYears < 6

    @property
    def under_18(self):
        '''
        returns a bool "is patient under 18?".
        '''
        return self.ageYears < 18

    def forget_fee_table(self):
        self._fee_table = None

    @property
    def fee_table(self):
        '''
        logic to determine which feeTable should be used for standard items
        '''
        if self._fee_table is None:
            if self.treatment_course.accd is None:
                cse_accd = localsettings.currentDay()
            else:
                cse_accd = self.treatment_course.accd
            for table in localsettings.FEETABLES.tables.values():
                LOGGER.debug(
                    "checking feescale %s to see if suitable a feetable" % (
                        table))

                start, end = table.startDate, table.endDate
                LOGGER.debug("categories, start, end = %s, %s, %s" % (
                    table.categories, start, end))
                if end is None:
                    end = localsettings.currentDay()

                if self.cset in table.categories and start <= cse_accd <= end:
                    self._fee_table = table

            if self._fee_table is None:
                #-- no matching table found, use the default.
                LOGGER.warning("NO SUITABLE FEETABLE FOUND, RETURNING DEFAULT")
                self._fee_table = localsettings.FEETABLES.default_table

        return self._fee_table

    def getEsts(self):
        '''
        get estimate data
        '''
        db = connect.connect()
        cursor = db.cursor()

        cursor.execute(ESTS_QUERY, (self.serialno, self.courseno0))

        rows = cursor.fetchall()
        self.estimates = []

        for row in rows:
            hash_ = row[10]
            completed = bool(row[9])

            tx_hash = estimates.TXHash(hash_, completed)

            ix = row[0]

            found = False
            # use existing est if one relates to multiple treatments
            for existing_est in self.estimates:
                if existing_est.ix == ix:
                    existing_est.tx_hashes.append(tx_hash)
                    found = True
                    break
            if found:
                continue

            # initiate a custom data class
            est = estimates.Estimate()

            est.ix = ix
            est.courseno = row[11]
            est.number = row[1]
            est.itemcode = row[2]
            est.description = row[3]
            est.fee = row[4]
            est.ptfee = row[5]
            est.feescale = row[6]
            est.csetype = row[7]
            est.dent = row[8]

            est.tx_hashes = [tx_hash]
            self.estimates.append(est)

        cursor.close()

        self.est_logger = EstLogger(self.courseno0)

    def getSynopsis(self):
        db = connect.connect()
        cursor = db.cursor()
        fields = clinical_memos
        query = ""
        for field in fields:
            query += field + ","
        query = query.strip(",")
        try:
            if cursor.execute(
                'SELECT %s from clinical_memos where serialno=%d' % (query,
                                                                     self.serialno)):
                self.synopsis = cursor.fetchall()[-1][0]
        except connect.OperationalError as e:
            'necessary because the column is missing is db schema 1.4'
            print "WARNING -", e

    @property
    def underTreatment(self):
        return (self.treatment_course is not None and
                self.treatment_course.underTreatment)

    @property
    def max_tx_courseno(self):
        return self.treatment_course.max_tx_courseno

    @property
    def newer_course_found(self):
        return self.treatment_course.newer_course_found

    def getNotesTuple(self):
        '''
        connect and poll the formatted_notes table
        '''
        self.notes_dict = formatted_notes.get_notes_dict(self.serialno)

    def flipDec_Perm(self, tooth):
        '''
        switches a deciduous tooth to a permanent one,
        and viceVersa pass a variable like "ur5"
        '''
        quadrant = tooth[:2]
        pos = int(tooth[2]) - 1                 # will be 0-7
        if quadrant == "ul":
            var = self.dent1
            pos = 7 - pos
        elif quadrant == "ur":
            var = self.dent0
        elif quadrant == "ll":
            var = self.dent2
        else:  # lr
            var = self.dent3
            pos = 7 - pos
        existing = dec_perm.fromSignedByte(var)
        if existing[pos] == "1":
            existing = existing[:pos] + "0" + existing[pos + 1:]
        else:
            existing = existing[:pos] + "1" + existing[pos + 1:]
        if quadrant == "ul":
            self.dent1 = dec_perm.toSignedByte(existing)
        elif quadrant == "ur":
            self.dent0 = dec_perm.toSignedByte(existing)
        elif quadrant == "ll":
            self.dent2 = dec_perm.toSignedByte(existing)
        else:  # lr
            self.dent3 = dec_perm.toSignedByte(existing)
        self.updateChartgrid()

    def updateChartgrid(self):
        grid = ""
        for quad in (self.dent1, self.dent0, self.dent3, self.dent2):
            grid += dec_perm.fromSignedByte(quad)
        for pos in mouth:
            if grid[mouth.index(pos)] == "0":
                self.chartgrid[pos] = pos
            else:
                self.chartgrid[pos] = decidmouth[mouth.index(pos)]

    def apply_fees(self):
        if "N" in self.cset:
            self.money0 = self.dbstate.money0 + self.fees_accrued
        else:
            self.money1 = self.dbstate.money1 + self.fees_accrued

    @property
    def fees(self):
        return int(self.money0 + self.money1 + self.money9 + self.money10 +
                   self.money11 - self.money2 - self.money3 - self.money8)

    @property
    def fees_accrued(self):
        old_estimate_charges = 0
        if self.courseno0 == self.dbstate.courseno0:
            old_estimate_charges = self.dbstate.estimate_charges

        accrued_fees = self.estimate_charges - old_estimate_charges
        LOGGER.debug("fees_accrued = (new-existing) = %d - %d = %d" % (
            self.estimate_charges,
            old_estimate_charges,
            accrued_fees)
        )
        return accrued_fees

    @property
    def estimate_charges(self):
        charges = 0
        for est in self.estimates:
            if est.completed == 2:
                charges += est.ptfee
            elif est.completed == 1:
                charges += est.interim_pt_fee
        return charges

    @property
    def est_logger_text(self):
        '''
        a summary of the estimate for use in the est_logger_table
        est_logger is unconcerned whether treatment is completed etc..
        '''
        text = ""
        total, p_total = 0, 0
        for estimate in sorted(self.estimates):
            text += estimate.log_text
            total += estimate.fee
            p_total += estimate.ptfee
        text += "TOTAL ||  ||  ||  ||  ||  || %s || %s" % (total, p_total)
        return text

    def resetAllMonies(self):
        '''
        zero's everything except money11 (bad debt)
        '''
        self.money0 = 0
        self.money1 = 0
        self.money9 = 0
        self.money10 = 0
        self.money2 = 0
        self.money3 = 0
        self.money8 = 0

        self.dbstate.money0 = 0
        self.dbstate.money1 = 0

    def nhs_claims(self, completed_only=True):
        claims = []
        for est in self.estimates:
            if (est.csetype.startswith("N") and
               (not completed_only or est.completed == 2)
                ):
                # yield est
                claims.append(est)
        return claims

    def addHiddenNote(self, ntype, note="", attempt_delete=False):
        '''
        re-written for schema 1.9
        '''
        LOGGER.info(
            "patient.addHiddenNote(ntype='%s',note='%s', attempt_delete='%s'" % (
                ntype, note, attempt_delete))

        HN = ()
        if ntype == "payment":
            HN = ("RECEIVED: ", note)
        elif ntype == "printed":
            HN = ("PRINTED: ", note)
        elif ntype == "exam":
            HN = ("TC: EXAM", note)
        elif ntype == "chart_treatment":
            HN = ("TC:", note)
        elif ntype == "perio_treatment":
            HN = ("TC: PERIO", note)
        elif ntype == "xray_treatment":
            HN = ("TC: XRAY", note)
        elif ntype == "treatment":
            HN = ("TC: OTHER", note)
        elif ntype == "mednotes":  # other treatment
            HN = ("UPDATED:Medical Notes", note)
        elif ntype == "close_course":
            HN = ("COURSE CLOSED", "=" * 10)
        elif ntype == "open_course":
            HN = ("COURSE OPENED", "= " * 5)
        elif ntype == "resume_course":
            HN = ("COURSE RE-OPENED", "= " * 5)
        elif ntype == "fee":
            HN = ("INTERIM: ", note)

        if not HN:
            print "unable to add Hidden Note notetype '%s' not found" % ntype
            return

        reversing_note = ("UNCOMPLETED", "{%s}" % note)

        if attempt_delete:
            try:
                self.HIDDENNOTES.remove(HN)
            except ValueError:
                self.HIDDENNOTES.append(reversing_note)
        else:
            try:
                self.HIDDENNOTES.remove(reversing_note)
            except ValueError:
                self.HIDDENNOTES.append(HN)

    def clearHiddenNotes(self):
        self.HIDDENNOTES = []

    def updateBilling(self, tone):
        self.billdate = localsettings.currentDay()
        self.billct += 1
        self.billtype = tone

    def treatmentOutstanding(self):
        return (self.treatment_course and
        self.treatment_course.has_treatment_outstanding)

    def checkExemption(self):
        if (self.exemption == "S" and
        self.getAge(self.treatment_course.accd)[0] > 19):
            self.exemption = ""
            self.load_warnings.append(_("Student Exemption removed"))
        else:
            return True

    @property
    def name_id(self):
        return u"%s - %s" % (
            self.name, self.serialno)

    @property
    def name(self):
        return u"%s %s %s" % (
            self.title, self.fname, self.sname)

    @property
    def n_family_members(self):
        if self._n_family_members is None:
            db = connect.connect()
            cursor = db.cursor()
            cursor.execute("select count(*) from patients where familyno=%s",
                (self.familyno,))
            self._n_family_members = cursor.fetchone()[0]

        return self._n_family_members

    @property
    def under_capitation(self):
        if self.cset != "N":
            return False
        years, months = self.age_course_start

        return years < 17 or (years == 17 and months < 11)

    def new_tx_course(self, new_courseno):
        self.courseno0 = new_courseno
        self.treatment_course = TreatmentCourse(self.serialno, new_courseno)

    @property
    def COPIED_ATTRIBUTES(self):
        '''
        these are what is copied over into pt.dbstate
        '''
        return (patientTableAtts +
            exemptionTableAtts + bpeTableAtts + mnhistTableAtts +
            perioTableAtts + clinical_memos + (
                "fees", "estimate_charges", "serialno", "estimates",
            "appt_prefs", "treatment_course", "chartgrid"))

    @property
    def USER_CHANGEABLE_ATTRIBUTES(self):
        '''
        the attributes, common to pt and the object copy pt.db_state
        which is generated during take_snapshot
        used to determine whether the patient has been edited.
        '''
        for att in self.COPIED_ATTRIBUTES:
            # if att not in ("treatment_course", "estimates", "chartgrid"):
            yield att

    @property
    def changes(self):
        changes = []
        for att in self.USER_CHANGEABLE_ATTRIBUTES:
            new_value = self.__dict__.get(att, "")
            db_value = self.dbstate.__dict__.get(att, "")
            if new_value != db_value:
                message = "Altered pt.%s" % att.ljust(20)
                if att not in ("treatment_course", "estimates"):
                    message += (
                        " ORIG = '%s' NEW = '%s'" % (db_value, new_value))
                LOGGER.debug(message)
                changes.append(att)
        return changes

    def take_snapshot(self):
        # create a snapshot of this class, copying all attributes that the
        # user can change
        memo = {}
        cls = self.__class__
        snapshot = cls.__new__(cls)
        memo[id(self)] = snapshot
        for att, v in self.__dict__.items():
            if att in self.COPIED_ATTRIBUTES:
                setattr(snapshot, att, deepcopy(v, memo))
        self.dbstate = snapshot

        LOGGER.debug("snapshot of %s taken" % self)

    @property
    def course_dentist(self):
        '''
        returns the course dentist for NHS and private courses, but the
        contracted dentist otherwise.
        this is used in the daybook for "work done for lists".
        '''
        if self.cset == "I":
            return self.dnt1
        if self.dnt2 not in (0, None):
            return self.dnt2
        return self.dnt1

    @property
    def has_new_course(self):
        if self.treatment_course and self.dbstate.treatment_course is None:
            return True
        return (self.treatment_course.courseno !=
               self.dbstate.treatment_course.courseno)

    @property
    def tx_hash_tups(self):
        '''
        a list of unique hashes of all treatment on the current treatment plan
        returns a tuple (unique hash, attribute, treatment)
        '''
        for hash_, att, tx in self.treatment_course._get_tx_hashes():
            if re.match("[ul][lr][1-8]", att):
                att = self.chartgrid.get(att)
            yield hash_, att, tx

    @property
    def completed_tx_hash_tups(self):
        for hash_, att, tx in self.treatment_course.completed_tx_hash_tups:
            if re.match("[ul][lr][1-8]", att):
                att = self.chartgrid.get(att)
            yield hash_, att, tx

    @property
    def completed_tx_hashes(self):
        return list(self.treatment_course.completed_tx_hashes)

    @property
    def planned_tx_hash_tups(self):
        return self.treatment_course.planned_tx_hash_tups

    @property
    def has_planned_perio_txs(self):
        for hash_, att, tx in self.planned_tx_hash_tups:
            if att == "perio":
                return True
        return False

    def get_tx_from_hash(self, hash_):
        return self.treatment_course.get_tx_from_hash(hash_)

    def ests_from_hash(self, hash_):
        '''
        return all estimate items associated with a unique tx_hash
        '''
        for est in self.estimates:
            for tx_hash in est.tx_hashes:
                if tx_hash == hash_:
                    yield est