added tests and fixed code for patient and injury follower logic

This commit is contained in:
Marc Durepos 2024-08-27 16:56:14 -04:00
parent 03227bb5f4
commit eda7f32c7a
6 changed files with 534 additions and 200 deletions

View file

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

View file

@ -180,3 +180,9 @@ class PatientInjury(models.Model):
}, },
) )
return res return res
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
res.patient_id.recompute_followers()
return res

View file

@ -4,25 +4,35 @@ from odoo.exceptions import ValidationError
class Partner(models.Model): class Partner(models.Model):
_inherit = 'res.partner' _inherit = "res.partner"
owned_team_ids = fields.One2many(comodel_name='sports.team', owned_team_ids = fields.One2many(
inverse_name='parent_id') comodel_name="sports.team", inverse_name="parent_id"
staff_ids = fields.One2many(comodel_name='sports.team.staff', )
inverse_name='team_id') staff_ids = fields.One2many(
team_staff_rel_ids = fields.One2many(comodel_name='sports.team.staff', comodel_name="sports.team.staff", inverse_name="team_id"
inverse_name='partner_id', )
string='Teams Served', team_staff_rel_ids = fields.One2many(
help='The teams this person works for.') comodel_name="sports.team.staff",
teams_served_ids = fields.One2many(comodel_name='sports.team', compute='_compute_teams_served') inverse_name="partner_id",
patient_ids = fields.One2many(comodel_name='sports.patient', inverse_name='partner_id') string="Employer(s)",
help="The teams this person works for.",
)
teams_served_ids = fields.One2many(
comodel_name="sports.team", compute="_compute_teams_served"
)
patient_ids = fields.One2many(
comodel_name="sports.patient", inverse_name="partner_id"
)
def write(self, vals): def write(self, vals):
if self.patient_ids and 'name' in vals: if self.patient_ids and "name" in vals:
raise ValidationError(_("To change a patient's name, change it from the patient form.")) raise ValidationError(
_("To change a patient's name, change it from the patient form.")
)
return super().write(vals) return super().write(vals)
@api.depends('team_staff_rel_ids.team_id') @api.depends("team_staff_rel_ids.team_id")
def _compute_teams_served(self): def _compute_teams_served(self):
for rec in self: for rec in self:
rec.teams_served_ids = rec.team_staff_rel_ids.mapped('team_id') rec.teams_served_ids = rec.team_staff_rel_ids.mapped("team_id")

View file

