bemade-addons/bemade_sports_clinic/models/patient.py
2024-09-18 14:35:51 -04:00

323 lines
12 KiB
Python

from odoo import models, fields, _, api, Command
from odoo.exceptions import ValidationError
from datetime import date
from dateutil.relativedelta import relativedelta
from odoo.addons.phone_validation.tools import phone_validation
import logging
_logger = logging.getLogger(__name__)
external_tracking_fields = {
"last_consultation_date",
"match_status",
"practice_status",
"predicted_return_date",
"return_date",
}
internal_tracking_fields = {
"team_info_notes",
"age",
"date_of_birth",
}
class Patient(models.Model):
_name = "sports.patient"
_description = "Patient"
_inherit = ["mail.thread", "mail.activity.mixin"]
_order = "last_name, first_name"
# res.partner fields
partner_id = fields.Many2one(
comodel_name="res.partner",
string="Contact",
ondelete="restrict",
compute_sudo=True,
)
first_name = fields.Char(required=True, tracking=True)
last_name = fields.Char(required=True, tracking=True)
name = fields.Char(
related="partner_id.name", compute="_compute_name", compute_sudo=True
)
phone = fields.Char(related="partner_id.phone", readonly=False)
mobile = fields.Char(related="partner_id.mobile", readonly=False)
street = fields.Char(related="partner_id.street", readonly=False)
street2 = fields.Char(related="partner_id.street2", readonly=False)
city = fields.Char(related="partner_id.city", readonly=False)
state_id = fields.Many2one(related="partner_id.state_id", readonly=False)
zip = fields.Char(related="partner_id.zip", readonly=False)
country_id = fields.Many2one(related="partner_id.country_id", readonly=False)
email = fields.Char(related="partner_id.email", readonly=False)
# Patient fields
date_of_birth = fields.Date(
groups="bemade_sports_clinic.group_sports_clinic_treatment_professional",
tracking=True,
)
age = fields.Integer(
compute="_compute_age",
groups="bemade_sports_clinic.group_sports_clinic_treatment_professional",
)
contact_ids = fields.One2many(
comodel_name="sports.patient.contact",
inverse_name="patient_id",
string="Patient Contacts",
groups="bemade_sports_clinic.group_sports_clinic_user",
)
team_ids = fields.Many2many(
comodel_name="sports.team",
relation="sports_team_patient_rel",
column1="patient_id",
column2="team_id",
string="Teams",
)
match_status = fields.Selection(
# Selection rather than bool for easy expansion later
selection=[
("yes", "Yes"),
("no", "No"),
],
required=True,
default="yes",
tracking=True,
)
practice_status = fields.Selection(
selection=[("yes", "Yes"), ("no_contact", "Yes, no contact"), ("no", "No")],
tracking=True,
required=True,
default="yes",
)
injury_ids = fields.One2many(
comodel_name="sports.patient.injury",
inverse_name="patient_id",
string="Injuries",
)
injured_since = fields.Date(compute="_compute_is_injured")
predicted_return_date = fields.Date(tracking=True)
return_date = fields.Date(
tracking=True,
help="When the player was cleared by medical staff to " "return to match play.",
)
is_injured = fields.Boolean(compute="_compute_is_injured")
stage = fields.Selection(
selection=[
("no_play", "Injured"),
("practice_ok", "Practice OK"),
("healthy", "Play OK"),
],
compute="_compute_stage",
)
last_consultation_date = fields.Date(tracking=True)
active_injury_count = fields.Integer(compute="_compute_active_injury_count")
allergies = fields.Text()
team_info_notes = fields.Html(
string="Notes",
tracking=True,
)
def default_get(self, fields_list):
res = super().default_get(fields_list)
if (
"team_ids" in fields_list
and "params" in self.env.context
and self.env.context.get("params")["model"] == "sports.team"
):
team = self.env["sports.team"].browse(self.env.context.get("params")["id"])
team_ids = [Command.set([team.id])]
if team_ids:
res.update({"team_ids": team_ids})
return res
def write(self, values):
res = super().write(values)
if "team_ids" in values:
self.sudo().recompute_followers()
return res
@api.model_create_multi
def create(self, vals_list):
for row in vals_list:
if "partner_id" not in row:
row["partner_id"] = (
self.env["res.partner"]
.create(
{
"name": self._get_name_from_first_and_last(
row["first_name"], row["last_name"]
)
}
)
.id
)
res = super().create(vals_list)
res.sudo().recompute_followers()
return res
@api.constrains("match_status", "practice_status")
def constrain_match_and_practice_status(self):
"""Avoid invalid combinations of match and practice status:
- Yes (match), No (practice)
- Yes (match), No Contact (practice)
"""
# combinations of (match_status, practice_status) that are valid
valid_combinations = [
("yes", "yes"),
("no", "yes"),
("no", "no_contact"),
("no", "no"),
]
for rec in self:
if (rec.match_status, rec.practice_status) not in valid_combinations:
raise ValidationError(
_("Invalid combination of match and practice status.")
)
@api.depends("injury_ids.stage")
def _compute_active_injury_count(self):
for rec in self:
rec.active_injury_count = len(
rec.injury_ids.filtered(lambda r: r.stage == "active")
)
@api.depends("match_status", "practice_status")
def _compute_stage(self):
stage_map = {
("yes", "yes"): "healthy",
("no", "yes"): "practice_ok",
("no", "no_contact"): "practice_ok",
("no", "no"): "no_play",
}
for rec in self:
# not a valid combination, will be caught by constraint if save is attempted
if (rec.match_status, rec.practice_status) not in stage_map:
rec.stage = False
continue
rec.stage = stage_map[(rec.match_status, rec.practice_status)]
@api.depends("date_of_birth")
def _compute_age(self):
for rec in self:
if not rec.date_of_birth:
rec.age = False
else:
rec.age = relativedelta(date.today(), rec.date_of_birth).years
@api.depends("first_name", "last_name")
def _compute_name(self):
for rec in self:
rec.name = self._get_name_from_first_and_last(rec.first_name, rec.last_name)
@api.model
def _get_name_from_first_and_last(self, first_name, last_name):
return ((first_name or "") + " " + (last_name or "")).strip()
@api.depends("practice_status", "match_status", "injury_ids.injury_date")
def _compute_is_injured(self):
for rec in self:
rec.is_injured = rec.practice_status != "yes" or rec.match_status != "yes"
if rec.is_injured:
unresolved_injuries = rec.injury_ids.filtered(
lambda r: not r.stage == "resolved"
)
rec.injured_since = (
unresolved_injuries and unresolved_injuries[0].injury_date
)
else:
rec.injured_since = False
def action_view_patient_form(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "sports.patient",
"res_id": self.id,
"context": self._context,
}
def action_consulted_today(self):
self.ensure_one() # should just be called from form view
self.last_consultation_date = date.today()
return {
"view_mode": "form",
"res_model": "sports.patient",
"context": self._context,
"res_id": self.id,
}
@api.onchange("mobile", "country_id")
def _onchange_mobile_validation(self):
if self.mobile:
self.mobile = self._phone_format(self.mobile, force_format="INTERNATIONAL")
@api.onchange("phone", "country_id")
def _onchange_phone_validation(self):
if self.phone:
self.phone = self._phone_format(self.phone, force_format="INTERNATIONAL")
def _phone_format(self, number, force_format="E164"):
country = self.country_id or self.env.company.country_id
if not country or not number:
return number
return phone_validation.phone_format(
number,
country.code if country else None,
country.phone_code if country else None,
force_format=force_format,
raise_exception=False,
)
def _track_subtype(self, init_values):
return self.env.ref("mail.mt_note")
def _track_template(self, changes):
res = super()._track_template(changes)
params = set(changes)
external = bool(external_tracking_fields & params)
if external:
first_external_field = (external_tracking_fields & params).pop()
res[first_external_field] = (
self.env.ref(
"bemade_sports_clinic.mail_template_patient_status_update"
),
{
"auto_delete_message": False,
"subtype_id": self.env.ref(
"bemade_sports_clinic.subtype_patient_external_update"
).id,
"email_layout_xmlid": "mail.mail_notification_light",
},
)
if "team_info_notes" in changes:
res["team_info_notes"] = (
self.env.ref(
"bemade_sports_clinic.mail_template_patient_new_internal_note"
),
{
"auto_delete_message": False,
"subtype_id": self.env.ref(
"bemade_sports_clinic.subtype_patient_internal_update"
).id,
"email_layout_xmlid": "mail.mail_notification_light",
},
)
return res
def recompute_followers(self):
"""Recompute the followers for this patient (and its injuries) based on the
changes to a specific team's staff members. Ignoring manually unsubscribed
followers, the set of followers should be the set of staff on all teams the
patient is part of."""
for patient in self:
current_followers = patient.message_partner_ids
future_followers = patient.team_ids.mapped("staff_ids").mapped("partner_id")
removed_followers = current_followers - future_followers
if removed_followers:
_logger.debug(f"{self} unsubscribing {removed_followers}")
patient.message_unsubscribe(removed_followers.ids)
patient.injury_ids.message_unsubscribe(removed_followers.ids)
if future_followers:
_logger.debug(f"{self} subscribing {future_followers}")
patient.message_subscribe(future_followers.ids)
patient.injury_ids.message_subscribe(future_followers.ids)