示例#1
0
def donation_tags(dbo, p):
    """
    Generates a list of tags from a donation result.
    """
    l = dbo.locale
    tags = { 
        "DONATIONID"            : str(p["ID"]),
        "RECEIPTNUM"            : utils.padleft(p["ID"], 8),
        "DONATIONTYPE"          : p["DONATIONNAME"],
        "DONATIONDATE"          : python2display(l, p["DATE"]),
        "DONATIONDATEDUE"       : python2display(l, p["DATEDUE"]),
        "DONATIONAMOUNT"        : format_currency_no_symbol(l, p["DONATION"]),
        "DONATIONCOMMENTS"      : p["COMMENTS"],
        "DONATIONGIFTAID"       : p["ISGIFTAIDNAME"],
        "DONATIONCREATEDBY"     : p["CREATEDBY"],
        "DONATIONCREATEDBYNAME" : p["CREATEDBY"],
        "DONATIONCREATEDDATE"   : python2display(l, p["CREATEDDATE"]),
        "DONATIONLASTCHANGEDBY" : p["LASTCHANGEDBY"],
        "DONATIONLASTCHANGEDBYNAME" : p["LASTCHANGEDBY"],
        "DONATIONLASTCHANGEDDATE" : python2display(l, p["LASTCHANGEDDATE"])
    }
    return tags
示例#2
0
文件: utils.py 项目: tgage/asm3
def csv(l, rows, cols=None, includeheader=True):
    """
    Creates a CSV file from a set of resultset rows. If cols has been 
    supplied as a list of strings, fields will be output in that
    order.
    """
    if rows is None or len(rows) == 0: return ""
    strio = StringIO()
    out = UnicodeCSVWriter(strio)
    if cols is None:
        cols = []
        for k, v in rows[0].iteritems():
            cols.append(k)
        cols = sorted(cols)
    if includeheader:
        out.writerow(cols)
    for r in rows:
        rd = []
        for c in cols:
            if is_currency(c):
                rd.append(decode_html(format_currency_no_symbol(l, r[c])))
            elif is_date(r[c]):
                timeportion = "00:00:00"
                dateportion = ""
                try:
                    dateportion = python2display(l, r[c])
                    timeportion = format_time(r[c])
                except:
                    pass  # Don't stop the show for bad dates/times
                if timeportion != "00:00:00":  # include time if non-midnight
                    dateportion = "%s %s" % (dateportion, timeportion)
                rd.append(decode_html(dateportion))
            else:
                rd.append(decode_html(r[c]))
        out.writerow(rd)
    return strio.getvalue()