@ -5,91 +5,107 @@ from odoo.exceptions import ValidationError
class SportsTeam(models.Model): class SportsTeam(models.Model):
_name = "sports.team" _name = "sports.team"
_description = "Sports Team" _description = "Sports Team"
_inherit = ['mail.thread', 'mail.activity.mixin'] _inherit = ["mail.thread", "mail.activity.mixin"]
name = fields.Char() name = fields.Char()
patient_ids = fields.Many2many( patient_ids = fields.Many2many(
comodel_name='sports.patient', comodel_name="sports.patient",
relation='sports_team_patient_rel', relation="sports_team_patient_rel",
column1='team_id', column1="team_id",
column2='patient_id', column2="patient_id",
string='Players', string="Players",
tracking=True, tracking=True,
) )
player_count = fields.Integer(compute="_compute_player_counts") player_count = fields.Integer(compute="_compute_player_counts")
injured_count = fields.Integer(compute="_compute_player_counts") injured_count = fields.Integer(compute="_compute_player_counts")
healthy_count = fields.Integer(compute="_compute_player_counts") healthy_count = fields.Integer(compute="_compute_player_counts")
parent_id = fields.Many2one( parent_id = fields.Many2one(
comodel_name='res.partner', comodel_name="res.partner",
string='Parent Organization', string="Parent Organization",
ondelete='restrict', ondelete="restrict",
tracking=True, tracking=True,
) )
staff_ids = fields.One2many( staff_ids = fields.One2many(
comodel_name='sports.team.staff', comodel_name="sports.team.staff",
inverse_name='team_id', inverse_name="team_id",
tracking=True, tracking=True,
) )
head_coach_id = fields.Many2one( head_coach_id = fields.Many2one(
comodel_name='res.partner', comodel_name="res.partner",
compute='_compute_head_coach', compute="_compute_head_coach",
store=True, store=True,
) )
head_coach_name = fields.Char(related='head_coach_id.name') head_coach_name = fields.Char(
related="head_coach_id.name",
string="Head Coach Name",
)
head_therapist_id = fields.Many2one( head_therapist_id = fields.Many2one(
comodel_name='res.partner', comodel_name="res.partner",
compute='_compute_head_therapist', compute="_compute_head_therapist",
store=True, store=True,
string="Head Therapist",
)
head_therapist_name = fields.Char(
related="head_therapist_id.name",
string="Head Therapist Name",
) )
head_therapist_name = fields.Char(related='head_therapist_id.name')
website = fields.Char() website = fields.Char()
allowed_user_ids = fields.Many2many( allowed_user_ids = fields.Many2many(
comodel_name='res.users', comodel_name="res.users",
relation='sports_team_res_users_rel', relation="sports_team_res_users_rel",
column1='team_id', column1="team_id",
column2='user_id', column2="user_id",
string='Allowed Users', string="Allowed Users",
domain=lambda self: [['groups_id', 'in', self.env.ref("base.group_user").ids]], domain=lambda self: [["groups_id", "in", self.env.ref("base.group_user").ids]],
) )
def write(self, vals): def write(self, vals):
previous_patient_ids = self.patient_ids
res = super().write(vals) res = super().write(vals)
if 'staff_ids' in vals: if "staff_ids" in vals or "patient_ids" in vals:
self._allow_access_for_staff_internal_users() self._allow_access_for_staff_internal_users()
self.patient_ids.recompute_followers() (self.patient_ids | previous_patient_ids).recompute_followers()
return res return res
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):
res = super().create(vals_list) res = super().create(vals_list)
for index, rec in enumerate(res): for index, rec in enumerate(res):
if 'staff_ids' in vals_list[index]: if "staff_ids" in vals_list[index]:
rec._allow_access_for_staff_internal_users() rec._allow_access_for_staff_internal_users()
rec.patient_ids.recompute_followers() rec.patient_ids.recompute_followers()
return res return res
@api.depends('patient_ids.is_injured') def unlink(self):
to_recompute = self.patient_ids
res = super().unlink()
to_recompute.recompute_followers()
return res
@api.depends("patient_ids.is_injured")
def _compute_player_counts(self): def _compute_player_counts(self):
for rec in self: for rec in self:
rec.player_count = len(rec.patient_ids) rec.player_count = len(rec.patient_ids)
rec.injured_count = len(rec.patient_ids.filtered(lambda p: p.is_injured)) rec.injured_count = len(rec.patient_ids.filtered(lambda p: p.is_injured))
rec.healthy_count = rec.player_count - rec.injured_count rec.healthy_count = rec.player_count - rec.injured_count
@api.depends('staff_ids.role') @api.depends("staff_ids.role")
def _compute_head_coach(self): def _compute_head_coach(self):
for rec in self: for rec in self:
staff = rec.staff_ids.filtered(lambda r: r.role == 'head_coach') staff = rec.staff_ids.filtered(lambda r: r.role == "head_coach")
rec.head_coach_id = staff.partner_id if staff else False rec.head_coach_id = staff.partner_id if staff else False
@api.depends('staff_ids.role') @api.depends("staff_ids.role")
def _compute_head_therapist(self): def _compute_head_therapist(self):
for rec in self: for rec in self:
staff = rec.staff_ids.filtered(lambda r: r.role == 'head_therapist') staff = rec.staff_ids.filtered(lambda r: r.role == "head_therapist")
rec.head_therapist_id = staff.partner_id if staff else False rec.head_therapist_id = staff.partner_id if staff else False
def _allow_access_for_staff_internal_users(self): def _allow_access_for_staff_internal_users(self):
for rec in self: for rec in self:
rec.allowed_user_ids |= rec.staff_ids.user_ids.filtered(lambda user: user.has_group("base.group_user")) rec.allowed_user_ids |= rec.staff_ids.user_ids.filtered(
lambda user: user.has_group("base.group_user")
)
class TeamStaff(models.Model): class TeamStaff(models.Model):
@ -97,56 +113,116 @@ class TeamStaff(models.Model):
_description = "Sports Team Staff" _description = "Sports Team Staff"
sequence = fields.Integer() sequence = fields.Integer()
team_id = fields.Many2one(comodel_name='sports.team', string='Team', required=True) team_id = fields.Many2one(
partner_id = fields.Many2one(comodel_name='res.partner', string='Staff Member', comodel_name="sports.team",
required=True, domain=[('is_company', '=', False)]) string="Team",
role = fields.Selection(selection=[ required=True,
('head_coach', 'Head Coach'), ondelete="cascade",
('head_therapist', 'Head Therapist'), )
('coach', 'Coach'), partner_id = fields.Many2one(
('therapist', 'Therapist'), comodel_name="res.partner",
('doctor', 'Doctor'), string="Staff Member",
('other', 'Other') required=True,
], required=True) domain=[("is_company", "=", False)],
mobile = fields.Char(related='partner_id.mobile', readonly=False) ondelete="cascade",
name = fields.Char(related='partner_id.name', readonly=False) )
parent_id = fields.Many2one(related='partner_id.parent_id', readonly=False, string="Organization", role = fields.Selection(
domain=[('is_company', '=', True)]) selection=[
email = fields.Char(related='partner_id.email', readonly=False) ("head_coach", "Head Coach"),
user_ids = fields.One2many(related='partner_id.user_ids', readonly=True) ("head_therapist", "Head Therapist"),
has_portal_access = fields.Boolean(compute='_compute_has_portal_access', compute_sudo=True) ("coach", "Coach"),
("therapist", "Therapist"),
("doctor", "Doctor"),
("other", "Other"),
],
required=True,
)
mobile = fields.Char(related="partner_id.mobile", readonly=False)
name = fields.Char(related="partner_id.name", readonly=False)
parent_id = fields.Many2one(
related="partner_id.parent_id",
readonly=False,
string="Organization",
domain=[("is_company", "=", True)],
)
email = fields.Char(related="partner_id.email", readonly=False)
user_ids = fields.One2many(related="partner_id.user_ids", readonly=True)
has_portal_access = fields.Boolean(
compute="_compute_has_portal_access", compute_sudo=True
)
_sql_constraints = [('team_staff_unique', 'unique(team_id, partner_id)', _sql_constraints = [
'Each partner can only be related to a given team once.')] (
"team_staff_unique",
"unique(team_id, partner_id)",
"Each partner can only be related to a given team once.",
)
]
@api.constrains('role') @api.constrains("role")
def _constrain_role(self): def _constrain_role(self):
teams = self.mapped('team_id') teams = self.mapped("team_id")
for team in teams: for team in teams:
if len(team.staff_ids.filtered(lambda r: r.role == 'head_coach')) > 1: if len(team.staff_ids.filtered(lambda r: r.role == "head_coach")) > 1:
raise ValidationError(_("A team can have only one head coach.")) raise ValidationError(_("A team can have only one head coach."))
if len(team.staff_ids.filtered(lambda r: r.role == 'head_therapist')) > 1: if len(team.staff_ids.filtered(lambda r: r.role == "head_therapist")) > 1:
raise ValidationError(_("A team can have only one head therapist.")) raise ValidationError(_("A team can have only one head therapist."))
@api.onchange('mobile') @api.onchange("mobile")
def _onchange_mobile_validation(self): def _onchange_mobile_validation(self):
if self.mobile: if self.mobile:
self.mobile = self.partner_id._phone_format(self.mobile, force_format='INTERNATIONAL') self.mobile = self.partner_id._phone_format(
self.mobile, force_format="INTERNATIONAL"
)
@api.depends('user_ids', 'user_ids.groups_id') @api.depends("user_ids", "user_ids.groups_id")
def _compute_has_portal_access(self): def _compute_has_portal_access(self):
for rec in self: for rec in self:
rec.has_portal_access = bool(rec.user_ids.filtered(lambda r: r.has_group('base.group_portal'))) or bool( rec.has_portal_access = (
rec.user_ids.filtered(lambda r: r.has_group('base.group_user'))) or bool(rec.partner_id.signup_token) bool(rec.user_ids.filtered(lambda r: r.has_group("base.group_portal")))
or bool(rec.user_ids.filtered(lambda r: r.has_group("base.group_user")))
or bool(rec.partner_id.signup_token)
)
def action_revoke_portal_access(self): def action_revoke_portal_access(self):
group_portal = self.env.ref('base.group_portal') group_portal = self.env.ref("base.group_portal")
group_public = self.env.ref('base.group_public') group_public = self.env.ref("base.group_public")
self.user_ids.write( self.user_ids.write(
{'groups_id': [Command.unlink(group_portal.id), Command.link(group_public.id)], 'active': False}) {
"groups_id": [
Command.unlink(group_portal.id),
Command.link(group_public.id),
],
"active": False,
}
)
# Remove the signup token, so it cannot be used # Remove the signup token, so it cannot be used
self.partner_id.sudo().signup_token = False self.partner_id.sudo().signup_token = False
def action_grant_portal_access(self): def action_grant_portal_access(self):
wiz = self.env['portal.wizard'].create({'partner_ids': [(4, self.partner_id.id)]}) wiz = self.env["portal.wizard"].create(
{"partner_ids": [(4, self.partner_id.id)]}
)
return wiz._action_open_modal() return wiz._action_open_modal()
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
res.team_id.mapped("patient_ids").recompute_followers()
return res
def unlink(self):
patients = self.team_id.mapped("patient_ids")
super().unlink()
patients.recompute_followers()
def write(self, values):
if "team_id" in values:
to_recompute = self.env["sports.patient"]
for rec in self:
if rec.team_id.id != values["team_id"]:
to_recompute |= rec.team_id.patient_ids
res = super().write(values)
to_recompute.recompute_followers()
return res
return super().write(values)

