From c60ffd5bfa34b1008b8c8093fa6ba428b2d984c4 Mon Sep 17 00:00:00 2001 From: Denis Durepos Date: Sat, 21 Jun 2025 21:24:50 -0400 Subject: [PATCH] Created portal views for therapists and Report Injury functionality for portal users (coaches and therapists). --- bemade_sports_clinic/__manifest__.py | 9 ++ bemade_sports_clinic/controllers/__init__.py | 1 + .../data/demo/sports_clinic_demo_data.xml | 12 +- bemade_sports_clinic/i18n/fr_CA.po | 4 +- bemade_sports_clinic/models/patient.py | 34 +++++ .../models/patient_contact.py | 7 +- bemade_sports_clinic/models/patient_injury.py | 35 ++++- bemade_sports_clinic/models/res_users.py | 17 ++- bemade_sports_clinic/models/sports_team.py | 125 +++++++++++++++--- .../security/ir.model.access.csv | 1 + .../security/sports_clinic_groups.xml | 5 +- bemade_sports_clinic/tests/__init__.py | 2 + .../views/res_users_views.xml | 19 ++- .../views/sports_clinic_menus.xml | 2 +- .../views/sports_clinic_portal_views.xml | 2 +- .../views/sports_patient_injury_views.xml | 14 +- .../views/sports_patient_views.xml | 17 ++- .../views/sports_team_views.xml | 14 +- 18 files changed, 262 insertions(+), 58 deletions(-) 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 @@ therapist therapist - + + + @@ -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.inherit res.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 @@ Organizations res.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 @@ - + diff --git a/bemade_sports_clinic/views/sports_patient_injury_views.xml b/bemade_sports_clinic/views/sports_patient_injury_views.xml index b7796d2..aa1ace5 100644 --- a/bemade_sports_clinic/views/sports_patient_injury_views.xml +++ b/bemade_sports_clinic/views/sports_patient_injury_views.xml @@ -18,6 +18,7 @@ + -
- - - -
+
- - sports.patient.injury.view.tree + + sports.patient.injury.view.list sports.patient.injury - + + diff --git a/bemade_sports_clinic/views/sports_patient_views.xml b/bemade_sports_clinic/views/sports_patient_views.xml index 578f12e..4d8237d 100644 --- a/bemade_sports_clinic/views/sports_patient_views.xml +++ b/bemade_sports_clinic/views/sports_patient_views.xml @@ -79,7 +79,7 @@ - +
@@ -114,7 +114,14 @@ -
+
+ +

Patient Record

@@ -176,11 +183,7 @@
-
- - - -
+
diff --git a/bemade_sports_clinic/views/sports_team_views.xml b/bemade_sports_clinic/views/sports_team_views.xml index 089ce6f..247c54a 100644 --- a/bemade_sports_clinic/views/sports_team_views.xml +++ b/bemade_sports_clinic/views/sports_team_views.xml @@ -26,7 +26,7 @@ - +
@@ -110,8 +110,8 @@ - - + +