示例#3
0
文件: petrescue.py 项目: rutaq/asm3
    def run(self):
        
        self.log("PetRescuePublisher starting...")

        if self.isPublisherExecuting(): return
        self.updatePublisherProgress(0)
        self.setLastError("")
        self.setStartPublishing()

        token = configuration.petrescue_token(self.dbo)
        all_desexed = configuration.petrescue_all_desexed(self.dbo)
        interstate = configuration.petrescue_interstate(self.dbo)
        postcode = configuration.organisation_postcode(self.dbo)
        suburb = configuration.organisation_town(self.dbo)
        state = configuration.organisation_county(self.dbo)
        contact_name = configuration.organisation(self.dbo)
        contact_email = configuration.petrescue_email(self.dbo)
        if contact_email == "": contact_email = configuration.email(self.dbo)
        contact_number = configuration.organisation_telephone(self.dbo)

        if token == "":
            self.setLastError("No PetRescue auth token has been set.")
            return

        if postcode == "" or contact_email == "":
            self.setLastError("You need to set your organisation postcode and contact email under Settings->Options->Shelter Details->Email")
            return

        animals = self.getMatchingAnimals(includeAdditionalFields=True)
        processed = []

        if len(animals) == 0:
            self.setLastError("No animals found to publish.")
            self.cleanup()
            return

        headers = { "Authorization": "Token token=%s" % token, "Accept": "*/*" }

        anCount = 0
        for an in animals:
            try:
                anCount += 1
                self.log("Processing: %s: %s (%d of %d)" % ( an["SHELTERCODE"], an["ANIMALNAME"], anCount, len(animals)))
                self.updatePublisherProgress(self.getProgress(anCount, len(animals)))

                # If the user cancelled, stop now
                if self.shouldStopPublishing(): 
                    self.log("User cancelled publish. Stopping.")
                    self.resetPublisherProgress()
                    self.cleanup()
                    return
       
                isdog = an.SPECIESID == 1
                iscat = an.SPECIESID == 2

                ageinyears = i18n.date_diff_days(an.DATEOFBIRTH, i18n.now())
               
                size = ""
                if an.SIZE == 2: size = "medium"
                elif an.SIZE < 2: size = "large"
                else: size = "small"

                coat = ""
                if an.COATTYPE == 0: coat = "short"
                elif an.COATTYPE == 1: coat = "long"
                else: coat = "medium_coat"

                origin = ""
                if an.ISTRANSFER == 1 and str(an.BROUGHTINBYOWNERNAME).lower().find("pound") == -1: origin = "shelter_transfer"
                elif an.ISTRANSFER == 1 and str(an.BROUGHTINBYOWNERNAME).lower().find("pound") != -1: origin = "pound_transfer"
                elif an.ORIGINALOWNERID > 0: origin = "owner_surrender"
                else: origin = "community_cat"

                best_feature = "Looking for love"
                if "BESTFEATURE" in an and an.BESTFEATURE != "":
                    best_feature = an.BESTFEATURE

                breeder_id = ""
                if "BREEDERID" in an and an.BREEDERID != "":
                    breeder_id = an.BREEDERID

                needs_constant_care = False
                if "NEEDSCONSTANTCARE" in an and an.NEEDSCONSTANTCARE != "" and an.NEEDSCONSTANTCARE != "0":
                    needs_constant_care = True

                # Check whether we've been vaccinated, wormed and hw treated
                vaccinated = medical.get_vaccinated(self.dbo, an.ID)
                sixmonths = self.dbo.today(offset=-182)
                hwtreated = isdog and self.dbo.query_int("SELECT COUNT(*) FROM animalmedical WHERE LOWER(TreatmentName) LIKE ? " \
                    "AND LOWER(TreatmentName) LIKE ? AND StartDate>? AND AnimalID=?", ("%heart%", "%worm%", sixmonths, an.ID)) > 0
                wormed = (isdog or iscat) and self.dbo.query_int("SELECT COUNT(*) FROM animalmedical WHERE LOWER(TreatmentName) LIKE ? " \
                    "AND LOWER(TreatmentName) NOT LIKE ? AND StartDate>? AND AnimalID=?", ("%worm%", "%heart%", sixmonths, an.ID)) > 0
                # PR want a null value to hide never-treated animals, so we
                # turn False into a null.
                if not hwtreated: hwtreated = None
                if not wormed: wormed = None

                # Use the fosterer's postcode, state and suburb if available
                location_postcode = postcode
                location_state_abbr = state
                location_suburb = suburb
                if an.ACTIVEMOVEMENTID and an.ACTIVEMOVEMENTTYPE == 2:
                    fr = self.dbo.first_row(self.dbo.query("SELECT OwnerTown, OwnerCounty, OwnerPostcode FROM adoption m " \
                        "INNER JOIN owner o ON m.OwnerID = o.ID WHERE m.ID=?", [ an.ACTIVEMOVEMENTID ]))
                    if fr is not None and fr.OWNERPOSTCODE: location_postcode = fr.OWNERPOSTCODE
                    if fr is not None and fr.OWNERCOUNTY: location_state_abbr = fr.OWNERCOUNTY
                    if fr is not None and fr.OWNERTOWN: location_suburb = fr.OWNERTOWN

                # Build a list of immutable photo URLs
                photo_urls = []
                photos = self.dbo.query("SELECT MediaName FROM media " \
                    "WHERE LinkTypeID = 0 AND LinkID = ? AND MediaMimeType = 'image/jpeg' " \
                    "AND (ExcludeFromPublish = 0 OR ExcludeFromPublish Is Null) " \
                    "ORDER BY WebsitePhoto DESC, ID", [an.ID])
                for m in photos:
                    photo_urls.append("%s?account=%s&method=dbfs_image&title=%s" % (SERVICE_URL, self.dbo.database, m.MEDIANAME))

                # Only send microchip_number for locations with a Victoria postcode 3xxx
                microchip_number = ""
                if location_postcode.startswith("3"):
                    microchip_number = utils.iif(an.IDENTICHIPPED == 1, an.IDENTICHIPNUMBER, "")

                # Construct a dictionary of info for this animal
                data = {
                    "remote_id":                str(an.ID), # animal identifier in ASM
                    "remote_source":            "SM%s" % self.dbo.database, # system/database identifier
                    "name":                     an.ANIMALNAME.title(), # animal name (title case, they validate against caps)
                    "shelter_code":             an.SHELTERCODE,
                    "adoption_fee":             i18n.format_currency_no_symbol(self.locale, an.FEE),
                    "species_name":             an.SPECIESNAME,
                    "breed_names":              self.get_breed_names(an), # [breed1,breed2] or [breed1]
                    "breeder_id":               breeder_id, # mandatory for QLD dogs born after 2017-05-26
                    "mix":                      an.CROSSBREED == 1, # true | false
                    "date_of_birth":            i18n.format_date("%Y-%m-%d", an.DATEOFBIRTH), # iso
                    "gender":                   an.SEXNAME.lower(), # male | female
                    "personality":              self.replace_html_entities(self.getDescription(an)), # 20-4000 chars of free type
                    "best_feature":             best_feature, # 25 chars free type, defaults to "Looking for love" requires BESTFEATURE additional field
                    "location_postcode":        location_postcode, # shelter/fosterer postcode
                    "location_state_abbr":      location_state_abbr, # shelter/fosterer state
                    "location_suburb":          location_suburb, # shelter/fosterer suburb
                    "microchip_number":         microchip_number, 
                    "desexed":                  an.NEUTERED == 1 or all_desexed, # true | false, validates to always true according to docs
                    "contact_method":           "email", # email | phone
                    "size":                     utils.iif(isdog, size, ""), # dogs only - small | medium | high
                    "senior":                   isdog and ageinyears > (7 * 365), # dogs only, true | false
                    "vaccinated":               vaccinated, # cats, dogs, rabbits, true | false
                    "wormed":                   wormed, # cats & dogs, true | false
                    "heart_worm_treated":       hwtreated, # dogs only, true | false
                    "coat":                     coat, # Only applies to cats and guinea pigs, but we send for everything: short | medium_coat | long
                    "intake_origin":            utils.iif(iscat, origin, ""), # cats only, community_cat | owner_surrender | pound_transfer | shelter_transfer
                    "incompatible_with_cats":   an.ISGOODWITHCATS == 1,
                    "incompatible_with_dogs":   an.ISGOODWITHDOGS == 1,
                    "incompatible_with_kids_under_5": an.ISGOODWITHCHILDREN == 1,
                    "incompatible_with_kids_6_to_12": an.ISGOODWITHCHILDREN == 1,
                    "needs_constant_care":      needs_constant_care,
                    "adoption_process":         "", # 4,000 chars how to adopt
                    "contact_details_source":   "self", # self | user | group
                    "contact_preferred_method": "email", # email | phone
                    "contact_name":             contact_name, # name of contact details owner
                    "contact_number":           contact_number, # number to enquire about adoption
                    "contact_email":            contact_email, # email to enquire about adoption
                    "foster_needed":            False, # true | false
                    "interstate":               interstate, # true | false - can the animal be flown to another state for adoption
                    "medical_notes":            "", # DISABLED an.HEALTHPROBLEMS, # 4,000 characters medical notes
                    "multiple_animals":         an.BONDEDANIMALID > 0 or an.BONDEDANIMAL2ID > 0, # More than one animal included in listing true | false
                    "photo_urls":               photo_urls, # List of photo URL strings
                    "status":                   "active" # active | removed | on_hold | rehomed | suspended | group_suspended
                }

                # PetRescue will insert/update accordingly based on whether remote_id/remote_source exists
                url = PETRESCUE_URL + "listings"
                jsondata = utils.json(data)
                self.log("Sending POST to %s to create/update listing: %s" % (url, jsondata))
                r = utils.post_json(url, jsondata, headers=headers)

                if r["status"] != 200:
                    self.logError("HTTP %d, headers: %s, response: %s" % (r["status"], r["headers"], self.utf8_to_ascii(r["response"])))
                else:
                    self.log("HTTP %d, headers: %s, response: %s" % (r["status"], r["headers"], self.utf8_to_ascii(r["response"])))
                    self.logSuccess("Processed: %s: %s (%d of %d)" % ( an["SHELTERCODE"], an["ANIMALNAME"], anCount, len(animals)))
                    processed.append(an)

            except Exception as err:
                self.logError("Failed processing animal: %s, %s" % (str(an["SHELTERCODE"]), err), sys.exc_info())

        try:
            # Get a list of all animals that we sent to PR recently (14 days)
            prevsent = self.dbo.query("SELECT AnimalID FROM animalpublished WHERE SentDate>=? AND PublishedTo='petrescue'", [self.dbo.today(offset=-14)])
            
            # Build a list of IDs we just sent, along with a list of ids for animals
            # that we previously sent and are not in the current sent list.
            # This identifies the listings we need to cancel
            animalids_just_sent = set([ x.ID for x in animals ])
            animalids_to_cancel = set([ str(x.ANIMALID) for x in prevsent if x.ANIMALID not in animalids_just_sent])

            # Get the animal records for the ones we need to cancel
            if len(animalids_to_cancel) == 0:
                animals = []
            else:
                animals = self.dbo.query("SELECT ID, ShelterCode, AnimalName, ActiveMovementDate, ActiveMovementType, DeceasedDate " \
                    "FROM animal a WHERE ID IN (%s)" % ",".join(animalids_to_cancel))

        except Exception as err:
            self.logError("Failed finding listings to cancel: %s" % err, sys.exc_info())

        # Cancel the inactive listings
        for an in animals:
            try:
                status = "on_hold"
                if an.ACTIVEMOVEMENTDATE is not None and an.ACTIVEMOVEMENTTYPE == 1: status = "rehomed"
                if an.DECEASEDDATE is not None: status = "removed"
                data = { "status": status }
                jsondata = utils.json(data)
                url = PETRESCUE_URL + "listings/%s/SM%s" % (an.ID, self.dbo.database)

                self.log("Sending PATCH to %s to update existing listing: %s" % (url, jsondata))
                r = utils.patch_json(url, jsondata, headers=headers)

                if r["status"] == 200:
                    self.log("HTTP %d, headers: %s, response: %s" % (r["status"], r["headers"], self.utf8_to_ascii(r["response"])))
                    self.logSuccess("%s - %s: Marked with new status %s" % (an.SHELTERCODE, an.ANIMALNAME, status))
                    # It used to be that we updated animalpublished for this animal to get sentdate to today
                    # we don't do this now so that we'll update dead listings every day for however many days we
                    # look back, but that's it
                else:
                    self.logError("HTTP %d, headers: %s, response: %s" % (r["status"], r["headers"], self.utf8_to_ascii(r["response"])))

            except Exception as err:
                self.logError("Failed closing listing for %s - %s: %s" % (an.SHELTERCODE, an.ANIMALNAME, err), sys.exc_info())

        # Mark sent animals published
        self.markAnimalsPublished(processed, first=True)

        self.cleanup()