View file

@ -0,0 +1 @@
from . import test_patient

View file

@ -0,0 +1,188 @@
from odoo.tests import TransactionCase, tagged
from odoo import fields, Command
from datetime import timedelta
@tagged("-at_install", "post_install")
class TestPatient(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
organization = cls.env["res.partner"].create(
{"name": "Test Org"},
)
team1 = cls.env["sports.team"].create(
{
"name": "Test team",
"parent_id": organization.id,
}
)
coach = cls.env["sports.team.staff"].create(
{
"partner_id": cls.env["res.partner"]
.create(
{
"name": "Test Coach",
}
)
.id,
"team_id": team1.id,
"role": "head_coach",
}
)
patient1 = cls.env["sports.patient"].create(
{
"first_name": "Test",
"last_name": "Patient 1",
"team_ids": [Command.set(team1.ids)],
"date_of_birth": fields.Date.today() - timedelta(days=18 * 365),
}
)
patient1_injury = cls.env["sports.patient.injury"].create(
{
"patient_id": patient1.id,
}
)
patient2 = cls.env["sports.patient"].create(
{
"first_name": "Test",
"last_name": "Patient2",
"team_ids": [Command.set(team1.ids)],
"date_of_birth": fields.Date.today() - timedelta(days=21 * 365),
}
)
patient2_injury = cls.env["sports.patient.injury"].create(
{
"patient_id": patient2.id,
}
)
(
cls.organization,
cls.team1,
cls.coach,
cls.patient1,
cls.patient1_injury,
cls.patient2,
cls.patient2_injury,
) = (
organization,
team1,
coach,
patient1,
patient1_injury,
patient2,
patient2_injury,
)
def test_adding_staff_adds_follower_to_patient_and_injury(self):
therapist = self.env["sports.team.staff"].create(
{
"team_id": self.team1.id,
"partner_id": self.env["res.partner"]
.create(
{"name": "Tester"},
)
.id,
"role": "therapist",
}
)
therapist = therapist.partner_id
self.assertIn(therapist, self.patient1.message_partner_ids)
self.assertIn(therapist, self.patient2.message_partner_ids)
self.assertIn(therapist, self.patient1_injury.message_partner_ids)
self.assertIn(therapist, self.patient2_injury.message_partner_ids)
def test_removing_staff_removes_follower_from_patient_and_injury(self):
coach = self.coach.partner_id
self.team1.staff_ids = False
self.assertNotIn(coach, self.patient1.message_partner_ids)
self.assertNotIn(coach, self.patient1_injury.message_partner_ids)
self.assertNotIn(coach, self.patient2.message_partner_ids)
self.assertNotIn(coach, self.patient2_injury.message_partner_ids)
def test_deleting_team_removes_follower_from_patient_and_injury(self):
coach = self.coach.partner_id
self.team1.unlink()
self.assertNotIn(coach, self.patient1.message_partner_ids)
self.assertNotIn(coach, self.patient1_injury.message_partner_ids)
self.assertNotIn(coach, self.patient2.message_partner_ids)
self.assertNotIn(coach, self.patient2_injury.message_partner_ids)
def test_adding_second_team_subscribes_new_staff(self):
team2, therapist, coach = self._generate_second_team_and_staff()
team2.patient_ids = self.patient1
self.assertIn(therapist, self.patient1.message_partner_ids)
self.assertIn(coach, self.patient1.message_partner_ids)
self.assertIn(therapist, self.patient1_injury.message_partner_ids)
self.assertIn(coach, self.patient1_injury.message_partner_ids)
self.assertEqual(len(self.patient1_injury.message_partner_ids), 2)
self.assertEqual(len(self.patient1.message_partner_ids), 2)
def test_creating_patient_in_team_assigns_followers(self):
patient = self.env["sports.patient"].create(
{
"first_name": "Test",
"last_name": "Patient",
"date_of_birth": fields.Date.today() - timedelta(days=365 * 20),
"team_ids": [(6, 0, self.team1.ids)],
}
)
cp_id = self.coach.partner_id
self.assertIn(cp_id, patient.message_partner_ids)
injury = self.env["sports.patient.injury"].create(
{
"patient_id": patient.id,
"diagnosis": "Something",
}
)
self.assertIn(cp_id, injury.message_partner_ids)
def test_removing_second_team_correctly_adjusts_staff(self):
team2, therapist, coach = self._generate_second_team_and_staff()
self.patient1.write({"team_ids": [Command.link(team2.id)]})
self.assertIn(self.patient1, team2.patient_ids)
self.assertIn(therapist, self.patient1.message_partner_ids)
team2.write({"patient_ids": [Command.unlink(self.patient1.id)]})
self.assertNotIn(self.patient1, team2.patient_ids)
self.assertEqual(self.patient1.message_partner_ids, coach)
self.assertEqual(self.patient1_injury.message_partner_ids, coach)
def _generate_second_team_and_staff(self):
team2 = self.env["sports.team"].create(
{
"parent_id": self.organization.id,
"name": "Test team 2",
}
)
therapist = (
self.env["sports.team.staff"]
.create(
{
"team_id": team2.id,
"partner_id": self.env["res.partner"]
.create(
{"name": "Tester"},
)
.id,
"role": "therapist",
}
)
.partner_id
)
coach = (
self.env["sports.team.staff"]
.create(
{
"team_id": team2.id,
"partner_id": self.coach.partner_id.id,
"role": "coach",
}
)
.partner_id
)
return team2, therapist, coach