From eda7f32c7aa11c025f227c7b516fae44e05ea605 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Tue, 27 Aug 2024 16:56:14 -0400 Subject: [PATCH] added tests and fixed code for patient and injury follower logic --- bemade_sports_clinic/models/patient.py | 293 +++++++++++------- bemade_sports_clinic/models/patient_injury.py | 6 + bemade_sports_clinic/models/res_partner.py | 40 ++- bemade_sports_clinic/models/sports_team.py | 206 ++++++++---- bemade_sports_clinic/tests/__init__.py | 1 + bemade_sports_clinic/tests/test_patient.py | 188 +++++++++++ 6 files changed, 534 insertions(+), 200 deletions(-) create mode 100644 bemade_sports_clinic/tests/__init__.py create mode 100644 bemade_sports_clinic/tests/test_patient.py diff --git a/bemade_sports_clinic/models/patient.py b/bemade_sports_clinic/models/patient.py index 2916ad8..c247e06 100644 --- a/bemade_sports_clinic/models/patient.py +++ b/bemade_sports_clinic/models/patient.py @@ -3,106 +3,113 @@ from odoo.exceptions import ValidationError from datetime import date from dateutil.relativedelta import relativedelta from odoo.addons.phone_validation.tools import phone_validation -from typing import Set, Tuple +import logging + +_logger = logging.getLogger(__name__) external_tracking_fields = { - 'last_consultation_date', - 'match_status', - 'practice_status', - 'predicted_return_date', - 'return_date', + "last_consultation_date", + "match_status", + "practice_status", + "predicted_return_date", + "return_date", } internal_tracking_fields = { - 'team_info_notes', - 'age', - 'date_of_birth', + "team_info_notes", + "age", + "date_of_birth", } class Patient(models.Model): - _name = 'sports.patient' + _name = "sports.patient" _description = "Patient" - _inherit = ['mail.thread', 'mail.activity.mixin'] - _order = 'last_name, first_name' + _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) + 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) + 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) + tracking=True, + ) age = fields.Integer( - compute='_compute_age', - groups="bemade_sports_clinic.group_sports_clinic_treatment_professional" + 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" + 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', + 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 + match_status = fields.Selection( + # Selection rather than bool for easy expansion later selection=[ - ('yes', 'Yes'), - ('no', 'No'), + ("yes", "Yes"), + ("no", "No"), ], required=True, - default='yes', - tracking=True) + default="yes", + tracking=True, + ) practice_status = fields.Selection( - selection=[ - ('yes', 'Yes'), - ('no_contact', 'Yes, no contact'), - ('no', 'No') - ], + selection=[("yes", "Yes"), ("no_contact", "Yes, no contact"), ("no", "No")], tracking=True, required=True, - default='yes', + default="yes", ) injury_ids = fields.One2many( - comodel_name='sports.patient.injury', - inverse_name='patient_id', - string='Injuries', - tracking=True, + comodel_name="sports.patient.injury", + inverse_name="patient_id", + string="Injuries", ) - injured_since = fields.Date(compute='_compute_is_injured') + 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." + 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') + ("no_play", "Injured"), + ("practice_ok", "Practice OK"), + ("healthy", "Play OK"), ], - compute='_compute_stage') + compute="_compute_stage", + ) 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() team_info_notes = fields.Html( string="Notes", @@ -111,55 +118,84 @@ class Patient(models.Model): 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']) + 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}) + 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 @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 - return super().create(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.recompute_followers() + return res - @api.constrains('match_status', 'practice_status') + @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) + """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')] + 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.")) + 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): 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): stage_map = { - ('yes', 'yes'): 'healthy', - ('no', 'yes'): 'practice_ok', - ('no', 'no_contact'): 'practice_ok', - ('no', 'no'): 'no_play', + ("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 # not a valid combination, will be caught by constraint if save is attempted + rec.stage = False continue rec.stage = stage_map[(rec.match_status, rec.practice_status)] - @api.depends('date_of_birth') + @api.depends("date_of_birth") def _compute_age(self): for rec in self: if not rec.date_of_birth: @@ -167,7 +203,7 @@ class Patient(models.Model): else: 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): for rec in self: 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): 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): 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: - unresolved_injuries = rec.injury_ids.filtered(lambda r: not r.stage == 'resolved') - rec.injured_since = unresolved_injuries and unresolved_injuries[0].injury_date + 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, + "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, + "view_mode": "form", + "res_model": "sports.patient", + "context": self._context, + "res_id": self.id, } - @api.onchange('mobile', 'country_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') + @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'): + 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 @@ -225,11 +265,11 @@ class Patient(models.Model): country.code if country else None, country.phone_code if country else None, force_format=force_format, - raise_exception=False + raise_exception=False, ) 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): res = super()._track_template(changes) @@ -238,33 +278,46 @@ class Patient(models.Model): 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', - } + 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', - } + 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 + """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') + future_followers = patient.team_ids.mapped("staff_ids").mapped("partner_id") removed_followers = current_followers - future_followers - added_followers = future_followers - current_followers - patient.message_unsubscribe(removed_followers.ids) - patient.message_subscribe(added_followers.ids) - patient.injury_ids.message_unsubscribe(removed_followers.ids) - patient.injury_ids.message_subscribe(added_followers.ids) \ No newline at end of file + 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) diff --git a/bemade_sports_clinic/models/patient_injury.py b/bemade_sports_clinic/models/patient_injury.py index 7a9fa2a..712833a 100644 --- a/bemade_sports_clinic/models/patient_injury.py +++ b/bemade_sports_clinic/models/patient_injury.py @@ -180,3 +180,9 @@ class PatientInjury(models.Model): }, ) return res + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + res.patient_id.recompute_followers() + return res diff --git a/bemade_sports_clinic/models/res_partner.py b/bemade_sports_clinic/models/res_partner.py index 6ff7a0d..2d7f4d1 100644 --- a/bemade_sports_clinic/models/res_partner.py +++ b/bemade_sports_clinic/models/res_partner.py @@ -4,25 +4,35 @@ from odoo.exceptions import ValidationError class Partner(models.Model): - _inherit = 'res.partner' + _inherit = "res.partner" - owned_team_ids = fields.One2many(comodel_name='sports.team', - inverse_name='parent_id') - staff_ids = fields.One2many(comodel_name='sports.team.staff', - inverse_name='team_id') - team_staff_rel_ids = fields.One2many(comodel_name='sports.team.staff', - inverse_name='partner_id', - string='Teams Served', - 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') + owned_team_ids = fields.One2many( + comodel_name="sports.team", inverse_name="parent_id" + ) + staff_ids = fields.One2many( + comodel_name="sports.team.staff", inverse_name="team_id" + ) + team_staff_rel_ids = fields.One2many( + comodel_name="sports.team.staff", + 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): - if self.patient_ids and 'name' in vals: - raise ValidationError(_("To change a patient's name, change it from the patient form.")) + if self.patient_ids and "name" in vals: + raise ValidationError( + _("To change a patient's name, change it from the patient form.") + ) 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): 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") diff --git a/bemade_sports_clinic/models/sports_team.py b/bemade_sports_clinic/models/sports_team.py index 452bd9e..8a0cb53 100644 --- a/bemade_sports_clinic/models/sports_team.py +++ b/bemade_sports_clinic/models/sports_team.py @@ -5,91 +5,107 @@ from odoo.exceptions import ValidationError class SportsTeam(models.Model): _name = "sports.team" _description = "Sports Team" - _inherit = ['mail.thread', 'mail.activity.mixin'] + _inherit = ["mail.thread", "mail.activity.mixin"] name = fields.Char() patient_ids = fields.Many2many( - comodel_name='sports.patient', - relation='sports_team_patient_rel', - column1='team_id', - column2='patient_id', - string='Players', + comodel_name="sports.patient", + relation="sports_team_patient_rel", + column1="team_id", + column2="patient_id", + string="Players", tracking=True, ) player_count = fields.Integer(compute="_compute_player_counts") injured_count = fields.Integer(compute="_compute_player_counts") healthy_count = fields.Integer(compute="_compute_player_counts") parent_id = fields.Many2one( - comodel_name='res.partner', - string='Parent Organization', - ondelete='restrict', + comodel_name="res.partner", + string="Parent Organization", + ondelete="restrict", tracking=True, ) staff_ids = fields.One2many( - comodel_name='sports.team.staff', - inverse_name='team_id', + comodel_name="sports.team.staff", + inverse_name="team_id", tracking=True, ) head_coach_id = fields.Many2one( - comodel_name='res.partner', - compute='_compute_head_coach', + comodel_name="res.partner", + compute="_compute_head_coach", 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( - comodel_name='res.partner', - compute='_compute_head_therapist', + comodel_name="res.partner", + compute="_compute_head_therapist", 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() allowed_user_ids = fields.Many2many( - comodel_name='res.users', - relation='sports_team_res_users_rel', - column1='team_id', - column2='user_id', - string='Allowed Users', - domain=lambda self: [['groups_id', 'in', self.env.ref("base.group_user").ids]], + comodel_name="res.users", + relation="sports_team_res_users_rel", + column1="team_id", + column2="user_id", + string="Allowed Users", + domain=lambda self: [["groups_id", "in", self.env.ref("base.group_user").ids]], ) def write(self, vals): + previous_patient_ids = self.patient_ids 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.patient_ids.recompute_followers() + (self.patient_ids | previous_patient_ids).recompute_followers() return res @api.model_create_multi def create(self, vals_list): res = super().create(vals_list) 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.patient_ids.recompute_followers() 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): for rec in self: rec.player_count = len(rec.patient_ids) rec.injured_count = len(rec.patient_ids.filtered(lambda p: p.is_injured)) rec.healthy_count = rec.player_count - rec.injured_count - @api.depends('staff_ids.role') + @api.depends("staff_ids.role") def _compute_head_coach(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 - @api.depends('staff_ids.role') + @api.depends("staff_ids.role") def _compute_head_therapist(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 def _allow_access_for_staff_internal_users(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): @@ -97,56 +113,116 @@ class TeamStaff(models.Model): _description = "Sports Team Staff" sequence = fields.Integer() - team_id = fields.Many2one(comodel_name='sports.team', string='Team', required=True) - partner_id = fields.Many2one(comodel_name='res.partner', string='Staff Member', - required=True, domain=[('is_company', '=', False)]) - role = fields.Selection(selection=[ - ('head_coach', 'Head Coach'), - ('head_therapist', 'Head Therapist'), - ('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) + team_id = fields.Many2one( + comodel_name="sports.team", + string="Team", + required=True, + ondelete="cascade", + ) + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Staff Member", + required=True, + domain=[("is_company", "=", False)], + ondelete="cascade", + ) + role = fields.Selection( + selection=[ + ("head_coach", "Head Coach"), + ("head_therapist", "Head Therapist"), + ("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)', - 'Each partner can only be related to a given team once.')] + _sql_constraints = [ + ( + "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): - teams = self.mapped('team_id') + teams = self.mapped("team_id") 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.")) - 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.")) - @api.onchange('mobile') + @api.onchange("mobile") def _onchange_mobile_validation(self): 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): for rec in self: - rec.has_portal_access = 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) + rec.has_portal_access = ( + 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): - group_portal = self.env.ref('base.group_portal') - group_public = self.env.ref('base.group_public') + group_portal = self.env.ref("base.group_portal") + group_public = self.env.ref("base.group_public") 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 self.partner_id.sudo().signup_token = False 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() + + @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) diff --git a/bemade_sports_clinic/tests/__init__.py b/bemade_sports_clinic/tests/__init__.py new file mode 100644 index 0000000..145e1df --- /dev/null +++ b/bemade_sports_clinic/tests/__init__.py @@ -0,0 +1 @@ +from . import test_patient diff --git a/bemade_sports_clinic/tests/test_patient.py b/bemade_sports_clinic/tests/test_patient.py new file mode 100644 index 0000000..67cbc41 --- /dev/null +++ b/bemade_sports_clinic/tests/test_patient.py @@ -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