示例#4
0
    def run(self):

        self.log("PetRescuePublisher starting...")

        if self.isPublisherExecuting(): return
        self.updatePublisherProgress(0)
        self.setLastError("")
        self.setStartPublishing()

        token = configuration.petrescue_token(self.dbo)
        postcode = configuration.organisation_postcode(self.dbo)
        contact_name = configuration.organisation(self.dbo)
        contact_email = configuration.email(self.dbo)
        contact_number = configuration.organisation_telephone(self.dbo)

        if token == "":
            self.setLastError("No PetRescue auth token has been set.")
            return

        if postcode == "" or contact_email == "":
            self.setLastError(
                "You need to set your organisation postcode and contact email under Settings->Options->Shelter Details->Email"
            )
            return

        animals = self.getMatchingAnimals()
        processed = []

        if len(animals) == 0:
            self.setLastError("No animals found to publish.")
            self.cleanup()
            return

        headers = {"Authorization": "Token token=%s" % token, "Accept": "*/*"}

        anCount = 0
        for an in animals:
            try:
                anCount += 1
                self.log("Processing: %s: %s (%d of %d)" %
                         (an["SHELTERCODE"], an["ANIMALNAME"], anCount,
                          len(animals)))
                self.updatePublisherProgress(
                    self.getProgress(anCount, len(animals)))

                # If the user cancelled, stop now
                if self.shouldStopPublishing():
                    self.log("User cancelled publish. Stopping.")
                    self.resetPublisherProgress()
                    self.cleanup()
                    return

                isdog = an.SPECIESID == 1
                iscat = an.SPECIESID == 2

                ageinyears = i18n.date_diff_days(an.DATEOFBIRTH, i18n.now())

                vaccinated = medical.get_vaccinated(self.dbo, an.ID)

                size = ""
                if an.SIZE == 2: size = "medium"
                elif an.SIZE < 2: size = "large"
                else: size = "small"

                coat = ""
                if an.COATTYPE == 0: coat = "short"
                elif an.COATTYPE == 1: coat = "long"
                else: coat = "medium_coat"

                origin = ""
                if an.ISTRANSFER == 1 and an.BROUGHTINBYOWNERNAME.lower().find(
                        "pound") == -1:
                    origin = "shelter_transfer"
                elif an.ISTRANSFER == 1 and an.BROUGHTINBYOWNERNAME.lower(
                ).find("pound") != -1:
                    origin = "pound_transfer"
                elif an.ORIGINALOWNERID > 0:
                    origin = "owner_surrender"
                else:
                    origin = "community_cat"

                photo_url = "%s?account=%s&method=animal_image&animalid=%d" % (
                    SERVICE_URL, self.dbo.database, an.ID)

                # Construct a dictionary of info for this animal
                data = {
                    "remote_id":
                    str(an.ID),  # animal identifier in ASM
                    "remote_source":
                    "SM%s" % self.dbo.database,  # system/database identifier
                    "name":
                    an.ANIMALNAME,  # animal name
                    "adoption_fee":
                    i18n.format_currency_no_symbol(self.locale, an.FEE),
                    "species_name":
                    an.SPECIESNAME,
                    "breed_names":
                    self.get_breed_names(an),  # breed1,breed2 or breed1
                    "mix":
                    an.CROSSBREED == 1,  # true | false
                    "date_of_birth":
                    i18n.format_date("%Y-%m-%d", an.DATEOFBIRTH),  # iso
                    "gender":
                    an.SEXNAME.lower(),  # male | female
                    "personality":
                    an.WEBSITEMEDIANOTES,  # 20-4000 chars of free type
                    "location_postcode":
                    postcode,  # shelter postcode
                    "postcode":
                    postcode,  # shelter postcode
                    "microchip_number":
                    utils.iif(an.IDENTICHIPPED == 1, an.IDENTICHIPNUMBER, ""),
                    "desexed":
                    an.NEUTERED ==
                    1,  # true | false, validates to always true according to docs
                    "contact_method":
                    "email",  # email | phone
                    "size":
                    utils.iif(isdog, size,
                              ""),  # dogs only - small | medium | high
                    "senior":
                    isdog and ageinyears > 7,  # dogs only, true | false
                    "vaccinated":
                    vaccinated,  # cats, dogs, rabbits, true | false
                    "wormed":
                    vaccinated,  # cats & dogs, true | false
                    "heart_worm_treated":
                    vaccinated,  # dogs only, true | false
                    "coat":
                    utils.iif(iscat, coat,
                              ""),  # cats only, short | medium_coat | long
                    "intake_origin":
                    utils.iif(
                        iscat, origin, ""
                    ),  # cats only, community_cat | owner_surrender | pound_transfer | shelter_transfer
                    "adoption_process":
                    "",  # 4,000 chars how to adopt
                    "contact_details_source":
                    "self",  # self | user | group
                    "contact_preferred_method":
                    "email",  # email | phone
                    "contact_name":
                    contact_name,  # name of contact details owner
                    "contact_number":
                    contact_number,  # number to enquire about adoption
                    "contact_email":
                    contact_email,  # email to enquire about adoption
                    "foster_needed":
                    False,  # true | false
                    "interstate":
                    True,  # true | false - can the animal be adopted to another state
                    "medical_notes":
                    an.HEALTHPROBLEMS,  # 4,000 characters medical notes
                    "multiple_animals":
                    False,  # More than one animal included in listing true | false
                    "photo_urls": [photo_url],  # List of photo URL strings
                    "status":
                    "active"  # active | removed | on_hold | rehomed | suspended | group_suspended
                }

                # PetRescue will insert/update accordingly based on whether remote_id/remote_source exists
                url = PETRESCUE_URL + "listings"
                jsondata = utils.json(data)
                self.log("Sending POST to %s to create/update listing: %s" %
                         (url, jsondata))
                r = utils.post_json(url, jsondata, headers=headers)

                if r["status"] != 200:
                    self.logError("HTTP %d, headers: %s, response: %s" %
                                  (r["status"], r["headers"], r["response"]))
                else:
                    self.log("HTTP %d, headers: %s, response: %s" %
                             (r["status"], r["headers"], r["response"]))
                    self.logSuccess("Processed: %s: %s (%d of %d)" %
                                    (an["SHELTERCODE"], an["ANIMALNAME"],
                                     anCount, len(animals)))
                    processed.append(an)

            except Exception as err:
                self.logError(
                    "Failed processing animal: %s, %s" %
                    (str(an["SHELTERCODE"]), err), sys.exc_info())

        # Next, identify animals we've previously sent who:
        # 1. Have an active exit movement in the last month or died in the last month
        # 2. Have an entry in animalpublished/petrescue where the sent date is older than the active movement
        # 3. Have an entry in animalpublished/petrescue where the sent date is older than the deceased date

        animals = self.dbo.query("SELECT a.ID, a.ShelterCode, a.AnimalName, p.SentDate, a.ActiveMovementDate, a.DeceasedDate FROM animal a " \
            "INNER JOIN animalpublished p ON p.AnimalID = a.ID AND p.PublishedTo='petrescue' " \
            "WHERE Archived = 1 AND ((DeceasedDate Is Not Null AND DeceasedDate >= ?) OR " \
            "(ActiveMovementDate Is Not Null AND ActiveMovementDate >= ? AND ActiveMovementType NOT IN (2,8))) " \
            "ORDER BY a.ID", [self.dbo.today(offset=-30), self.dbo.today(offset=-30)])

        for an in animals:
            if (an.ACTIVEMOVEMENTDATE and an.SENTDATE < an.ACTIVEMOVEMENTDATE
                ) or (an.DECEASEDDATE and an.SENTDATE < an.DECEASEDDATE):

                status = utils.iif(an.DECEASEDDATE is not None, "removed",
                                   "rehomed")
                data = {"status": status}
                jsondata = utils.json(data)
                url = PETRESCUE_URL + "listings/%s/SM%s" % (an.ID,
                                                            self.dbo.database)

                self.log("Sending PATCH to %s to update existing listing: %s" %
                         (url, jsondata))
                r = utils.patch_json(url, jsondata, headers=headers)

                if r["status"] != 200:
                    self.logError("HTTP %d, headers: %s, response: %s" %
                                  (r["status"], r["headers"], r["response"]))
                else:
                    self.log("HTTP %d, headers: %s, response: %s" %
                             (r["status"], r["headers"], r["response"]))
                    self.logSuccess("%s - %s: Marked with new status %s" %
                                    (an.SHELTERCODE, an.ANIMALNAME, status))
                    # By marking these animals in the processed list again, their SentDate
                    # will become today, which should exclude them from sending these status
                    # updates to close the listing again in future
                    processed.append(an)

        # Mark sent animals published
        self.markAnimalsPublished(processed, first=True)

        self.cleanup()
