bemade-addons/bemade_sports_clinic/models/sports_team.py
2025-08-10 13:50:38 -04:00

595 lines
26 KiB
Python

from odoo import models, fields, api, _, Command
from odoo.exceptions import ValidationError
class SportsTeam(models.Model):
_name = "sports.team"
_description = "Sports Team"
_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",
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")
activity_count = fields.Integer(compute="_compute_activity_count")
parent_id = fields.Many2one(
comodel_name="res.partner",
string="Parent Organization",
ondelete="restrict",
tracking=True,
)
staff_ids = fields.One2many(
comodel_name="sports.team.staff",
inverse_name="team_id",
tracking=True,
)
head_coach_id = fields.Many2one(
comodel_name="res.partner",
compute="_compute_head_coach",
store=True,
)
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",
store=True,
string="Head Therapist",
)
head_therapist_name = fields.Char(
related="head_therapist_id.name",
string="Head Therapist Name",
)
website = fields.Char()
allowed_user_ids = fields.Many2many(
comodel_name="res.users",
compute="_compute_allowed_user_ids",
inverse="_inverse_allowed_user_ids",
)
def write(self, vals):
previous_patient_ids = self.sudo().patient_ids
res = super().write(vals)
if "staff_ids" in vals or "patient_ids" in vals:
(self.sudo().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]:
rec.sudo().patient_ids.recompute_followers()
return res
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")
def _compute_head_coach(self):
for rec in self:
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")
def _compute_head_therapist(self):
for rec in self:
staff = rec.staff_ids.filtered(lambda r: r.role == "head_therapist")
rec.head_therapist_id = staff.partner_id if staff else False
def _compute_activity_count(self):
for rec in self:
rec.activity_count = self.env['mail.activity'].search_count([
('res_model', '=', 'sports.team'),
('res_id', '=', rec.id)
])
def _compute_allowed_user_ids(self):
for rec in self:
rec.allowed_user_ids = rec.staff_ids.user_ids
def _inverse_allowed_user_ids(self):
for rec in self:
removed_staff = rec.staff_ids.filtered(
lambda staff: staff.user_ids not in rec.allowed_user_ids
)
added_users = rec.allowed_user_ids - rec.staff_ids.user_ids
removed_staff.unlink()
self.env["sports.team.staff"].create(
[
{
"team_id": rec.id,
"partner_id": user.partner_id.id,
"role": "other",
}
for user in added_users
]
)
def remove_access(self, user):
self.staff_ids.filtered(lambda staff: user in staff.user_ids).unlink()
class TeamStaff(models.Model):
_name = "sports.team.staff"
_description = "Sports Team Staff"
sequence = fields.Integer()
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",
)
active = fields.Boolean(related="partner_id.active")
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.",
)
]
@api.constrains("role")
def _constrain_role(self):
teams = self.mapped("team_id")
for team in teams:
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:
raise ValidationError(_("A team can have only one head therapist."))
@api.onchange("mobile")
def _onchange_mobile_validation(self):
import logging
_logger = logging.getLogger(__name__)
_logger.info(f"Mobile validation triggered. mobile={self.mobile}, partner_id={self.partner_id}")
# Check for phone formatting dependencies
try:
import phonenumbers
_logger.info(f"phonenumbers library available: {phonenumbers.__version__}")
# Test phonenumbers library directly
try:
test_number = phonenumbers.parse(self.mobile, "CA")
_logger.info(f"phonenumbers.parse result: {test_number}")
formatted_direct = phonenumbers.format_number(test_number, phonenumbers.PhoneNumberFormat.INTERNATIONAL)
_logger.info(f"Direct phonenumbers formatting result: {formatted_direct}")
except Exception as e:
_logger.error(f"Direct phonenumbers test failed: {e}")
except ImportError as e:
_logger.error(f"phonenumbers library not available: {e}")
return
# Check if Odoo's phone formatting method exists and is callable
if not hasattr(self.partner_id, '_phone_format'):
_logger.error("Partner does not have _phone_format method")
return
_logger.info(f"_phone_format method available: {callable(self.partner_id._phone_format)}")
if self.mobile and self.partner_id:
try:
_logger.info(f"Attempting to format mobile: {self.mobile}")
country_code = self.partner_id.country_id.code if self.partner_id.country_id else None
_logger.info(f"Partner country code: {country_code}")
# Since Odoo's _phone_format is broken, use phonenumbers library directly
if country_code:
try:
import phonenumbers
parsed_number = phonenumbers.parse(self.mobile, country_code)
if phonenumbers.is_valid_number(parsed_number):
formatted_mobile = phonenumbers.format_number(
parsed_number, phonenumbers.PhoneNumberFormat.INTERNATIONAL
)
_logger.info(f"Direct phonenumbers formatting successful: {formatted_mobile}")
self.mobile = formatted_mobile
else:
_logger.warning(f"Invalid phone number: {self.mobile}")
except Exception as e:
_logger.error(f"Direct phonenumbers formatting failed: {e}")
else:
_logger.warning(f"No country code available for formatting")
except Exception as e:
# If formatting fails, keep the original value
_logger.error(f"Mobile formatting failed: {e}")
else:
_logger.info(f"Skipping validation - mobile: {bool(self.mobile)}, partner_id: {bool(self.partner_id)}")
@api.depends("user_ids", "user_ids.groups_id")
def _compute_has_portal_access(self):
for rec in self:
# Check if the partner has any active users with portal or internal access
# Use direct group membership check instead of has_group() to avoid security violations
portal_group = self.env.ref('base.group_portal')
user_group = self.env.ref('base.group_user')
rec.has_portal_access = (
bool(rec.user_ids.filtered(lambda r: portal_group in r.groups_id))
or bool(rec.user_ids.filtered(lambda r: user_group in r.groups_id))
)
def action_revoke_portal_access(self):
"""Public method to revoke portal access with proper permission checks."""
# Check permissions - only admins and system users can revoke portal access
if not (self.env.user.has_group('bemade_sports_clinic.group_sports_clinic_admin') or
self.env.user.has_group('base.group_system')):
raise AccessError(_("You don't have permission to revoke portal access"))
# Call the private implementation
return self._action_revoke_portal_access()
def _action_revoke_portal_access(self):
"""Private method containing the actual sudo operations for revoking portal access."""
group_public = self.env.ref("base.group_public")
# Deactivate the user and set to public user type (standard Odoo approach)
if self.user_ids:
self.user_ids.sudo().write(
{
"groups_id": [(6, 0, [group_public.id])], # Set to public user only
"active": False,
}
)
# If there's an active signup invitation, cancel it
# This uses the portal.wizard from Odoo core to handle all the details
if self.partner_id and self.has_portal_access:
self.env['res.partner'].sudo().invalidate_model(['signup_valid'])
users = self.env['res.users'].sudo().search([('partner_id', '=', self.partner_id.id)])
users.write({'active': False})
def action_grant_portal_access(self):
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)
# Update all portal group memberships for new records
res._update_all_portal_groups()
# Update group membership based on staff roles
affected_users = res.mapped('user_ids')
if affected_users:
for user in affected_users.sudo():
res._update_all_portal_groups(user)
# Handle follower recomputation
res.team_id.mapped("patient_ids").recompute_followers()
return res
def unlink(self):
# Store affected partners and users before deletion
affected_partners = self.mapped('partner_id')
affected_users = self.mapped('user_ids')
# Standard processing for follower recomputation
patients = self.team_id.mapped("patient_ids")
res = super().unlink()
patients.recompute_followers()
# After deletion, update group memberships for all affected users
# Use a new empty recordset to avoid using the deleted recordset
empty_staff = self.env['sports.team.staff']
if affected_users:
for user in affected_users.sudo():
# Use the comprehensive group update method for each affected user
empty_staff._update_all_portal_groups(user)
return res
def _has_therapist_role(self):
"""Check if the staff member has a therapist role.
Returns:
bool: True if the staff member has a therapist role, False otherwise.
"""
return self.role in {'head_therapist', 'therapist'}
def _has_coach_role(self):
"""Check if the staff member has a coach role.
Returns:
bool: True if the staff member has a coach role, False otherwise.
"""
return self.role in {'head_coach', 'coach'}
def _get_treatment_professional_group(self):
"""Get the treatment professional security group.
Returns:
record: The treatment professional security group record.
"""
return self.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional')
def _get_portal_coach_group(self):
"""Get the portal team coach security group.
Returns:
record: The portal team coach security group record.
"""
return self.env.ref('bemade_sports_clinic.group_portal_team_coach')
def _update_user_group_membership(self, user, should_have_access, treatment_prof_group):
"""Update group membership for a single user.
Args:
user (res.users): The user to update
should_have_access (bool): Whether the user should have treatment professional access
treatment_prof_group (res.groups): The treatment professional group
"""
# Use direct group membership check instead of has_group() to avoid security violations
has_access = treatment_prof_group in user.groups_id
if should_have_access and not has_access:
user.sudo().write({'groups_id': [(4, treatment_prof_group.id)]})
elif not should_have_access and has_access:
user.sudo().write({'groups_id': [(3, treatment_prof_group.id)]})
def _get_staff_with_therapist_roles(self, partner_id):
"""Get all staff records with therapist roles for a partner.
Args:
partner_id (int): ID of the partner to check
Returns:
recordset: Staff records with therapist roles
"""
return self.env['sports.team.staff'].sudo().search([
('partner_id', '=', partner_id),
('role', 'in', ['head_therapist', 'therapist', 'doctor'])
])
def _get_staff_with_coach_roles(self, partner_id):
"""Get all staff records with coach roles for a partner.
Args:
partner_id (int): ID of the partner to check
Returns:
recordset: Staff records with coach roles
"""
return self.env['sports.team.staff'].sudo().search([
('partner_id', '=', partner_id),
('role', 'in', ['head_coach', 'coach'])
])
def update_all_treatment_professional_groups(self):
"""Manual method to update treatment professional group assignments for all staff.
This can be called to refresh group assignments after role changes or system updates.
Useful for fixing cases where staff members were added but not assigned to correct groups.
Updated to use comprehensive group assignment that handles both therapist and coach roles.
"""
all_staff = self.env['sports.team.staff'].search([])
all_staff._update_all_portal_groups()
return True
def update_all_portal_groups(self):
"""Manual method to update all portal group assignments for all staff.
This can be called to refresh group assignments after role changes or system updates.
Useful for fixing cases where staff members were added but not assigned to correct groups.
"""
all_staff = self.env['sports.team.staff'].search([])
all_staff._update_all_portal_groups()
return True
def _update_all_portal_groups(self, specific_user=None):
"""Update all portal group assignments based on staff roles.
This method ensures that users have the appropriate portal group memberships
based on their roles. It handles both therapist and coach roles.
Args:
specific_user (res.users, optional): If provided, only update this specific user.
Otherwise, update all users for the staff records.
"""
# Skip if in module installation context to avoid demo data conflicts
if self.env.context.get('module'):
return
treatment_prof_group = self._get_treatment_professional_group()
portal_treatment_prof_group = self.env.ref('bemade_sports_clinic.group_portal_treatment_professional')
portal_coach_group = self._get_portal_coach_group()
portal_group = self.env.ref('base.group_portal')
user_group = self.env.ref('base.group_user')
if specific_user:
# Process only the specific user
has_therapist_role = bool(self._get_staff_with_therapist_roles(specific_user.partner_id.id))
has_coach_role = bool(self._get_staff_with_coach_roles(specific_user.partner_id.id))
# Handle internal users
if user_group in specific_user.groups_id:
# Internal users get treatment professional group for therapist roles
if has_therapist_role and treatment_prof_group not in specific_user.groups_id:
specific_user.sudo().write({'groups_id': [(4, treatment_prof_group.id)]})
elif not has_therapist_role and treatment_prof_group in specific_user.groups_id:
specific_user.sudo().write({'groups_id': [(3, treatment_prof_group.id)]})
# Handle portal users
elif portal_group in specific_user.groups_id:
# Portal users get appropriate portal groups
if has_therapist_role and portal_treatment_prof_group not in specific_user.groups_id:
specific_user.sudo().write({'groups_id': [(4, portal_treatment_prof_group.id)]})
elif not has_therapist_role and portal_treatment_prof_group in specific_user.groups_id:
specific_user.sudo().write({'groups_id': [(3, portal_treatment_prof_group.id)]})
if has_coach_role and portal_coach_group not in specific_user.groups_id:
specific_user.sudo().write({'groups_id': [(4, portal_coach_group.id)]})
elif not has_coach_role and portal_coach_group in specific_user.groups_id:
specific_user.sudo().write({'groups_id': [(3, portal_coach_group.id)]})
return
# Process staff members with users if no specific user provided
for staff in self.filtered('user_ids'):
has_therapist_role = bool(self._get_staff_with_therapist_roles(staff.partner_id.id))
has_coach_role = bool(self._get_staff_with_coach_roles(staff.partner_id.id))
for user in staff.user_ids:
# Handle internal users
if user_group in user.groups_id:
# Internal users get treatment professional group for therapist roles
if has_therapist_role and treatment_prof_group not in user.groups_id:
user.sudo().write({'groups_id': [(4, treatment_prof_group.id)]})
elif not has_therapist_role and treatment_prof_group in user.groups_id:
user.sudo().write({'groups_id': [(3, treatment_prof_group.id)]})
# Handle portal users
elif portal_group in user.groups_id:
# Portal users get appropriate portal groups
if has_therapist_role and portal_treatment_prof_group not in user.groups_id:
user.sudo().write({'groups_id': [(4, portal_treatment_prof_group.id)]})
elif not has_therapist_role and portal_treatment_prof_group in user.groups_id:
user.sudo().write({'groups_id': [(3, portal_treatment_prof_group.id)]})
if has_coach_role and portal_coach_group not in user.groups_id:
user.sudo().write({'groups_id': [(4, portal_coach_group.id)]})
elif not has_coach_role and portal_coach_group in user.groups_id:
user.sudo().write({'groups_id': [(3, portal_coach_group.id)]})
def _update_treatment_professional_group(self, specific_user=None):
"""Update treatment professional status based on staff role.
This method ensures that users with therapist roles have the appropriate
group memberships. It handles both internal and portal users appropriately.
For internal users, this manages group membership directly.
For portal users, it manages the portal treatment professional group.
Args:
specific_user (res.users, optional): If provided, only update this specific user.
Otherwise, update all users for the staff records.
"""
# Skip if in module installation context to avoid demo data conflicts
if self.env.context.get('module'):
return
treatment_prof_group = self._get_treatment_professional_group()
portal_treatment_prof_group = self.env.ref('bemade_sports_clinic.group_portal_treatment_professional')
if specific_user:
# Process only the specific user
staff = self.filtered(lambda s: specific_user in s.user_ids)
if not staff:
# Check if the user has any therapist roles through their partner
has_therapist_role = bool(self._get_staff_with_therapist_roles(specific_user.partner_id.id))
users_to_process = specific_user
else:
# Check if this partner has any staff records with therapist roles
has_therapist_role = bool(self._get_staff_with_therapist_roles(specific_user.partner_id.id))
users_to_process = specific_user
# Apply the appropriate group membership
# Use direct group membership check instead of has_group() to avoid security violations
portal_group = self.env.ref('base.group_portal')
if portal_group not in users_to_process.groups_id:
# Internal user
if has_therapist_role and treatment_prof_group not in users_to_process.groups_id:
users_to_process.sudo().write({'groups_id': [(4, treatment_prof_group.id)]})
elif not has_therapist_role and treatment_prof_group in users_to_process.groups_id:
users_to_process.sudo().write({'groups_id': [(3, treatment_prof_group.id)]})
else:
# Portal user
if has_therapist_role and portal_treatment_prof_group not in users_to_process.groups_id:
users_to_process.sudo().write({'groups_id': [(4, portal_treatment_prof_group.id)]})
elif not has_therapist_role and portal_treatment_prof_group in users_to_process.groups_id:
users_to_process.sudo().write({'groups_id': [(3, portal_treatment_prof_group.id)]})
return
# Process staff members with users if no specific user provided
for staff in self.filtered('user_ids'):
# Check if this partner has any staff records with therapist roles
has_therapist_role = bool(self._get_staff_with_therapist_roles(staff.partner_id.id))
# Process each user linked to this partner
portal_group = self.env.ref('base.group_portal')
user_group = self.env.ref('base.group_user')
for user in staff.user_ids:
# Handle internal users
if user_group in user.groups_id:
self._update_user_group_membership(user, has_therapist_role, treatment_prof_group)
# Handle portal users
elif portal_group in user.groups_id:
if has_therapist_role and portal_treatment_prof_group not in user.groups_id:
user.sudo().write({'groups_id': [(4, portal_treatment_prof_group.id)]})
elif not has_therapist_role and portal_treatment_prof_group in user.groups_id:
user.sudo().write({'groups_id': [(3, portal_treatment_prof_group.id)]})
def write(self, values):
old_roles = {record.id: record.role for record in self}
result = super().write(values)
# If role changed or team changed, handle group membership updates
if 'role' in values or 'team_id' in values:
self._update_all_portal_groups()
# Handle team changes for follower recomputation
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
to_recompute.recompute_followers()
return result