diff --git a/bemade_sports_clinic/__manifest__.py b/bemade_sports_clinic/__manifest__.py
index c4d7de0..e9b7644 100644
--- a/bemade_sports_clinic/__manifest__.py
+++ b/bemade_sports_clinic/__manifest__.py
@@ -32,6 +32,11 @@
External users (portal users) can be added to give coaches and other team
personnel access to limited player data such as estimated return-to-play dates.
+ Medical professionals (primarily field therapists) can also access the system
+ via the portal interface instead of requiring full internal user accounts, allowing
+ for more flexible collaboration while maintaining appropriate access controls.
+ Coaches and treatment professionals can signal player injuries directly through the
+ portal interface, streamlining communication and injury tracking.
""",
"category": "Services/Medical",
"author": "Bemade Inc.",
@@ -45,14 +50,18 @@
},
"data": [
"security/sports_clinic_groups.xml",
+ "security/sports_clinic_portal_groups.xml",
"security/ir.model.access.csv",
"security/sports_clinic_rules.xml",
+ "security/sports_clinic_portal_rules.xml",
"data/sports_clinic_data.xml",
+ "data/admin_access_data.xml",
"views/sports_team_views.xml",
"views/sports_clinic_menus.xml",
"views/sports_patient_injury_views.xml",
"views/sports_patient_views.xml",
"views/sports_clinic_portal_views.xml",
+ "views/sports_patient_injury_portal.xml",
"views/res_partner_views.xml",
"views/res_users_views.xml",
],
diff --git a/bemade_sports_clinic/controllers/__init__.py b/bemade_sports_clinic/controllers/__init__.py
index accb329..95a9e8e 100644
--- a/bemade_sports_clinic/controllers/__init__.py
+++ b/bemade_sports_clinic/controllers/__init__.py
@@ -1 +1,2 @@
from . import team_staff_portal
+from . import patient_injury_portal
diff --git a/bemade_sports_clinic/data/demo/sports_clinic_demo_data.xml b/bemade_sports_clinic/data/demo/sports_clinic_demo_data.xml
index 2b1dd30..99312d8 100644
--- a/bemade_sports_clinic/data/demo/sports_clinic_demo_data.xml
+++ b/bemade_sports_clinic/data/demo/sports_clinic_demo_data.xml
@@ -165,7 +165,10 @@
therapisttherapist
-
+
+
+
@@ -173,12 +176,7 @@
coach
-
-
-
-
-
-
+
diff --git a/bemade_sports_clinic/i18n/fr_CA.po b/bemade_sports_clinic/i18n/fr_CA.po
index a7fe0ed..9901aea 100644
--- a/bemade_sports_clinic/i18n/fr_CA.po
+++ b/bemade_sports_clinic/i18n/fr_CA.po
@@ -287,7 +287,7 @@ msgid "Date of Injury"
msgstr "Date de la blessure"
#. module: bemade_sports_clinic
-#: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.sports_patient_injury_view_tree
+#: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.sports_patient_injury_view_list
#: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.sports_patient_view_list_embedded
msgid "Details"
msgstr "Détails"
@@ -798,7 +798,7 @@ msgid "Patient Injury"
msgstr "La blessure d'un patient."
#. module: bemade_sports_clinic
-#: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.sports_patient_injury_view_tree
+#: model_terms:ir.ui.view,arch_db:bemade_sports_clinic.sports_patient_injury_view_list
msgid "Patient Injury History"
msgstr "Historique des blessures du patient"
diff --git a/bemade_sports_clinic/models/patient.py b/bemade_sports_clinic/models/patient.py
index 226ded8..76abce7 100644
--- a/bemade_sports_clinic/models/patient.py
+++ b/bemade_sports_clinic/models/patient.py
@@ -253,6 +253,40 @@ class Patient(models.Model):
"context": self._context,
"res_id": self.id,
}
+
+ def action_report_injury(self):
+ """Open the injury report form for this patient.
+ For portal users: redirects to the portal form
+ For backend users: opens a new injury form in the backend
+ """
+ self.ensure_one()
+
+ # Check if current user is a portal user
+ is_portal = self.env.user.has_group('base.group_portal')
+
+ if is_portal:
+ # Redirect to portal injury form
+ base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+ portal_url = f"{base_url}/my/patient/injury/new?patient_id={self.id}"
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': portal_url,
+ 'target': 'self',
+ }
+ else:
+ # Open backend injury form
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': f'Report Injury for {self.name}',
+ 'view_mode': 'form',
+ 'res_model': 'sports.patient.injury',
+ 'context': {
+ 'default_patient_id': self.id,
+ 'default_patient_name': self.name,
+ 'default_stage': 'active',
+ 'default_team_id': self.team_ids[0].id if self.team_ids else False
+ },
+ }
@api.onchange("mobile", "country_id")
def _onchange_mobile_validation(self):
diff --git a/bemade_sports_clinic/models/patient_contact.py b/bemade_sports_clinic/models/patient_contact.py
index 90420b4..a6a6d2f 100644
--- a/bemade_sports_clinic/models/patient_contact.py
+++ b/bemade_sports_clinic/models/patient_contact.py
@@ -5,7 +5,12 @@ from odoo.addons.phone_validation.tools import phone_validation
class PatientContact(models.Model):
_name = 'sports.patient.contact'
_description = "Emergency or other contacts for a patient."
-
+
+ @api.model
+ def _valid_field_parameter(self, field, name):
+ # Allow 'unaccent' parameter for fields
+ return name == 'unaccent' or super()._valid_field_parameter(field, name)
+
sequence = fields.Integer(required=True, default=0)
name = fields.Char(unaccent=True)
contact_type = fields.Selection(selection=[
diff --git a/bemade_sports_clinic/models/patient_injury.py b/bemade_sports_clinic/models/patient_injury.py
index c9c9c27..29e6c1e 100644
--- a/bemade_sports_clinic/models/patient_injury.py
+++ b/bemade_sports_clinic/models/patient_injury.py
@@ -42,6 +42,11 @@ class PatientInjury(models.Model):
ondelete="cascade",
)
patient_name = fields.Char(related="patient_id.name")
+ team_id = fields.Many2one(
+ comodel_name="sports.team",
+ string="Team",
+ help="The team for which this injury was reported, especially important when a player belongs to multiple teams.",
+ )
diagnosis = fields.Char(tracking=True)
injury_date = fields.Date(
@@ -171,5 +176,33 @@ class PatientInjury(models.Model):
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
- res.patient_id.recompute_followers()
+
+ for record in res:
+ # Automatically assign therapist when creating an injury
+ current_user = self.env.user
+
+ # If the injury creator is a treatment professional, assign them
+ if current_user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional'):
+ record.treatment_professional_ids = [(4, current_user.id)]
+ # Otherwise, if there's a team_id, find and assign team therapists
+ elif record.team_id:
+ # Find all team staff users
+ team_staff = self.env['sports.team.staff'].search([
+ ('team_id', '=', record.team_id.id)
+ ])
+
+ # Filter to only staff users who are treatment professionals
+ if team_staff:
+ treatment_professional_group = self.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional')
+ # Get all users from staff and filter them by group
+ staff_users = team_staff.mapped('user_ids')
+ therapist_users = staff_users.filtered(
+ lambda user: user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
+ )
+
+ if therapist_users:
+ record.treatment_professional_ids = [(6, 0, therapist_users.ids)]
+
+ record.patient_id.recompute_followers()
+
return res
diff --git a/bemade_sports_clinic/models/res_users.py b/bemade_sports_clinic/models/res_users.py
index 0fa392b..bfd0f63 100644
--- a/bemade_sports_clinic/models/res_users.py
+++ b/bemade_sports_clinic/models/res_users.py
@@ -14,12 +14,25 @@ class User(models.Model):
inverse="_inverse_accessible_team_ids",
)
- @api.depends("groups_id")
+ @api.depends("groups_id", "partner_id", "partner_id.staff_ids", "partner_id.staff_ids.role")
def _compute_is_treatment_professional(self):
for rec in self:
- rec.is_treatment_professional = rec.has_group(
+ # Check if user has the security group
+ has_security_group = rec.has_group(
"bemade_sports_clinic.group_sports_clinic_treatment_professional"
)
+
+ # Check if user is linked to any team staff as head therapist or therapist
+ is_therapist_staff = False
+ if rec.partner_id:
+ staff_records = self.env['sports.team.staff'].sudo().search([
+ ('partner_id', '=', rec.partner_id.id),
+ ('role', 'in', ['head_therapist', 'therapist'])
+ ])
+ is_therapist_staff = bool(staff_records)
+
+ # Set field based on either condition
+ rec.is_treatment_professional = has_security_group or is_therapist_staff
def _compute_accessible_team_ids(self):
for rec in self:
diff --git a/bemade_sports_clinic/models/sports_team.py b/bemade_sports_clinic/models/sports_team.py
index 91300bd..fa90632 100644
--- a/bemade_sports_clinic/models/sports_team.py
+++ b/bemade_sports_clinic/models/sports_team.py
@@ -193,26 +193,33 @@ class TeamStaff(models.Model):
@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
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")
- self.user_ids.write(
- {
- "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
+ # Deactivate the user and remove from portal group
+ if self.user_ids:
+ self.user_ids.write(
+ {
+ "groups_id": [
+ Command.unlink(group_portal.id),
+ Command.link(group_public.id),
+ ],
+ "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(
@@ -223,22 +230,110 @@ class TeamStaff(models.Model):
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
+
+ # Update treatment professional group membership for new records
+ res._update_treatment_professional_group()
+
+ # Recompute the is_treatment_professional field on affected users
+ affected_users = res.mapped('user_ids')
+ if affected_users:
+ affected_users.sudo()._compute_is_treatment_professional()
+
+ # 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')
+
+ # Store roles - only therapist roles matter for the update
+ had_therapist_role = self.filtered(lambda s: s.role in ['head_therapist', 'therapist'])
+ therapist_partners = had_therapist_role.mapped('partner_id')
+
+ # Standard processing for follower recomputation
patients = self.team_id.mapped("patient_ids")
res = super().unlink()
patients.recompute_followers()
+
+ # After deletion, check if therapist partners still have any therapist roles
+ # and update their group membership accordingly
+ for partner in therapist_partners:
+ remaining_therapist_roles = self.env['sports.team.staff'].sudo().search_count([
+ ('partner_id', '=', partner.id),
+ ('role', 'in', ['head_therapist', 'therapist']),
+ ])
+
+ if not remaining_therapist_roles:
+ # No therapist roles left, remove from treatment professional group
+ users = self.env['res.users'].sudo().search([('partner_id', '=', partner.id)])
+ treatment_prof_group = self.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional')
+ for user in users:
+ if user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional'):
+ user.sudo().write({'groups_id': [(3, treatment_prof_group.id)]})
+
+ # Recompute is_treatment_professional on all affected users
+ affected_users.sudo().invalidate_model(['is_treatment_professional'])
+
return res
+ def _update_treatment_professional_group(self):
+ """Update treatment professional status based on staff role
+
+ For internal users, this adds them to the treatment professional group.
+ For portal users, we set the flag via recomputation.
+ """
+ treatment_prof_group = self.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional')
+
+ for staff in self:
+ # Skip if partner has no user accounts
+ if not staff.user_ids:
+ continue
+
+ # Check if this partner has any staff records with therapist roles
+ all_staff_records = self.env['sports.team.staff'].sudo().search([
+ ('partner_id', '=', staff.partner_id.id),
+ ('role', 'in', ['head_therapist', 'therapist'])
+ ])
+
+ should_be_treatment_professional = bool(all_staff_records)
+
+ # Update all users linked to this partner
+ for user in staff.user_ids:
+ # Skip changes during module installation to avoid conflicts in demo data
+ if self.env.context.get('module'):
+ continue
+
+ # Always trigger a recomputation of is_treatment_professional
+ user.sudo().invalidate_model(['is_treatment_professional'])
+
+ # For internal users, we can directly manage group membership
+ if user.has_group('base.group_user'):
+ if should_be_treatment_professional and not user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional'):
+ user.sudo().write({'groups_id': [(4, treatment_prof_group.id)]}) # Add to group
+ elif not should_be_treatment_professional and user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional'):
+ user.sudo().write({'groups_id': [(3, treatment_prof_group.id)]}) # Remove from group
+
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_treatment_professional_group()
+
+ # Recompute the `is_treatment_professional` field on affected users
+ affected_users = self.mapped('user_ids')
+ if affected_users:
+ affected_users.sudo()._compute_is_treatment_professional()
+
+ # 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
- res = super().write(values)
to_recompute.recompute_followers()
- return res
- return super().write(values)
+
+ return result
diff --git a/bemade_sports_clinic/security/ir.model.access.csv b/bemade_sports_clinic/security/ir.model.access.csv
index 793f2d9..978a121 100644
--- a/bemade_sports_clinic/security/ir.model.access.csv
+++ b/bemade_sports_clinic/security/ir.model.access.csv
@@ -11,3 +11,4 @@ access_team_admin,Admin Access for Teams,model_sports_team,group_sports_clinic_a
access_team_portal,Portal Access for Teams,model_sports_team,base.group_portal,1,0,0,0
access_team_staff_user,User Access for Team Staff,model_sports_team_staff,group_sports_clinic_user,1,1,1,1
access_team_staff_portal,Portal Access for Team Staff,model_sports_team_staff,base.group_portal,1,0,0,0
+access_team_staff_admin,Admin Access for Team Staff,model_sports_team_staff,group_sports_clinic_admin,1,1,1,1
diff --git a/bemade_sports_clinic/security/sports_clinic_groups.xml b/bemade_sports_clinic/security/sports_clinic_groups.xml
index 455b2fe..4dc1390 100644
--- a/bemade_sports_clinic/security/sports_clinic_groups.xml
+++ b/bemade_sports_clinic/security/sports_clinic_groups.xml
@@ -19,13 +19,14 @@
Treatment Professional
-
+
Administrator
+ eval="[Command.link(ref('group_sports_clinic_user')),
+ Command.link(ref('group_sports_clinic_treatment_professional'))]"/>
diff --git a/bemade_sports_clinic/tests/__init__.py b/bemade_sports_clinic/tests/__init__.py
index e5e9a1e..4137576 100644
--- a/bemade_sports_clinic/tests/__init__.py
+++ b/bemade_sports_clinic/tests/__init__.py
@@ -1,3 +1,5 @@
from . import test_patient
from . import test_rights
from . import test_users
+from . import test_portal_access
+from . import test_treatment_professional_consistency
diff --git a/bemade_sports_clinic/views/res_users_views.xml b/bemade_sports_clinic/views/res_users_views.xml
index cbef072..27a4e5b 100644
--- a/bemade_sports_clinic/views/res_users_views.xml
+++ b/bemade_sports_clinic/views/res_users_views.xml
@@ -1,14 +1,29 @@
+
view.users.form.inheritres.users
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bemade_sports_clinic/views/sports_clinic_menus.xml b/bemade_sports_clinic/views/sports_clinic_menus.xml
index 6f36ddc..1db9ab2 100644
--- a/bemade_sports_clinic/views/sports_clinic_menus.xml
+++ b/bemade_sports_clinic/views/sports_clinic_menus.xml
@@ -28,7 +28,7 @@
Organizationsres.partner
- tree,kanban,form
+ list,kanban,form[('is_company', '=', True)]
diff --git a/bemade_sports_clinic/views/sports_clinic_portal_views.xml b/bemade_sports_clinic/views/sports_clinic_portal_views.xml
index cca1435..4454ad5 100644
--- a/bemade_sports_clinic/views/sports_clinic_portal_views.xml
+++ b/bemade_sports_clinic/views/sports_clinic_portal_views.xml
@@ -176,7 +176,7 @@