diff --git a/bemade_sports_clinic/controllers/patient_injury_portal.py b/bemade_sports_clinic/controllers/patient_injury_portal.py index 9a95ad0..6a49d92 100644 --- a/bemade_sports_clinic/controllers/patient_injury_portal.py +++ b/bemade_sports_clinic/controllers/patient_injury_portal.py @@ -155,76 +155,62 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin): # Create the injury record - portal users now have create permission injury = request.env['sports.patient.injury'].create(vals) - # Determine if user is a coach or treatment professional + # Determine role for assignment behavior # Use request.env.user.has_group() directly to avoid security violations is_portal_coach = request.env.user.has_group('bemade_sports_clinic.group_portal_team_coach') is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') user = request.env.user - - # Assign treatment professionals based on user role - - # Handle treatment professional assignments - treatment_prof_ids = [] - - # If user is a treatment professional, add them by default + + # Assignment rules: + # - If any therapists were explicitly selected in the form, assign exactly those and do not auto-assign others. + # - If none were selected, auto-assign team therapists (if determinable) and do not auto-assign the creator by default. + + # Read explicit selections (checkbox-based multi-select) + selected_tp_ids = [] if is_treatment_prof: - treatment_prof_ids.append(user.id) - - # Also add any additional treatment professionals selected in the form (checkbox-based) - selected_tp_ids = request.httprequest.form.getlist('treatment_professional_ids[]') - if selected_tp_ids: - # Convert to integers and add to list (avoiding duplicates) - for tp_id in selected_tp_ids: - tp_id_int = int(tp_id) - if tp_id_int not in treatment_prof_ids: - treatment_prof_ids.append(tp_id_int) - - # Assign treatment professionals if any were identified - if treatment_prof_ids: - injury.write({ - 'treatment_professional_ids': [(6, 0, treatment_prof_ids)] - }) + selected_tp_ids = request.httprequest.form.getlist('treatment_professional_ids[]') or [] + # Normalize to ints and remove empties + selected_tp_ids = [int(tp_id) for tp_id in selected_tp_ids if tp_id] + + if selected_tp_ids: + # Respect explicit selection only + injury.write({'treatment_professional_ids': [(6, 0, selected_tp_ids)]}) else: - # User is not a treatment professional - pass - - # Get current treatment professionals - treatment_profs = injury.treatment_professional_ids + # No explicit selection: perform team-based auto-assignment (if a single team context exists) + if team_id: + selected_team_id = int(team_id) + # Find therapists (head and regular) specifically for this team + team_staff = request.env['sports.team.staff'].sudo().search([ + ('team_id', '=', selected_team_id), + ('role', 'in', ['head_therapist', 'therapist']) + ]) - # Always try to assign team therapists regardless of who created the injury - # Only when a single team context is determinable - if team_id: - selected_team_id = int(team_id) - # Find therapists (head and regular) specifically for this team - team_staff = request.env['sports.team.staff'].sudo().search([ - ('team_id', '=', selected_team_id), - ('role', 'in', ['head_therapist', 'therapist']) - ]) + # Collect user IDs from team staff (prefer direct user_ids relation, fallback to partner mapping) + team_tp_user_ids = set() + for staff in team_staff: + if staff.user_ids: + for u in staff.user_ids: + team_tp_user_ids.add(u.id) + else: + users = request.env['res.users'].sudo().search([('partner_id', '=', staff.partner_id.id)]) + for u in users: + team_tp_user_ids.add(u.id) - # Collect user IDs from team staff (prefer direct user_ids relation, fallback to partner mapping) - team_tp_user_ids = set() - for staff in team_staff: - if staff.user_ids: - for u in staff.user_ids: - team_tp_user_ids.add(u.id) - else: - users = request.env['res.users'].sudo().search([('partner_id', '=', staff.partner_id.id)]) - for u in users: - team_tp_user_ids.add(u.id) + if user.id in team_tp_user_ids and (user.id not in (selected_tp_ids or [])): + # Do not auto-assign the creating user unless explicitly selected + team_tp_user_ids.discard(user.id) - if not team_tp_user_ids: - _logger.warning("No valid therapists found to assign to the injury for team %s", selected_team_id) - - # Merge any team therapists with any already set/selected ones - merged_ids = set(treatment_prof_ids) | team_tp_user_ids if 'treatment_prof_ids' in locals() else team_tp_user_ids - if merged_ids: - injury.write({'treatment_professional_ids': [(6, 0, list(merged_ids))]}) - else: - _logger.info( - "Skipping team-based therapist auto-assignment: patient %s has %s teams", - patient.id, - len(patient.team_ids), - ) + if not team_tp_user_ids: + _logger.warning("No valid therapists found to assign to the injury for team %s", selected_team_id) + + if team_tp_user_ids: + injury.write({'treatment_professional_ids': [(6, 0, list(team_tp_user_ids))]}) + else: + _logger.info( + "Skipping team-based therapist auto-assignment: patient %s has %s teams", + patient.id, + len(patient.team_ids), + ) # Handle treatment note creation if provided by treatment professional if is_treatment_prof and post.get('treatment_note'): diff --git a/bemade_sports_clinic/tests/__init__.py b/bemade_sports_clinic/tests/__init__.py index cfcffc7..8263300 100644 --- a/bemade_sports_clinic/tests/__init__.py +++ b/bemade_sports_clinic/tests/__init__.py @@ -7,3 +7,5 @@ from . import test_player_removal from . import test_mail_activity_portal_access from . import test_mail_activity_portal_integration from . import test_project_task_portal_security +from . import test_portal_activity_default_todo +from . import test_portal_injury_autoassign_integration diff --git a/bemade_sports_clinic/tests/test_portal_activity_default_todo.py b/bemade_sports_clinic/tests/test_portal_activity_default_todo.py new file mode 100644 index 0000000..c8094f8 --- /dev/null +++ b/bemade_sports_clinic/tests/test_portal_activity_default_todo.py @@ -0,0 +1,72 @@ +from odoo.tests import HttpCase, tagged +from odoo import Command +from odoo import fields +import re + + +@tagged("-at_install", "post_install") +class TestPortalActivityDefaultTodo(HttpCase): + """Verify the activity creation form defaults the activity type to 'To Do'.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + # Org/team/patient setup + cls.organization = cls.env['res.partner'].create({ + 'name': 'Default Todo Org', + 'is_company': True, + }) + cls.team = cls.env['sports.team'].create({ + 'name': 'Default Todo Team', + 'parent_id': cls.organization.id, + }) + cls.patient = cls.env['sports.patient'].create({ + 'first_name': 'Default', + 'last_name': 'TodoPatient', + 'date_of_birth': '2005-01-01', + 'team_ids': [(4, cls.team.id)], + }) + + # Create a portal treatment professional user and add to team staff + cls.tp_partner = cls.env['res.partner'].create({ + 'name': 'Portal TP', + 'email': 'portal.tp.todo@example.com', + }) + cls.tp_user = cls.env['res.users'].with_context(no_reset_password=True).create({ + 'partner_id': cls.tp_partner.id, + 'login': 'portal.tp.todo@example.com', + 'password': 'tp', + 'name': cls.tp_partner.name, + 'groups_id': [ + Command.link(cls.env.ref('base.group_portal').id), + Command.link(cls.env.ref('bemade_sports_clinic.group_portal_treatment_professional').id), + ] + }) + cls.env['sports.team.staff'].create({ + 'team_id': cls.team.id, + 'partner_id': cls.tp_partner.id, + 'role': 'therapist', + }) + + # Resolve the expected default To Do activity type id using the same priority as controller + todo_type = cls.env.ref('mail.mail_activity_data_todo', raise_if_not_found=False) + if not todo_type: + todo_type = cls.env['mail.activity.type'].search([('category', '=', 'todo')], limit=1) + if not todo_type: + todo_type = cls.env['mail.activity.type'].search([('name', 'ilike', 'todo')], limit=1) + cls.todo_type_id = todo_type and todo_type.id or False + + def test_activity_create_form_defaults_todo(self): + self.assertTrue(self.todo_type_id, "A 'To Do' activity type must exist for this test") + + # Login + self.authenticate('portal.tp.todo@example.com', 'tp') + + # Open create activity form for patient + resp = self.url_open(f"/my/activity/create?model=sports.patient&res_id={self.patient.id}", timeout=30) + self.assertEqual(resp.status_code, 200) + + # Assert the To Do option is pre-selected in the HTML + # QWeb renders selected attribute when t-att-selected resolves truthy, typically selected="selected" + pattern = rf']+value="{self.todo_type_id}"[^>]+selected' + self.assertRegex(resp.text, pattern, msg="Expected the 'To Do' activity type to be pre-selected") diff --git a/bemade_sports_clinic/tests/test_portal_injury_autoassign_integration.py b/bemade_sports_clinic/tests/test_portal_injury_autoassign_integration.py new file mode 100644 index 0000000..ddc146a --- /dev/null +++ b/bemade_sports_clinic/tests/test_portal_injury_autoassign_integration.py @@ -0,0 +1,137 @@ +from odoo.tests import HttpCase, tagged +from odoo import Command + + +@tagged("-at_install", "post_install") +class TestPortalInjuryAutoAssign(HttpCase): + """Validate therapist auto-assignment during portal injury creation. + + Scenarios: + - Treatment Professional creates injury: assigned = current TP + selected TPs + team therapists (unique set) + - Coach creates injury: assigned = team therapists only (no coach user) + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + env = cls.env + + # Base org and team + cls.org = env['res.partner'].create({ + 'name': 'AutoAssign Org', + 'is_company': True, + }) + cls.team = env['sports.team'].create({ + 'name': 'AutoAssign Team', + 'parent_id': cls.org.id, + }) + + # Patient + cls.patient = env['sports.patient'].create({ + 'first_name': 'Injury', + 'last_name': 'Subject', + 'date_of_birth': '2006-06-06', + 'team_ids': [(4, cls.team.id)], + }) + + # Users: TP1 (actor), TP2 (select extra), Coach (actor) + base_portal = env.ref('base.group_portal') + tp_group_portal = env.ref('bemade_sports_clinic.group_portal_treatment_professional') + coach_group = env.ref('bemade_sports_clinic.group_portal_team_coach') + + # Team therapists via team staff: map partner to user(s) + cls.tp1_partner = env['res.partner'].create({'name': 'TP1', 'email': 'tp1@example.com'}) + cls.tp1_user = env['res.users'].with_context(no_reset_password=True).create({ + 'partner_id': cls.tp1_partner.id, + 'login': 'tp1@example.com', + 'password': 'tp1', + 'name': 'TP1', + 'groups_id': [Command.link(base_portal.id), Command.link(tp_group_portal.id)], + }) + env['sports.team.staff'].create({ + 'team_id': cls.team.id, + 'partner_id': cls.tp1_partner.id, + 'role': 'therapist', + }) + + # Second therapist, also part of team via staff + cls.tp2_partner = env['res.partner'].create({'name': 'TP2', 'email': 'tp2@example.com'}) + cls.tp2_user = env['res.users'].with_context(no_reset_password=True).create({ + 'partner_id': cls.tp2_partner.id, + 'login': 'tp2@example.com', + 'password': 'tp2', + 'name': 'TP2', + 'groups_id': [Command.link(base_portal.id), Command.link(tp_group_portal.id)], + }) + env['sports.team.staff'].create({ + 'team_id': cls.team.id, + 'partner_id': cls.tp2_partner.id, + 'role': 'head_therapist', + }) + + # Additional TP3 not in team; used for explicit selection by TP1 + cls.tp3_partner = env['res.partner'].create({'name': 'TP3', 'email': 'tp3@example.com'}) + cls.tp3_user = env['res.users'].with_context(no_reset_password=True).create({ + 'partner_id': cls.tp3_partner.id, + 'login': 'tp3@example.com', + 'password': 'tp3', + 'name': 'TP3', + 'groups_id': [Command.link(base_portal.id), Command.link(tp_group_portal.id)], + }) + + # Coach user + cls.coach_partner = env['res.partner'].create({'name': 'Coach', 'email': 'coach@example.com'}) + cls.coach_user = env['res.users'].with_context(no_reset_password=True).create({ + 'partner_id': cls.coach_partner.id, + 'login': 'coach@example.com', + 'password': 'coach', + 'name': 'Coach', + 'groups_id': [Command.link(base_portal.id), Command.link(coach_group.id)], + }) + # Add coach as staff for access (role coach) + env['sports.team.staff'].create({ + 'team_id': cls.team.id, + 'partner_id': cls.coach_partner.id, + 'role': 'coach', + }) + + def _latest_injury(self): + return self.env['sports.patient.injury'].search([('patient_id', '=', self.patient.id)], order='id desc', limit=1) + + def test_autoassign_when_tp_creates_injury(self): + # TP1 logs in and creates injury selecting TP3 additionally, team set + self.authenticate('tp1@example.com', 'tp1') + data = { + 'patient_id': str(self.patient.id), + 'team_id': str(self.team.id), + 'diagnosis': 'Hamstring strain', + # Multi-select checkboxes come as repeated keys treatment_professional_ids[] + 'treatment_professional_ids[]': [str(self.tp3_user.id)], + } + resp = self.url_open('/my/patient/injury/create', data=data, timeout=30) + self.assertEqual(resp.status_code, 200) + + injury = self._latest_injury() + self.assertTrue(injury, 'Injury should be created') + assigned = set(injury.treatment_professional_ids.ids) + # Expect TP1 (actor), both team therapists TP1+TP2, and selected TP3 + expected = {self.tp1_user.id, self.tp2_user.id, self.tp3_user.id} + # TP1 is also a team therapist via staff, ensure no duplicates and inclusion + self.assertTrue(expected.issubset(assigned), f"Assigned professionals {assigned} should include {expected}") + + def test_autoassign_when_coach_creates_injury(self): + # Coach logs in and creates injury; should auto-assign only team therapists (TP1, TP2), not coach + self.authenticate('coach@example.com', 'coach') + data = { + 'patient_id': str(self.patient.id), + 'team_id': str(self.team.id), + 'diagnosis': 'Ankle sprain', + } + resp = self.url_open('/my/patient/injury/create', data=data, timeout=30) + self.assertEqual(resp.status_code, 200) + + injury = self._latest_injury() + self.assertTrue(injury, 'Injury should be created by coach') + assigned = set(injury.treatment_professional_ids.ids) + expected_team = {self.tp1_user.id, self.tp2_user.id} + self.assertEqual(assigned, expected_team, f"Coach flow should assign only team therapists {expected_team}, got {assigned}") diff --git a/bemade_sports_clinic/views/sports_patient_injury_portal.xml b/bemade_sports_clinic/views/sports_patient_injury_portal.xml index 26fb20a..a362f72 100644 --- a/bemade_sports_clinic/views/sports_patient_injury_portal.xml +++ b/bemade_sports_clinic/views/sports_patient_injury_portal.xml @@ -153,7 +153,7 @@ - Select multiple professionals (you will be added automatically) + Select one or more professionals to assign. If left blank, team therapists will be auto-assigned. You will not be auto-assigned unless selected.