2025-07-11 21:47:50 -04:00
|
|
|
from odoo.tests import TransactionCase, tagged
|
|
|
|
|
from odoo import Command, fields
|
|
|
|
|
from datetime import timedelta
|
|
|
|
|
from freezegun import freeze_time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@tagged("-at_install", "post_install")
|
|
|
|
|
class TestEndToEndWorkflows(TransactionCase):
|
|
|
|
|
"""End-to-end workflow tests for the sports clinic module"""
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def setUpClass(cls):
|
|
|
|
|
super().setUpClass()
|
|
|
|
|
|
|
|
|
|
# Create organization and team
|
|
|
|
|
cls.organization = cls.env['sports.organization'].create({
|
|
|
|
|
'name': 'E2E Test Organization',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
cls.team = cls.env['sports.team'].create({
|
|
|
|
|
'name': 'E2E Test Team',
|
|
|
|
|
'organization_id': cls.organization.id,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Create a patient/player
|
|
|
|
|
cls.patient = cls.env['sports.patient'].create({
|
|
|
|
|
'first_name': 'E2E',
|
|
|
|
|
'last_name': 'Test Patient',
|
|
|
|
|
'birthdate': fields.Date.today() - timedelta(days=365 * 16), # 16 years old
|
|
|
|
|
'team_ids': [(4, cls.team.id)],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Create users with different roles
|
|
|
|
|
# 1. Coach
|
|
|
|
|
cls.coach_partner = cls.env['res.partner'].create({
|
|
|
|
|
'name': 'E2E Coach',
|
|
|
|
|
'email': 'e2e.coach@example.com',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
cls.coach_user = cls.env['res.users'].with_context(no_reset_password=True).create({
|
|
|
|
|
'partner_id': cls.coach_partner.id,
|
|
|
|
|
'login': 'e2e.coach@example.com',
|
|
|
|
|
'password': 'e2ecoach',
|
|
|
|
|
'name': cls.coach_partner.name,
|
|
|
|
|
'groups_id': [
|
|
|
|
|
Command.link(cls.env.ref('bemade_sports_clinic.group_sports_clinic_team_coach').id),
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# 2. Therapist (treatment professional)
|
|
|
|
|
cls.therapist_partner = cls.env['res.partner'].create({
|
|
|
|
|
'name': 'E2E Therapist',
|
|
|
|
|
'email': 'e2e.therapist@example.com',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
cls.therapist_user = cls.env['res.users'].with_context(no_reset_password=True).create({
|
|
|
|
|
'partner_id': cls.therapist_partner.id,
|
|
|
|
|
'login': 'e2e.therapist@example.com',
|
|
|
|
|
'password': 'e2etherapist',
|
|
|
|
|
'name': cls.therapist_partner.name,
|
|
|
|
|
'groups_id': [
|
|
|
|
|
Command.link(cls.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional').id),
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Create team staff entries
|
|
|
|
|
cls.coach_staff = cls.env['sports.team.staff'].create({
|
|
|
|
|
'team_id': cls.team.id,
|
|
|
|
|
'partner_id': cls.coach_partner.id,
|
2025-07-16 11:37:50 -04:00
|
|
|
# Role coach doesn't grant treatment professional status
|
2025-07-11 21:47:50 -04:00
|
|
|
'role': 'coach',
|
|
|
|
|
'user_id': cls.coach_user.id,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
cls.therapist_staff = cls.env['sports.team.staff'].create({
|
|
|
|
|
'team_id': cls.team.id,
|
|
|
|
|
'partner_id': cls.therapist_partner.id,
|
2025-07-16 11:37:50 -04:00
|
|
|
# Role therapist automatically grants treatment professional status
|
2025-07-11 21:47:50 -04:00
|
|
|
'role': 'therapist',
|
|
|
|
|
'user_id': cls.therapist_user.id,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
def test_01_complete_injury_workflow(self):
|
|
|
|
|
"""Test complete injury workflow from reporting to resolution"""
|
|
|
|
|
|
|
|
|
|
# Step 1: Coach reports an injury (unverified)
|
|
|
|
|
with freeze_time('2025-01-15'):
|
|
|
|
|
# Create injury as coach
|
|
|
|
|
injury = self.env['sports.patient.injury'].with_user(self.coach_user).create({
|
|
|
|
|
'patient_id': self.patient.id,
|
|
|
|
|
'team_id': self.team.id,
|
|
|
|
|
'diagnosis': 'Ankle Sprain - Initial',
|
|
|
|
|
'external_notes': 'Player twisted ankle during practice.',
|
|
|
|
|
'injury_date': fields.Date.today(),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Check that injury is unverified and has default values
|
|
|
|
|
self.assertEqual(injury.stage, 'unverified')
|
|
|
|
|
self.assertEqual(injury.parental_consent, 'no') # Default for coach-created injuries
|
|
|
|
|
self.assertFalse(injury.treatment_professional_ids) # No treatment pros yet
|
|
|
|
|
|
|
|
|
|
# Check that the player is now marked as injured
|
|
|
|
|
self.patient.invalidate_cache()
|
|
|
|
|
self.assertTrue(self.patient.is_injured)
|
|
|
|
|
|
|
|
|
|
# Step 2: Therapist verifies the injury and adds details
|
|
|
|
|
with freeze_time('2025-01-16'):
|
|
|
|
|
# Update injury as therapist
|
|
|
|
|
injury.with_user(self.therapist_user).write({
|
|
|
|
|
'diagnosis': 'Grade 2 Ankle Sprain',
|
|
|
|
|
'stage': 'active',
|
|
|
|
|
'treatment_professional_ids': [(4, self.therapist_partner.id)],
|
|
|
|
|
'internal_notes': 'Significant swelling observed. Recommend RICE protocol.',
|
|
|
|
|
'parental_consent': 'yes',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Check that injury is now active and therapist is assigned
|
|
|
|
|
self.assertEqual(injury.stage, 'active')
|
|
|
|
|
self.assertEqual(injury.parental_consent, 'yes')
|
|
|
|
|
self.assertIn(self.therapist_partner, injury.treatment_professional_ids)
|
|
|
|
|
|
|
|
|
|
# Add a message/note to the chatter as the therapist
|
|
|
|
|
injury.with_user(self.therapist_user).message_post(
|
|
|
|
|
body="Initial assessment complete. Ankle mobility restricted.",
|
|
|
|
|
message_type='comment',
|
|
|
|
|
subtype_xmlid='mail.mt_comment',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Step 3: Treatment progress and updates
|
|
|
|
|
with freeze_time('2025-01-23'):
|
|
|
|
|
# Add treatment progress note
|
|
|
|
|
injury.with_user(self.therapist_user).write({
|
|
|
|
|
'external_notes': 'Player twisted ankle during practice. Follow-up: '
|
|
|
|
|
'Swelling reduced, started basic rehabilitation exercises.',
|
|
|
|
|
'internal_notes': 'Significant swelling observed. Recommend RICE protocol. '
|
|
|
|
|
'Update: ROM improving, pain decreased from 7/10 to 4/10.',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Add another chatter message
|
|
|
|
|
injury.with_user(self.therapist_user).message_post(
|
|
|
|
|
body="Week 1 follow-up: Patient showing good progress. Cleared for light conditioning.",
|
|
|
|
|
message_type='comment',
|
|
|
|
|
subtype_xmlid='mail.mt_comment',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Check that coach can see the external updates
|
|
|
|
|
as_coach = injury.with_user(self.coach_user)
|
|
|
|
|
self.assertIn('started basic rehabilitation exercises', as_coach.external_notes)
|
|
|
|
|
# But not the internal notes
|
|
|
|
|
with self.assertRaises(Exception):
|
|
|
|
|
coach_view_notes = as_coach.internal_notes
|
|
|
|
|
|
|
|
|
|
# Step 4: More treatment and approaching resolution
|
|
|
|
|
with freeze_time('2025-02-06'):
|
|
|
|
|
# Add another treatment progress note
|
|
|
|
|
injury.with_user(self.therapist_user).write({
|
|
|
|
|
'external_notes': injury.external_notes + '\nFollow-up 2: '
|
|
|
|
|
'Good progress. Can resume non-contact training.',
|
|
|
|
|
'internal_notes': injury.internal_notes + '\nFollow-up 2: '
|
|
|
|
|
'Strength tests show 85% compared to uninjured side. Proprioception improving.',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Step 5: Final resolution
|
|
|
|
|
with freeze_time('2025-02-15'):
|
|
|
|
|
# Resolve the injury
|
|
|
|
|
injury.with_user(self.therapist_user).write({
|
|
|
|
|
'stage': 'resolved',
|
|
|
|
|
'resolution_date': fields.Date.today(),
|
|
|
|
|
'external_notes': injury.external_notes + '\nFinal update: '
|
|
|
|
|
'Player fully cleared to return to all activities.',
|
|
|
|
|
'internal_notes': injury.internal_notes + '\nFinal assessment: '
|
|
|
|
|
'Full ROM restored, strength tests at 95%. Cleared for all activities.',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Verify the resolution details
|
|
|
|
|
self.assertEqual(injury.stage, 'resolved')
|
|
|
|
|
self.assertEqual(injury.resolution_date, fields.Date.from_string('2025-02-15'))
|
|
|
|
|
|
|
|
|
|
# Check that the player is no longer marked as injured
|
|
|
|
|
self.patient.invalidate_cache()
|
|
|
|
|
self.assertFalse(self.patient.is_injured)
|
|
|
|
|
|
|
|
|
|
def test_02_player_removal_workflow(self):
|
|
|
|
|
"""Test complete player removal workflow"""
|
|
|
|
|
|
|
|
|
|
# Create a test player to be removed
|
|
|
|
|
player_to_remove = self.env['sports.patient'].create({
|
|
|
|
|
'first_name': 'Removal',
|
|
|
|
|
'last_name': 'Test Patient',
|
|
|
|
|
'birthdate': fields.Date.today() - timedelta(days=365 * 17),
|
|
|
|
|
'team_ids': [(4, self.team.id)],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Verify initial state
|
|
|
|
|
self.assertIn(self.team, player_to_remove.team_ids)
|
|
|
|
|
self.assertFalse(player_to_remove.archived)
|
|
|
|
|
|
|
|
|
|
# Step 1: Coach requests player removal
|
|
|
|
|
removal_request = self.env['sports.patient.team.removal'].with_user(self.coach_user).create({
|
|
|
|
|
'patient_id': player_to_remove.id,
|
|
|
|
|
'team_id': self.team.id,
|
|
|
|
|
'reason': 'Player transferred to another team',
|
|
|
|
|
'requested_by_id': self.coach_partner.id,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
self.assertEqual(removal_request.state, 'draft')
|
|
|
|
|
self.assertEqual(removal_request.requested_by_id, self.coach_partner)
|
|
|
|
|
|
|
|
|
|
# Step 2: Admin approves and processes the removal
|
|
|
|
|
admin_user = self.env.ref('base.user_admin')
|
|
|
|
|
|
|
|
|
|
# Admin processes the request
|
|
|
|
|
removal_request.with_user(admin_user).action_approve()
|
|
|
|
|
self.assertEqual(removal_request.state, 'approved')
|
|
|
|
|
|
|
|
|
|
# Execute the removal
|
|
|
|
|
removal_request.with_user(admin_user).action_execute()
|
|
|
|
|
self.assertEqual(removal_request.state, 'done')
|
|
|
|
|
|
|
|
|
|
# Verify player has been removed from team
|
|
|
|
|
player_to_remove.invalidate_cache()
|
|
|
|
|
self.assertNotIn(self.team, player_to_remove.team_ids)
|
|
|
|
|
|
|
|
|
|
# Check if player is archived when no teams left
|
|
|
|
|
self.assertTrue(len(player_to_remove.team_ids) == 0)
|
|
|
|
|
player_to_remove.invalidate_cache()
|
|
|
|
|
self.assertTrue(player_to_remove.archived)
|
|
|
|
|
|
|
|
|
|
def test_03_data_export_anonymization_process(self):
|
|
|
|
|
"""Test data export and anonymization process"""
|
|
|
|
|
# Create test patient with personal information
|
|
|
|
|
patient_for_anon = self.env['sports.patient'].create({
|
|
|
|
|
'first_name': 'Export',
|
|
|
|
|
'last_name': 'Test Patient',
|
|
|
|
|
'birthdate': fields.Date.today() - timedelta(days=365 * 15),
|
|
|
|
|
'team_ids': [(4, self.team.id)],
|
|
|
|
|
'street': '123 Test Street',
|
|
|
|
|
'city': 'Test City',
|
|
|
|
|
'zip': '12345',
|
|
|
|
|
'email': 'test.patient@example.com',
|
|
|
|
|
'phone': '555-123-4567',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Create a contact for the patient
|
|
|
|
|
patient_contact = self.env['sports.patient.contact'].create({
|
|
|
|
|
'patient_id': patient_for_anon.id,
|
|
|
|
|
'name': 'Test Parent',
|
|
|
|
|
'phone': '555-987-6543',
|
|
|
|
|
'email': 'test.parent@example.com',
|
|
|
|
|
'relationship': 'parent',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Create an injury with personal medical information
|
|
|
|
|
injury_for_anon = self.env['sports.patient.injury'].create({
|
|
|
|
|
'patient_id': patient_for_anon.id,
|
|
|
|
|
'team_id': self.team.id,
|
|
|
|
|
'diagnosis': 'Confidential Medical Condition',
|
|
|
|
|
'external_notes': 'Contains personally identifiable information',
|
|
|
|
|
'internal_notes': 'Contains sensitive medical details',
|
|
|
|
|
'injury_date': fields.Date.today(),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Run anonymization wizard (simulated since actual wizard implementation might vary)
|
|
|
|
|
# This is a placeholder for the actual anonymization process
|
|
|
|
|
# In a real implementation, we would call the anonymization wizard here
|
|
|
|
|
|
|
|
|
|
# For testing purposes, we'll manually anonymize the patient
|
|
|
|
|
patient_for_anon.write({
|
|
|
|
|
'first_name': 'Anonymized',
|
|
|
|
|
'last_name': 'Patient',
|
|
|
|
|
'street': False,
|
|
|
|
|
'city': False,
|
|
|
|
|
'zip': False,
|
|
|
|
|
'email': False,
|
|
|
|
|
'phone': False,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
patient_contact.write({
|
|
|
|
|
'name': 'Anonymized Contact',
|
|
|
|
|
'phone': False,
|
|
|
|
|
'email': False,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
injury_for_anon.write({
|
|
|
|
|
'external_notes': 'Anonymized external notes',
|
|
|
|
|
'internal_notes': 'Anonymized internal notes',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Verify anonymization was effective
|
|
|
|
|
self.assertEqual(patient_for_anon.first_name, 'Anonymized')
|
|
|
|
|
self.assertEqual(patient_for_anon.last_name, 'Patient')
|
|
|
|
|
self.assertFalse(patient_for_anon.email)
|
|
|
|
|
self.assertFalse(patient_for_anon.phone)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(patient_contact.name, 'Anonymized Contact')
|
|
|
|
|
self.assertFalse(patient_contact.email)
|
|
|
|
|
self.assertFalse(patient_contact.phone)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(injury_for_anon.external_notes, 'Anonymized external notes')
|
|
|
|
|
self.assertEqual(injury_for_anon.internal_notes, 'Anonymized internal notes')
|