示例#5
0
def animal_tags(dbo, a):
    """
    Generates a list of tags from an animal result (the deep type from
    calling animal.get_animal)
    """
    l = dbo.locale
    displaydob = python2display(l, a["DATEOFBIRTH"])
    displayage = a["ANIMALAGE"]
    estimate = ""
    if a["ESTIMATEDDOB"] == 1: 
        displaydob = a["AGEGROUP"]
        displayage = a["AGEGROUP"]
        estimate = _("estimate", l)

    tags = { 
        "ANIMALNAME"            : a["ANIMALNAME"],
        "ANIMALTYPENAME"        : a["ANIMALTYPENAME"],
        "BASECOLOURNAME"        : a["BASECOLOURNAME"],
        "BASECOLORNAME"         : a["BASECOLOURNAME"],
        "BREEDNAME"             : a["BREEDNAME"],
        "INTERNALLOCATION"      : a["SHELTERLOCATIONNAME"],
        "LOCATIONUNIT"          : a["SHELTERLOCATIONUNIT"],
        "COATTYPE"              : a["COATTYPENAME"],
        "HEALTHPROBLEMS"        : a["HEALTHPROBLEMS"],
        "ANIMALCREATEDBY"       : a["CREATEDBY"],
        "ANIMALCREATEDDATE"     : python2display(l, a["CREATEDDATE"]),
        "DATEBROUGHTIN"         : python2display(l, a["DATEBROUGHTIN"]),
        "DATEOFBIRTH"           : python2display(l, a["DATEOFBIRTH"]),
        "AGEGROUP"              : a["AGEGROUP"],
        "DISPLAYDOB"            : displaydob,
        "DISPLAYAGE"            : displayage,
        "ESTIMATEDDOB"          : estimate,
        "ANIMALID"              : str(a["ID"]),
        "IDENTICHIPNUMBER"      : a["IDENTICHIPNUMBER"],
        "IDENTICHIPPED"         : a["IDENTICHIPPEDNAME"],
        "IDENTICHIPPEDDATE"     : python2display(l, a["IDENTICHIPDATE"]),
        "MICROCHIPNUMBER"       : a["IDENTICHIPNUMBER"],
        "MICROCHIPPED"          : a["IDENTICHIPPEDNAME"],
        "MICROCHIPDATE"         : python2display(l, a["IDENTICHIPDATE"]),
        "TATTOO"                : a["TATTOONAME"],
        "TATTOODATE"            : python2display(l, a["TATTOODATE"]),
        "TATTOONUMBER"          : a["TATTOONUMBER"],
        "COMBITESTED"           : a["COMBITESTEDNAME"],
        "FIVLTESTED"            : a["COMBITESTEDNAME"],
        "COMBITESTDATE"         : python2display(l, a["COMBITESTDATE"]),
        "FIVLTESTDATE"          : python2display(l, a["COMBITESTDATE"]),
        "COMBITESTRESULT"       : a["COMBITESTRESULTNAME"],
        "FIVTESTRESULT"         : a["COMBITESTRESULTNAME"],
        "FIVRESULT"             : a["COMBITESTRESULTNAME"],
        "FLVTESTRESULT"         : a["FLVRESULTNAME"],
        "FLVRESULT"             : a["FLVRESULTNAME"],
        "HEARTWORMTESTED"       : a["HEARTWORMTESTEDNAME"],
        "HEARTWORMTESTDATE"     : python2display(l, a["HEARTWORMTESTDATE"]),
        "HEARTWORMTESTRESULT"   : a["HEARTWORMTESTRESULTNAME"],
        "HIDDENANIMALDETAILS"   : a["HIDDENANIMALDETAILS"],
        "ANIMALLASTCHANGEDBY"   : a["LASTCHANGEDBY"],
        "ANIMALLASTCHANGEDDATE" : python2display(l, a["LASTCHANGEDDATE"]),
        "MARKINGS"              : a["MARKINGS"],
        "DECLAWED"              : a["DECLAWEDNAME"],
        "RABIESTAG"             : a["RABIESTAG"],
        "GOODWITHCATS"          : a["ISGOODWITHCATSNAME"],
        "GOODWITHDOGS"          : a["ISGOODWITHDOGSNAME"],
        "GOODWITHCHILDREN"      : a["ISGOODWITHCHILDRENNAME"],
        "HOUSETRAINED"          : a["ISHOUSETRAINEDNAME"],
        "NAMEOFPERSONBROUGHTANIMALIN" : a["BROUGHTINBYOWNERNAME"],
        "ADDRESSOFPERSONBROUGHTANIMALIN" : a["BROUGHTINBYOWNERADDRESS"],
        "TOWNOFPERSONBROUGHTANIMALIN" : a["BROUGHTINBYOWNERTOWN"],
        "COUNTYOFPERSONBROUGHTANIMALIN": a["BROUGHTINBYOWNERCOUNTY"],
        "POSTCODEOFPERSONBROUGHTIN": a["BROUGHTINBYOWNERPOSTCODE"],
        "CITYOFPERSONBROUGHTANIMALIN" : a["BROUGHTINBYOWNERTOWN"],
        "STATEOFPERSONBROUGHTANIMALIN": a["BROUGHTINBYOWNERCOUNTY"],
        "ZIPCODEOFPERSONBROUGHTIN": a["BROUGHTINBYOWNERPOSTCODE"],
        "BROUGHTINBYNAME"     : a["BROUGHTINBYOWNERNAME"],
        "BROUGHTINBYADDRESS"  : a["BROUGHTINBYOWNERADDRESS"],
        "BROUGHTINBYTOWN"     : a["BROUGHTINBYOWNERTOWN"],
        "BROUGHTINBYCOUNTY"   : a["BROUGHTINBYOWNERCOUNTY"],
        "BROUGHTINBYPOSTCODE" : a["BROUGHTINBYOWNERPOSTCODE"],
        "BROUGHTINBYCITY"     : a["BROUGHTINBYOWNERTOWN"],
        "BROUGHTINBYSTATE"    : a["BROUGHTINBYOWNERCOUNTY"],
        "BROUGHTINBYZIPCODE"  : a["BROUGHTINBYOWNERPOSTCODE"],
        "BROUGHTINBYHOMEPHONE" : a["BROUGHTINBYHOMETELEPHONE"],
        "BROUGHTINBYPHONE"    : a["BROUGHTINBYHOMETELEPHONE"],
        "BROUGHTINBYWORKPHONE" : a["BROUGHTINBYWORKTELEPHONE"],
        "BROUGHTINBYMOBILEPHONE" : a["BROUGHTINBYMOBILETELEPHONE"],
        "BROUGHTINBYCELLPHONE" : a["BROUGHTINBYMOBILETELEPHONE"],
        "BROUGHTINBYEMAIL"    : a["BROUGHTINBYEMAILADDRESS"],
        "NAMEOFOWNERSVET"       : a["OWNERSVETNAME"],
        "NAMEOFCURRENTVET"      : a["CURRENTVETNAME"],
        "HASSPECIALNEEDS"       : a["HASSPECIALNEEDSNAME"],
        "NEUTERED"              : a["NEUTEREDNAME"],
        "FIXED"                 : a["NEUTEREDNAME"],
        "ALTERED"               : a["NEUTEREDNAME"],
        "NEUTEREDDATE"          : python2display(l, a["NEUTEREDDATE"]),
        "FIXEDDATE"             : python2display(l, a["NEUTEREDDATE"]),
        "ALTEREDDATE"           : python2display(l, a["NEUTEREDDATE"]),
        "ORIGINALOWNERNAME"     : a["ORIGINALOWNERNAME"],
        "ORIGINALOWNERADDRESS"  : a["ORIGINALOWNERADDRESS"],
        "ORIGINALOWNERTOWN"     : a["ORIGINALOWNERTOWN"],
        "ORIGINALOWNERCOUNTY"   : a["ORIGINALOWNERCOUNTY"],
        "ORIGINALOWNERPOSTCODE" : a["ORIGINALOWNERPOSTCODE"],
        "ORIGINALOWNERCITY"     : a["ORIGINALOWNERTOWN"],
        "ORIGINALOWNERSTATE"    : a["ORIGINALOWNERCOUNTY"],
        "ORIGINALOWNERZIPCODE"  : a["ORIGINALOWNERPOSTCODE"],
        "ORIGINALOWNERHOMEPHONE" : a["ORIGINALOWNERHOMETELEPHONE"],
        "ORIGINALOWNERPHONE"    : a["ORIGINALOWNERHOMETELEPHONE"],
        "ORIGINALOWNERWORKPHONE" : a["ORIGINALOWNERWORKTELEPHONE"],
        "ORIGINALOWNERMOBILEPHONE" : a["ORIGINALOWNERMOBILETELEPHONE"],
        "ORIGINALOWNERCELLPHONE" : a["ORIGINALOWNERMOBILETELEPHONE"],
        "ORIGINALOWNEREMAIL"    : a["ORIGINALOWNEREMAILADDRESS"],
        "CURRENTOWNERNAME"     : a["CURRENTOWNERNAME"],
        "CURRENTOWNERADDRESS"  : a["CURRENTOWNERADDRESS"],
        "CURRENTOWNERTOWN"     : a["CURRENTOWNERTOWN"],
        "CURRENTOWNERCOUNTY"   : a["CURRENTOWNERCOUNTY"],
        "CURRENTOWNERPOSTCODE" : a["CURRENTOWNERPOSTCODE"],
        "CURRENTOWNERCITY"     : a["CURRENTOWNERTOWN"],
        "CURRENTOWNERSTATE"    : a["CURRENTOWNERCOUNTY"],
        "CURRENTOWNERZIPCODE"  : a["CURRENTOWNERPOSTCODE"],
        "CURRENTOWNERHOMEPHONE" : a["CURRENTOWNERHOMETELEPHONE"],
        "CURRENTOWNERPHONE"    : a["CURRENTOWNERHOMETELEPHONE"],
        "CURRENTOWNERWORKPHONE" : a["CURRENTOWNERWORKTELEPHONE"],
        "CURRENTOWNERMOBILEPHONE" : a["CURRENTOWNERMOBILETELEPHONE"],
        "CURRENTOWNERCELLPHONE" : a["CURRENTOWNERMOBILETELEPHONE"],
        "CURRENTOWNEREMAIL"     : a["CURRENTOWNEREMAILADDRESS"],
        "CURRENTVETNAME"        : a["CURRENTVETNAME"],
        "CURRENTVETADDRESS"     : a["CURRENTVETADDRESS"],
        "CURRENTVETTOWN"        : a["CURRENTVETTOWN"],
        "CURRENTVETCOUNTY"      : a["CURRENTVETCOUNTY"],
        "CURRENTVETPOSTCODE"    : a["CURRENTVETPOSTCODE"],
        "CURRENTVETCITY"        : a["CURRENTVETTOWN"],
        "CURRENTVETSTATE"       : a["CURRENTVETCOUNTY"],
        "CURRENTVETZIPCODE"     : a["CURRENTVETPOSTCODE"],
        "CURRENTVETPHONE"       : a["CURRENTVETWORKTELEPHONE"],
        "OWNERSVETNAME"         : a["OWNERSVETNAME"],
        "OWNERSVETADDRESS"      : a["OWNERSVETADDRESS"],
        "OWNERSVETTOWN"         : a["OWNERSVETTOWN"],
        "OWNERSVETCOUNTY"       : a["OWNERSVETCOUNTY"],
        "OWNERSVETPOSTCODE"     : a["OWNERSVETPOSTCODE"],
        "OWNERSVETCITY"         : a["OWNERSVETTOWN"],
        "OWNERSVETSTATE"        : a["OWNERSVETCOUNTY"],
        "OWNERSVETZIPCODE"      : a["OWNERSVETPOSTCODE"],
        "OWNERSVETPHONE"        : a["OWNERSVETWORKTELEPHONE"],
        "RESERVEDOWNERNAME"     : a["RESERVEDOWNERNAME"],
        "RESERVEDOWNERADDRESS"  : a["RESERVEDOWNERADDRESS"],
        "RESERVEDOWNERTOWN"     : a["RESERVEDOWNERTOWN"],
        "RESERVEDOWNERCOUNTY"   : a["RESERVEDOWNERCOUNTY"],
        "RESERVEDOWNERPOSTCODE" : a["RESERVEDOWNERPOSTCODE"],
        "RESERVEDOWNERCITY"     : a["RESERVEDOWNERTOWN"],
        "RESERVEDOWNERSTATE"    : a["RESERVEDOWNERCOUNTY"],
        "RESERVEDOWNERZIPCODE"  : a["RESERVEDOWNERPOSTCODE"],
        "RESERVEDOWNERHOMEPHONE" : a["RESERVEDOWNERHOMETELEPHONE"],
        "RESERVEDOWNERPHONE"    : a["RESERVEDOWNERHOMETELEPHONE"],
        "RESERVEDOWNERWORKPHONE" : a["RESERVEDOWNERWORKTELEPHONE"],
        "RESERVEDOWNERMOBILEPHONE" : a["RESERVEDOWNERMOBILETELEPHONE"],
        "RESERVEDOWNERCELLPHONE" : a["RESERVEDOWNERMOBILETELEPHONE"],
        "RESERVEDOWNEREMAIL"    : a["RESERVEDOWNEREMAILADDRESS"],
        "ENTRYCATEGORY"         : a["ENTRYREASONNAME"],
        "REASONFORENTRY"        : a["REASONFORENTRY"],
        "REASONNOTBROUGHTBYOWNER" : a["REASONNO"],
        "SEX"                   : a["SEXNAME"],
        "SIZE"                  : a["SIZENAME"],
        "SPECIESNAME"           : a["SPECIESNAME"],
        "ANIMALCOMMENTS"        : a["ANIMALCOMMENTS"],
        "SHELTERCODE"           : a["SHELTERCODE"],
        "AGE"                   : a["ANIMALAGE"],
        "ACCEPTANCENUMBER"      : a["ACCEPTANCENUMBER"],
        "LITTERID"              : a["ACCEPTANCENUMBER"],
        "DECEASEDDATE"          : python2display(l, a["DECEASEDDATE"]),
        "DECEASEDNOTES"         : a["PTSREASON"],
        "DECEASEDCATEGORY"      : a["PTSREASONNAME"],
        "SHORTSHELTERCODE"      : a["SHORTCODE"],
        "MOSTRECENTENTRY"       : python2display(l, a["MOSTRECENTENTRYDATE"]),
        "TIMEONSHELTER"         : a["TIMEONSHELTER"],
        "WEBMEDIAFILENAME"      : a["WEBSITEMEDIANAME"],
        "WEBSITEMEDIANAME"      : a["WEBSITEMEDIANAME"],
        "WEBSITEVIDEOURL"       : a["WEBSITEVIDEOURL"],
        "WEBSITEVIDEONOTES"     : a["WEBSITEVIDEONOTES"],
        "WEBMEDIANOTES"         : a["WEBSITEMEDIANOTES"],
        "WEBSITEMEDIANOTES"     : a["WEBSITEMEDIANOTES"],
        "DOCUMENTIMGLINK"       : "<img height=\"200\" src=\"" + html.img_src(a, "animal") + "\" >",
        "DOCUMENTIMGTHUMBLINK"  : "<img src=\"" + html.thumbnail_img_src(a, "animalthumb") + "\" />",
        "DOCUMENTQRLINK"        : "<img src=\"http://chart.apis.google.com/chart?cht=qr&chl=%s&chs=150x150\" />" % (BASE_URL + "/animal?id=%s" % a["ID"]),
        "ANIMALONSHELTER"       : yes_no(l, a["ARCHIVED"] == 0),
        "ANIMALISRESERVED"      : yes_no(l, a["HASACTIVERESERVE"] == 1),
        "ADOPTIONID"            : a["ACTIVEMOVEMENTADOPTIONNUMBER"],
        "ADOPTIONNUMBER"        : a["ACTIVEMOVEMENTADOPTIONNUMBER"],
        "INSURANCENUMBER"       : a["ACTIVEMOVEMENTINSURANCENUMBER"],
        "RESERVATIONDATE"       : python2display(l, a["ACTIVEMOVEMENTRESERVATIONDATE"]),
        "RETURNDATE"            : python2display(l, a["ACTIVEMOVEMENTRETURNDATE"]),
        "ADOPTIONDATE"          : python2display(l, a["ACTIVEMOVEMENTDATE"]),
        "FOSTEREDDATE"          : python2display(l, a["ACTIVEMOVEMENTDATE"]),
        "TRANSFERDATE"          : python2display(l, a["ACTIVEMOVEMENTDATE"]),
        "MOVEMENTDATE"          : python2display(l, a["ACTIVEMOVEMENTDATE"]),
        "MOVEMENTTYPE"          : a["ACTIVEMOVEMENTTYPENAME"],
        "ADOPTIONDONATION"      : format_currency_no_symbol(l, a["ACTIVEMOVEMENTDONATION"]),
        "ADOPTIONCREATEDBY"     : a["ACTIVEMOVEMENTCREATEDBY"],
        "ADOPTIONCREATEDBYNAME" : a["ACTIVEMOVEMENTCREATEDBYNAME"],
        "ADOPTIONCREATEDDATE"   : python2display(l, a["ACTIVEMOVEMENTCREATEDDATE"]),
        "ADOPTIONLASTCHANGEDBY" : a["ACTIVEMOVEMENTLASTCHANGEDBY"],
        "ADOPTIONLASTCHANGEDDATE" : python2display(l, a["ACTIVEMOVEMENTLASTCHANGEDDATE"])
    }

    # Set original owner to be current owner on non-shelter animals
    if a["NONSHELTERANIMAL"] == 1 and a["ORIGINALOWNERNAME"] is not None and a["ORIGINALOWNERNAME"] != "":
        tags["CURRENTOWNERNAME"] = a["ORIGINALOWNERNAME"]
        tags["CURRENTOWNERADDRESS"] = a["ORIGINALOWNERADDRESS"]
        tags["CURRENTOWNERTOWN"] = a["ORIGINALOWNERTOWN"]
        tags["CURRENTOWNERCOUNTY"] = a["ORIGINALOWNERCOUNTY"]
        tags["CURRENTOWNERPOSTCODE"] = a["ORIGINALOWNERPOSTCODE"]
        tags["CURRENTOWNERCITY"] = a["ORIGINALOWNERTOWN"]
        tags["CURRENTOWNERSTATE"] = a["ORIGINALOWNERCOUNTY"]
        tags["CURRENTOWNERZIPCODE"] = a["ORIGINALOWNERPOSTCODE"]
        tags["CURRENTOWNERHOMEPHONE"] = a["ORIGINALOWNERHOMETELEPHONE"]
        tags["CURRENTOWNERPHONE"] = a["ORIGINALOWNERHOMETELEPHONE"]
        tags["CURRENTOWNERWORKPHONE"] = a["ORIGINALOWNERWORKTELEPHONE"]
        tags["CURRENTOWNERMOBILEPHONE"] = a["ORIGINALOWNERMOBILETELEPHONE"]
        tags["CURRENTOWNERCELLPHONE"] = a["ORIGINALOWNERMOBILETELEPHONE"]
        tags["CURRENTOWNEREMAIL"] = a["ORIGINALOWNEREMAILADDRESS"]

    # If the animal doesn't have a current owner, but does have an open
    # movement with a future date on it, look up the owner and use that 
    # instead so that we can still generate paperwork for future adoptions.
    if a["CURRENTOWNERID"] is None or a["CURRENTOWNERID"] == 0:
        latest = animal.get_latest_movement(dbo, a["ID"])
        if latest is not None:
            p = person.get_person(dbo, latest["OWNERID"])
            if p is not None:
                tags["CURRENTOWNERNAME"] = p["OWNERNAME"]
                tags["CURRENTOWNERADDRESS"] = p["OWNERADDRESS"]
                tags["CURRENTOWNERTOWN"] = p["OWNERTOWN"]
                tags["CURRENTOWNERCOUNTY"] = p["OWNERCOUNTY"]
                tags["CURRENTOWNERPOSTCODE"] = p["OWNERPOSTCODE"]
                tags["CURRENTOWNERCITY"] = p["OWNERTOWN"]
                tags["CURRENTOWNERSTATE"] = p["OWNERCOUNTY"]
                tags["CURRENTOWNERZIPCODE"] = p["OWNERPOSTCODE"]
                tags["CURRENTOWNERHOMEPHONE"] = p["HOMETELEPHONE"]
                tags["CURRENTOWNERPHONE"] = p["HOMETELEPHONE"]
                tags["CURRENTOWNERWORKPHONE"] = p["WORKTELEPHONE"]
                tags["CURRENTOWNERMOBILEPHONE"] = p["MOBILETELEPHONE"]
                tags["CURRENTOWNERCELLPHONE"] = p["MOBILETELEPHONE"]
                tags["CURRENTOWNEREMAIL"] = p["EMAILADDRESS"]

    # Additional fields
    add = additional.get_additional_fields(dbo, int(a["ID"]), "animal")
    for af in add:
        val = af["VALUE"]
        if af["FIELDTYPE"] == additional.YESNO:
            val = additional_yesno(l, af)
        if af["FIELDTYPE"] == additional.MONEY:
            val = format_currency_no_symbol(l, af["VALUE"])
        tags[af["FIELDNAME"].upper()] = val

    include_incomplete = configuration.include_incomplete_medical_doc(dbo)
    
    # Vaccinations
    vaccasc = medical.get_vaccinations(dbo, int(a["ID"]), not include_incomplete)
    vaccdesc = medical.get_vaccinations(dbo, int(a["ID"]), not include_incomplete, medical.DESCENDING_REQUIRED)
    for idx in range(1, 101):
        tags["VACCINATIONNAME" + str(idx)] = ""
        tags["VACCINATIONREQUIRED" + str(idx)] = ""
        tags["VACCINATIONGIVEN" + str(idx)] = ""
        tags["VACCINATIONCOMMENTS" + str(idx)] = ""
        tags["VACCINATIONDESCRIPTION" + str(idx)] = ""
        tags["VACCINATIONNAMELAST" + str(idx)] = ""
        tags["VACCINATIONREQUIREDLAST" + str(idx)] = ""
        tags["VACCINATIONGIVENLAST" + str(idx)] = ""
        tags["VACCINATIONCOMMENTSLAST" + str(idx)] = ""
        tags["VACCINATIONDESCRIPTIONLAST" + str(idx)] = ""
    idx = 1
    for v in vaccasc:
        tags["VACCINATIONNAME" + str(idx)] = v["VACCINATIONTYPE"]
        tags["VACCINATIONREQUIRED" + str(idx)] = python2display(l, v["DATEREQUIRED"])
        tags["VACCINATIONGIVEN" + str(idx)] = python2display(l, v["DATEOFVACCINATION"])
        tags["VACCINATIONCOMMENTS" + str(idx)] = v["COMMENTS"]
        tags["VACCINATIONDESCRIPTION" + str(idx)] = v["VACCINATIONDESCRIPTION"]
        idx += 1
    idx = 1
    uniquetypes = {}
    recentgiven = {}
    for v in vaccdesc:
        tags["VACCINATIONNAMELAST" + str(idx)] = v["VACCINATIONTYPE"]
        tags["VACCINATIONREQUIREDLAST" + str(idx)] = python2display(l, v["DATEREQUIRED"])
        tags["VACCINATIONGIVENLAST" + str(idx)] = python2display(l, v["DATEOFVACCINATION"])
        tags["VACCINATIONCOMMENTSLAST" + str(idx)] = v["COMMENTS"]
        tags["VACCINATIONDESCRIPTIONLAST" + str(idx)] = v["VACCINATIONDESCRIPTION"]
        idx += 1
        # If this is the first of this type of vacc we've seen, make
        # some keys based on its name.
        if not uniquetypes.has_key(v["VACCINATIONTYPE"]):
            vname = v["VACCINATIONTYPE"].upper().replace(" ", "").replace("/", "")
            uniquetypes[v["VACCINATIONTYPE"]] = v
            tags["VACCINATIONNAME" + vname] = v["VACCINATIONTYPE"]
            tags["VACCINATIONREQUIRED" + vname] = python2display(l, v["DATEREQUIRED"])
            tags["VACCINATIONGIVEN" + vname] = python2display(l, v["DATEOFVACCINATION"])
            tags["VACCINATIONCOMMENTS" + vname] = v["COMMENTS"]
            tags["VACCINATIONDESCRIPTION" + vname] = v["VACCINATIONDESCRIPTION"]
        # If this is the first of this type of vacc we've seen that's been given
        # make some keys based on its name
        if not recentgiven.has_key(v["VACCINATIONTYPE"]) and v["DATEOFVACCINATION"] is not None:
            vname = v["VACCINATIONTYPE"].upper().replace(" ", "").replace("/", "")
            recentgiven[v["VACCINATIONTYPE"]] = v
            tags["VACCINATIONNAMERECENT" + vname] = v["VACCINATIONTYPE"]
            tags["VACCINATIONREQUIREDRECENT" + vname] = python2display(l, v["DATEREQUIRED"])
            tags["VACCINATIONGIVENRECENT" + vname] = python2display(l, v["DATEOFVACCINATION"])
            tags["VACCINATIONCOMMENTSRECENT" + vname] = v["COMMENTS"]
            tags["VACCINATIONDESCRIPTIONRECENT" + vname] = v["VACCINATIONDESCRIPTION"]

    # Tests
    testasc = medical.get_tests(dbo, int(a["ID"]), not include_incomplete)
    testdesc = medical.get_tests(dbo, int(a["ID"]), not include_incomplete, medical.DESCENDING_REQUIRED)
    for idx in range(1, 101):
        tags["TESTNAME" + str(idx)] = ""
        tags["TESTRESULT" + str(idx)] = ""
        tags["TESTREQUIRED" + str(idx)] = ""
        tags["TESTGIVEN" + str(idx)] = ""
        tags["TESTCOMMENTS" + str(idx)] = ""
        tags["TESTDESCRIPTION" + str(idx)] = ""
        tags["TESTNAMELAST" + str(idx)] = ""
        tags["TESTREQUIREDLAST" + str(idx)] = ""
        tags["TESTGIVENLAST" + str(idx)] = ""
        tags["TESTCOMMENTSLAST" + str(idx)] = ""
        tags["TESTDESCRIPTIONLAST" + str(idx)] = ""
    idx = 1
    for t in testasc:
        tags["TESTNAME" + str(idx)] = t["TESTNAME"]
        tags["TESTRESULT" + str(idx)] = t["RESULTNAME"]
        tags["TESTREQUIRED" + str(idx)] = python2display(l, t["DATEREQUIRED"])
        tags["TESTGIVEN" + str(idx)] = python2display(l, t["DATEOFTEST"])
        tags["TESTCOMMENTS" + str(idx)] = t["COMMENTS"]
        tags["TESTDESCRIPTION" + str(idx)] = t["TESTDESCRIPTION"]
        idx += 1
    idx = 1
    uniquetypes = {}
    recentgiven = {}
    for t in testdesc:
        tags["TESTNAMELAST" + str(idx)] = t["TESTNAME"]
        tags["TESTRESULTLAST" + str(idx)] = t["RESULTNAME"]
        tags["TESTREQUIREDLAST" + str(idx)] = python2display(l, t["DATEREQUIRED"])
        tags["TESTGIVENLAST" + str(idx)] = python2display(l, t["DATEOFTEST"])
        tags["TESTCOMMENTSLAST" + str(idx)] = t["COMMENTS"]
        tags["TESTDESCRIPTIONLAST" + str(idx)] = t["TESTDESCRIPTION"]
        idx += 1
        # If this is the first of this type of test we've seen, make
        # some keys based on its name.
        if not uniquetypes.has_key(t["TESTNAME"]):
            tname = t["TESTNAME"].upper().replace(" ", "").replace("/", "")
            uniquetypes[t["TESTNAME"]] = t
            tags["TESTNAME" + tname] = t["TESTNAME"]
            tags["TESTRESULT" + tname] = t["RESULTNAME"]
            tags["TESTREQUIRED" + tname] = python2display(l, t["DATEREQUIRED"])
            tags["TESTGIVEN" + tname] = python2display(l, t["DATEOFTEST"])
            tags["TESTCOMMENTS" + tname] = t["COMMENTS"]
            tags["TESTDESCRIPTION" + tname] = t["TESTDESCRIPTION"]
        # If this is the first of this type of test we've seen that's been given
        # make some keys based on its name
        if not recentgiven.has_key(t["TESTNAME"]) and t["DATEOFTEST"] is not None:
            tname = t["TESTNAME"].upper().replace(" ", "").replace("/", "")
            recentgiven[t["TESTNAME"]] = t
            tags["TESTNAMERECENT" + tname] = t["TESTNAME"]
            tags["TESTRESULTRECENT" + tname] = t["RESULTNAME"]
            tags["TESTREQUIREDRECENT" + tname] = python2display(l, t["DATEREQUIRED"])
            tags["TESTGIVENRECENT" + tname] = python2display(l, t["DATEOFTEST"])
            tags["TESTCOMMENTSRECENT" + tname] = t["COMMENTS"]
            tags["TESTDESCRIPTIONRECENT" + tname] = t["TESTDESCRIPTION"]

    # Medical
    medasc = medical.get_regimens(dbo, int(a["ID"]), not include_incomplete)
    meddesc = medical.get_regimens(dbo, int(a["ID"]), not include_incomplete, medical.DESCENDING_REQUIRED)
    for idx in range(1, 101):
        tags["MEDICALNAME" + str(idx)] = ""
        tags["MEDICALCOMMENTS" + str(idx)] = ""
        tags["MEDICALFREQUENCY" + str(idx)] = ""
        tags["MEDICALNUMBEROFTREATMENTS" + str(idx)] = ""
        tags["MEDICALSTATUS" + str(idx)] = ""
        tags["MEDICALDOSAGE" + str(idx)] = ""
        tags["MEDICALSTARTDATE" + str(idx)] = ""
        tags["MEDICALTREATMENTSGIVEN" + str(idx)] = ""
        tags["MEDICALTREATMENTSREMAINING" + str(idx)] = ""
        tags["MEDICALNAMELAST" + str(idx)] = ""
        tags["MEDICALCOMMENTSLAST" + str(idx)] = ""
        tags["MEDICALFREQUENCYLAST" + str(idx)] = ""
        tags["MEDICALNUMBEROFTREATMENTSLAST" + str(idx)] = ""
        tags["MEDICALSTATUSLAST" + str(idx)] = ""
        tags["MEDICALDOSAGELAST" + str(idx)] = ""
        tags["MEDICALSTARTDATELAST" + str(idx)] = ""
        tags["MEDICALTREATMENTSGIVENLAST" + str(idx)] = ""
        tags["MEDICALTREATMENTSREMAININGLAST" + str(idx)] = ""
    idx = 1
    for m in medasc:
        tags["MEDICALNAME" + str(idx)] = m["TREATMENTNAME"]
        tags["MEDICALCOMMENTS" + str(idx)] = m["COMMENTS"]
        tags["MEDICALFREQUENCY" + str(idx)] = m["NAMEDFREQUENCY"]
        tags["MEDICALNUMBEROFTREATMENTS" + str(idx)] = m["NAMEDNUMBEROFTREATMENTS"]
        tags["MEDICALSTATUS" + str(idx)] = m["NAMEDSTATUS"]
        tags["MEDICALDOSAGE" + str(idx)] = m["DOSAGE"]
        tags["MEDICALSTARTDATE" + str(idx)] = python2display(l, m["STARTDATE"])
        tags["MEDICALTREATMENTSGIVEN" + str(idx)] = str(m["TREATMENTSGIVEN"])
        tags["MEDICALTREATMENTSREMAINING" + str(idx)] = str(m["TREATMENTSREMAINING"])
        idx += 1
    idx = 1
    uniquetypes = {}
    recentgiven = {}
    for m in meddesc:
        tags["MEDICALNAMELAST" + str(idx)] = m["TREATMENTNAME"]
        tags["MEDICALCOMMENTSLAST" + str(idx)] = m["COMMENTS"]
        tags["MEDICALFREQUENCYLAST" + str(idx)] = m["NAMEDFREQUENCY"]
        tags["MEDICALNUMBEROFTREATMENTSLAST" + str(idx)] = m["NAMEDNUMBEROFTREATMENTS"]
        tags["MEDICALSTATUSLAST" + str(idx)] = m["NAMEDSTATUS"]
        tags["MEDICALDOSAGELAST" + str(idx)] = m["DOSAGE"]
        tags["MEDICALSTARTDATELAST" + str(idx)] = python2display(l, m["STARTDATE"])
        tags["MEDICALTREATMENTSGIVENLAST" + str(idx)] = str(m["TREATMENTSGIVEN"])
        tags["MEDICALTREATMENTSREMAININGLAST" + str(idx)] = str(m["TREATMENTSREMAINING"])
        idx += 1
        # If this is the first of this type of med we've seen, make
        # some keys based on its name.
        if not uniquetypes.has_key(m["TREATMENTNAME"]):
            tname = m["TREATMENTNAME"].upper().replace(" ", "").replace("/", "")
            uniquetypes[m["TREATMENTNAME"]] = m
            tags["MEDICALNAME" + tname] = m["TREATMENTNAME"]
            tags["MEDICALCOMMENTS" + tname] = m["COMMENTS"]
            tags["MEDICALFREQUENCY" + tname] = m["NAMEDFREQUENCY"]
            tags["MEDICALNUMBEROFTREATMENTS" + tname] = m["NAMEDNUMBEROFTREATMENTS"]
            tags["MEDICALSTATUS" + tname] = m["NAMEDSTATUS"]
            tags["MEDICALDOSAGE" + tname] = m["DOSAGE"]
            tags["MEDICALSTARTDATE" + tname] = python2display(l, m["STARTDATE"])
            tags["MEDICALTREATMENTSGIVEN" + tname] = str(m["TREATMENTSGIVEN"])
            tags["MEDICALTREATMENTSREMAINING" + tname] = str(m["TREATMENTSREMAINING"])
        # If this is the first of this type of med we've seen that's complete
        if not recentgiven.has_key(m["TREATMENTNAME"]) and m["STATUS"] == 2:
            tname = m["TREATMENTNAME"].upper().replace(" ", "").replace("/", "")
            recentgiven[m["TREATMENTNAME"]] = m
            tags["MEDICALNAMERECENT" + tname] = m["TREATMENTNAME"]
            tags["MEDICALCOMMENTSRECENT" + tname] = m["COMMENTS"]
            tags["MEDICALFREQUENCYRECENT" + tname] = m["NAMEDFREQUENCY"]
            tags["MEDICALNUMBEROFTREATMENTSRECENT" + tname] = m["NAMEDNUMBEROFTREATMENTS"]
            tags["MEDICALSTATUSRECENT" + tname] = m["NAMEDSTATUS"]
            tags["MEDICALDOSAGERECENT" + tname] = m["DOSAGE"]
            tags["MEDICALSTARTDATERECENT" + tname] = python2display(l, m["STARTDATE"])
            tags["MEDICALTREATMENTSGIVENRECENT" + tname] = str(m["TREATMENTSGIVEN"])
            tags["MEDICALTREATMENTSREMAININGRECENT" + tname] = str(m["TREATMENTSREMAINING"])

    # Diet
    dietasc = animal.get_diets(dbo, int(a["ID"]))
    dietdesc = animal.get_diets(dbo, int(a["ID"]), animal.DESCENDING)
    for idx in range(1, 101):
        tags["DIETNAME" + str(idx)] = ""
        tags["DIETDESCRIPTION" + str(idx)] = ""
        tags["DIETDATESTARTED" + str(idx)] = ""
        tags["DIETCOMMENTS" + str(idx)] = ""
        tags["DIETNAMELAST" + str(idx)] = ""
        tags["DIETDESCRIPTIONLAST" + str(idx)] = ""
        tags["DIETDATESTARTEDLAST" + str(idx)] = ""
        tags["DIETCOMMENTSLAST" + str(idx)] = ""
    idx = 1
    for d in dietasc:
        tags["DIETNAME" + str(idx)] = d["DIETNAME"]
        tags["DIETDESCRIPTION" + str(idx)] = d["DIETDESCRIPTION"]
        tags["DIETDATESTARTED" + str(idx)] = python2display(l, d["DATESTARTED"])
        tags["DIETCOMMENTS" + str(idx)] = d["COMMENTS"]
        idx += 1
    idx = 1
    for d in dietdesc:
        tags["DIETNAMELAST" + str(idx)] = d["DIETNAME"]
        tags["DIETDESCRIPTIONLAST" + str(idx)] = d["DIETDESCRIPTION"]
        tags["DIETDATESTARTEDLAST" + str(idx)] = python2display(l, d["DATESTARTED"])
        tags["DIETCOMMENTSLAST" + str(idx)] = d["COMMENTS"]
        idx += 1

    # Donations
    donasc = financial.get_animal_donations(dbo, int(a["ID"]))
    dondesc = financial.get_animal_donations(dbo, int(a["ID"]), financial.DESCENDING)
    for idx in range(1, 101):
        tags["RECEIPTNUM" + str(idx)] = ""
        tags["DONATIONTYPE" + str(idx)] = ""
        tags["DONATIONDATE" + str(idx)] = ""
        tags["DONATIONDATEDUE" + str(idx)] = ""
        tags["DONATIONAMOUNT" + str(idx)] = ""
        tags["DONATIONCOMMENTS" + str(idx)] = ""
        tags["DONATIONGIFTAID" + str(idx)] = ""
        tags["RECEIPTNUMLAST" + str(idx)] = ""
        tags["DONATIONTYPELAST" + str(idx)] = ""
        tags["DONATIONDATELAST" + str(idx)] = ""
        tags["DONATIONDATEDUELAST" + str(idx)] = ""
        tags["DONATIONAMOUNTLAST" + str(idx)] = ""
        tags["DONATIONCOMMENTSLAST" + str(idx)] = ""
        tags["DONATIONGIFTAIDLAST" + str(idx)] = ""
    idx = 1
    for d in donasc:
        tags["RECEIPTNUM" + str(idx)] = utils.padleft(d["ID"], 8)
        tags["DONATIONTYPE" + str(idx)] = d["DONATIONNAME"]
        tags["DONATIONDATE" + str(idx)] = python2display(l, d["DATE"])
        tags["DONATIONDATEDUE" + str(idx)] = python2display(l, d["DATEDUE"])
        tags["DONATIONAMOUNT" + str(idx)] = format_currency_no_symbol(l, d["DONATION"])
        tags["DONATIONCOMMENTS" + str(idx)] = d["COMMENTS"]
        tags["DONATIONGIFTAID" + str(idx)] = d["ISGIFTAID"] == 1 and _("Yes", l) or _("No", l)
    idx = 1
    uniquetypes = {}
    recentrec = {}
    for d in dondesc:
        tags["RECEIPTNUMLAST" + str(idx)] = utils.padleft(d["ID"], 8)
        tags["DONATIONTYPELAST" + str(idx)] = d["DONATIONNAME"]
        tags["DONATIONDATELAST" + str(idx)] = python2display(l, d["DATE"])
        tags["DONATIONDATEDUELAST" + str(idx)] = python2display(l, d["DATEDUE"])
        tags["DONATIONAMOUNTLAST" + str(idx)] = format_currency_no_symbol(l, d["DONATION"])
        tags["DONATIONCOMMENTSLAST" + str(idx)] = d["COMMENTS"]
        tags["DONATIONGIFTAIDLAST" + str(idx)] = d["ISGIFTAID"] == 1 and _("Yes", l) or _("No", l)
        idx += 1
        # If this is the first of this type of donation we've seen, make
        # some keys based on its name.
        if not uniquetypes.has_key(d["DONATIONNAME"]):
            dname = d["DONATIONNAME"].upper().replace(" ", "").replace("/", "")
            uniquetypes[d["DONATIONNAME"]] = d
            tags["RECEIPTNUM" + dname] = utils.padleft(d["ID"], 8)
            tags["DONATIONTYPE" + dname] = d["DONATIONNAME"]
            tags["DONATIONDATE" + dname] = python2display(l, d["DATE"])
            tags["DONATIONDATEDUE" + dname] = python2display(l, d["DATEDUE"])
            tags["DONATIONAMOUNT" + dname] = format_currency_no_symbol(l, d["DONATION"])
            tags["DONATIONCOMMENTS" + dname] = d["COMMENTS"]
            tags["DONATIONGIFTAID" + dname] = d["ISGIFTAID"] == 1 and _("Yes", l) or _("No", l)
        # If this is the first of this type of donation we've seen that's received
        if not recentrec.has_key(d["DONATIONNAME"]) and d["DATE"] is not None:
            dname = d["DONATIONNAME"].upper().replace(" ", "").replace("/", "")
            recentrec[d["DONATIONNAME"]] = d
            tags["RECEIPTNUMRECENT" + dname] = utils.padleft(d["ID"], 8)
            tags["DONATIONTYPERECENT" + dname] = d["DONATIONNAME"]
            tags["DONATIONDATERECENT" + dname] = python2display(l, d["DATE"])
            tags["DONATIONDATEDUERECENT" + dname] = python2display(l, d["DATEDUE"])
            tags["DONATIONAMOUNTRECENT" + dname] = format_currency_no_symbol(l, d["DONATION"])
            tags["DONATIONCOMMENTSRECENT" + dname] = d["COMMENTS"]
            tags["DONATIONGIFTAIDRECENT" + dname] = d["ISGIFTAID"] == 1 and _("Yes", l) or _("No", l)

    # Logs
    logasc = log.get_logs(dbo, log.ANIMAL, int(a["ID"]), 0, log.ASCENDING)
    logdesc = log.get_logs(dbo, log.ANIMAL, int(a["ID"]), 0, log.DESCENDING)
    for idx in range(1, 101):
        tags["LOGNAME" + str(idx)] = ""
        tags["LOGDATE" + str(idx)] = ""
        tags["LOGCOMMENTS" + str(idx)] = ""
        tags["LOGNAMELAST" + str(idx)] = ""
        tags["LOGDATELAST" + str(idx)] = ""
        tags["LOGCOMMENTSLAST" + str(idx)] = ""
    idx = 1
    for o in logasc:
        tags["LOGNAME" + str(idx)] = o["LOGTYPENAME"]
        tags["LOGDATE" + str(idx)] = python2display(l, o["DATE"])
        tags["LOGCOMMENTS" + str(idx)] = o["COMMENTS"]
        idx += 1
    idx = 1
    uniquetypes = {}
    recentgiven = {}
    for o in logdesc:
        tags["LOGNAMELAST" + str(idx)] = o["LOGTYPENAME"]
        tags["LOGDATELAST" + str(idx)] = python2display(l, o["DATE"])
        tags["LOGCOMMENTSLAST" + str(idx)] = o["COMMENTS"]
        idx += 1
        uniquetypes = {}
        recentrec = {}
        # If this is the first of this type of log we've seen, make
        # some keys based on its name.
        if not uniquetypes.has_key(o["LOGTYPENAME"]):
            lname = o["LOGTYPENAME"].upper().replace(" ", "").replace("/", "")
            uniquetypes[o["LOGTYPENAME"]] = o
            tags["LOGNAME" + lname] = o["LOGTYPENAME"]
            tags["LOGDATE" + lname] = python2display(l, o["DATE"])
            tags["LOGCOMMENTS" + lname] = o["COMMENTS"]
            tags["LOGNAMERECENT" + lname] = o["LOGTYPENAME"]
            tags["LOGDATERECENT" + lname] = python2display(l, o["DATE"])
            tags["LOGCOMMENTSRECENT" + lname] = o["COMMENTS"]

    return tags