bemade-addons/bemade_sports_clinic/controllers/patient_injury_portal.py
Denis Durepos ca57ec8f16 [IMP] bemade_sports_clinic: Refactor portal views and enhance treatment notes
- Refactor portal templates to eliminate intra-module template inheritance
- Integrate emergency contacts section into player injuries template
- Add full internal admin views for treatment notes with chatter support
- Fix ORM warning in test_rights by using proper ORM commands
- Update manifest to reflect new functionality (v18.0.1.9.0)

This refactoring improves template stability by removing XPath errors and
provides treatment professionals with better access to emergency contacts
and treatment notes both in portal and backend interfaces.
2025-07-11 21:47:50 -04:00

634 lines
28 KiB
Python

import logging
import base64
import io
from odoo import http, fields, _
from odoo.exceptions import UserError, ValidationError
from odoo.http import request
from odoo.addons.portal.controllers.portal import CustomerPortal, pager
from datetime import datetime
_logger = logging.getLogger(__name__)
class PatientInjuryPortal(CustomerPortal):
"""Controller for all injury reporting functionality in the portal"""
def _check_access_to_patient(self, patient_id):
"""Verify the user has access to this patient"""
user = request.env.user
patient = request.env['sports.patient'].browse(int(patient_id))
# Check if user has access to any team this patient belongs to
patient_id_int = int(patient_id)
accessible_teams = request.env['sports.team'].search([
('staff_ids.user_ids', 'in', user.id),
('patient_ids', 'in', patient_id_int)
])
# Medical professionals might have specific access
is_medical = user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
if not patient.exists() or (not accessible_teams and not is_medical):
raise UserError(_('You do not have access to this patient.'))
return patient
@http.route(['/my/patient/injury/new'], type='http', auth='user', website=True)
def create_injury_form(self, patient_id=None, **post):
"""Show form to create a new injury report"""
if not patient_id:
return request.redirect('/my/players')
try:
patient = self._check_access_to_patient(patient_id)
except UserError as e:
return request.render('portal.403', {'error': str(e)})
return_url = post.get('return_url', f'/my/player?player_id={patient_id}')
# Get patient's teams for the dropdown
teams = patient.team_ids
# Pre-selected team ID (when player is only on one team)
default_team_id = teams[0].id if len(teams) == 1 else None
# Check if user is a treatment professional
user = request.env.user
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
values = {
'patient': patient,
'teams': teams, # Pass teams to the template for dropdown
'default_team_id': default_team_id, # Will be None if multiple teams
'return_url': return_url,
'page_name': 'report_injury',
'is_treatment_prof': is_treatment_prof, # Pass flag to template for conditional display
}
return request.render('bemade_sports_clinic.portal_create_injury', values)
@http.route(['/my/patient/injury/create'], type='http', auth='user', website=True, methods=['POST'])
def create_injury_submit(self, **post):
"""Process the form submission to create a new injury"""
patient_id = post.get('patient_id')
if not patient_id:
return request.redirect('/my/players')
try:
patient = self._check_access_to_patient(patient_id)
except UserError as e:
return request.render('portal.403', {'error': str(e)})
# Get the selected team
team_id = post.get('team_id')
if not team_id:
return request.redirect(f'/my/patient/injury/new?patient_id={patient_id}')
# Check if the current user is a treatment professional
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
# Prepare values for injury creation
vals = {
'patient_id': int(patient_id),
'diagnosis': post.get('diagnosis'),
'external_notes': post.get('external_notes'),
# Set stage based on user role - treatment professionals create active injuries
'stage': 'active' if is_treatment_prof else 'unverified',
'patient_name': patient.name,
# Store which team the injury was reported for
'team_id': int(team_id),
# Store parental consent value
'parental_consent': post.get('parental_consent', 'no'),
}
# Handle dates
if post.get('injury_date'):
vals['injury_date'] = post.get('injury_date')
if post.get('predicted_resolution_date'):
vals['predicted_resolution_date'] = post.get('predicted_resolution_date')
# Create the injury record
injury = request.env['sports.patient.injury'].sudo().create(vals)
user = request.env.user
# Determine if user is a coach or treatment professional
is_portal_coach = user.has_group('bemade_sports_clinic.group_portal_team_coach')
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
has_treatment_attr = hasattr(user, 'is_treatment_professional') and user.is_treatment_professional
# Detailed logging of the current user's status
_logger.info(f"Current user: {user.name} (ID: {user.id})")
_logger.info(f"Is portal coach: {is_portal_coach}")
_logger.info(f"Is in portal treatment professional group: {is_treatment_prof}")
_logger.info(f"Has is_treatment_professional=True: {has_treatment_attr}")
# If user is a treatment professional, add them to the treatment professionals
# Make sure to check both the group membership and the is_treatment_professional field
if is_treatment_prof or has_treatment_attr:
_logger.info(f"Adding current user {user.name} (ID: {user.id}) to treatment professionals")
injury.sudo().write({
'treatment_professional_ids': [(4, user.id)]
})
else:
_logger.info(f"User {user.name} is not identified as a treatment professional, not adding to injury")
# Double-check who's assigned after our additions
treatment_profs = injury.sudo().treatment_professional_ids
_logger.info(f"Treatment professionals after assignment: {[u.name for u in treatment_profs]} (IDs: {treatment_profs.ids})")
# Always try to assign team therapists regardless of who created the injury
if True:
# Use the selected team_id from the form
selected_team_id = int(team_id)
# Find therapists specifically for this team
team_staff = request.env['sports.team.staff'].sudo().search([
('team_id', '=', selected_team_id), # Only from selected team
('role', 'in', ['head_therapist', 'therapist'])
])
# Log debug info
_logger.info(f"Found {len(team_staff)} therapists/head therapists for team {selected_team_id}")
for staff in team_staff:
_logger.info(f"Staff: {staff.partner_id.name}, Role: {staff.role}, User IDs: {[u.id for u in staff.user_ids]}")
if not staff.user_ids:
_logger.info(f" - WARNING: No user_ids for {staff.partner_id.name}")
# Try to find a user directly associated with this partner
users = request.env['res.users'].sudo().search([('partner_id', '=', staff.partner_id.id)])
_logger.info(f" - Found direct users: {[u.id for u in users]}")
# Filter by role
head_therapists = team_staff.filtered(lambda s: s.role == 'head_therapist')
therapists = team_staff.filtered(lambda s: s.role == 'therapist')
_logger.info(f"Found {len(head_therapists)} head therapists and {len(therapists)} therapists")
# First try to assign head therapist, then any therapist from the selected team
treatment_pros_assigned = False
# Try to assign head therapist first
if head_therapists:
# Find users associated directly with the head therapist partner
head_therapist = head_therapists[0]
users = request.env['res.users'].sudo().search([('partner_id', '=', head_therapist.partner_id.id)])
if users:
_logger.info(f"Assigning head therapist: {head_therapist.partner_id.name} with user ID {users[0].id}")
injury.sudo().write({
'treatment_professional_ids': [(4, users[0].id)]
})
treatment_pros_assigned = True
else:
_logger.info(f"No user account found for head therapist: {head_therapist.partner_id.name}")
# Try to assign regular therapist if no head therapist was assigned
if not treatment_pros_assigned and therapists:
# Find users associated directly with the therapist partner
therapist = therapists[0]
users = request.env['res.users'].sudo().search([('partner_id', '=', therapist.partner_id.id)])
if users:
_logger.info(f"Assigning therapist: {therapist.partner_id.name} with user ID {users[0].id}")
injury.sudo().write({
'treatment_professional_ids': [(4, users[0].id)]
})
treatment_pros_assigned = True
else:
_logger.info(f"No user account found for therapist: {therapist.partner_id.name}")
# If no therapist was assigned, log a warning
if not treatment_pros_assigned:
_logger.warning("No valid therapists found to assign to the injury")
# Trigger recomputation of patient status based on the injury
patient.sudo()._compute_is_injured()
patient.sudo()._compute_stage()
return_url = f'/my/player?player_id={patient_id}'
values = {
'return_url': return_url,
}
return request.render('bemade_sports_clinic.portal_injury_created', values)
def _check_access_to_injury(self, injury_id):
"""Verify the user has access to this injury"""
user = request.env.user
injury = request.env['sports.patient.injury'].browse(int(injury_id))
# Check if user has access to the team this injury is associated with
if not injury.exists():
raise UserError(_('Injury not found.'))
# Get the team from the injury
team = injury.team_id
if team:
# Check if user is part of the team staff
is_team_staff = team.staff_ids.filtered(lambda s: s.user_ids and user.id in s.user_ids.ids)
# Or if user is a medical professional with broader access
is_medical = user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
if not is_team_staff and not is_medical:
raise UserError(_('You do not have access to this injury.'))
else:
# If no team is specified, only medical professionals can access
if not user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional'):
raise UserError(_('You do not have access to this injury.'))
return injury
@http.route(['/my/injury/edit'], type='http', auth='user', website=True)
def edit_injury_form(self, injury_id=None, **post):
"""Show form to edit an existing injury"""
if not injury_id:
return request.redirect('/my/players')
try:
injury = self._check_access_to_injury(injury_id)
except UserError as e:
return request.render('portal.403', {'error': str(e)})
return_url = post.get('return_url', f'/my/player?player_id={injury.patient_id.id}')
# Get possible injury stages - treatment professionals can change stage
stages = []
user = request.env.user
is_treatment_prof = user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
if is_treatment_prof:
stage_selection = request.env['sports.patient.injury']._fields['stage'].selection
stages = [(k, v) for k, v in stage_selection]
# Get body locations for dropdown
body_locations = request.env['sports.body.location'].search([])
# Get possible injury types for dropdown
injury_types = request.env['sports.injury.type'].search([])
# Get possible severity options
severity_options = request.env['sports.patient.injury']._fields['severity'].selection
# Get parental consent options if treatment professional
parental_consent_options = None
if is_treatment_prof:
parental_consent_options = request.env['sports.patient.injury']._fields['parental_consent'].selection
values = {
'injury': injury,
'stages': stages,
'body_locations': body_locations,
'injury_types': injury_types,
'severity_options': severity_options,
'parental_consent_options': parental_consent_options,
'return_url': return_url,
'is_treatment_prof': is_treatment_prof,
'page_name': 'edit_injury',
'error': post.get('error'),
'success': post.get('success'),
}
return request.render('bemade_sports_clinic.portal_edit_injury', values)
@http.route(['/my/injury/save'], type='http', auth='user', website=True, methods=['POST'])
def edit_injury_submit(self, **post):
"""Process the form submission to update an injury"""
injury_id = post.get('injury_id')
if not injury_id:
return request.redirect('/my/players')
try:
injury = self._check_access_to_injury(injury_id)
except UserError as e:
return request.render('portal.403', {'error': str(e)})
# Get user's role
user = request.env.user
is_treatment_prof = user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
# Prepare values for injury update
vals = {}
# Fields everyone can update
vals.update({
'diagnosis': post.get('diagnosis', injury.diagnosis or ''),
'external_notes': post.get('external_notes', injury.external_notes or ''),
})
# Fields only treatment professionals can update
if is_treatment_prof:
# Only add fields that were actually submitted
if post.get('body_location_id'):
vals['body_location_id'] = int(post.get('body_location_id'))
if post.get('injury_type_id'):
vals['injury_type_id'] = int(post.get('injury_type_id'))
if post.get('severity'):
vals['severity'] = post.get('severity')
if post.get('internal_notes'):
vals['internal_notes'] = post.get('internal_notes')
if post.get('stage'):
vals['stage'] = post.get('stage')
if post.get('parental_consent'):
vals['parental_consent'] = post.get('parental_consent')
# Update the injury
injury.sudo().write(vals)
# Add a treatment note if provided
if post.get('treatment_note') and is_treatment_prof:
self._add_treatment_note(injury, post.get('treatment_note'))
# Redirect back to the edit form with success message
return_url = post.get('return_url', f'/my/injury/edit?injury_id={injury_id}')
return request.redirect(f'{return_url}&success=injury_updated')
def _add_treatment_note(self, patient, note_content, injury=None):
"""Helper method to add a treatment note to a patient, optionally linked to an injury"""
if not note_content.strip():
return False
# Create a new treatment note linked to patient, optionally to injury
vals = {
'patient_id': patient.id,
'note': note_content,
'date': fields.Date.today(),
'user_id': request.env.user.id,
}
# If injury is provided, link the note to it
if injury:
vals['injury_id'] = injury.id
request.env['sports.treatment.note'].sudo().create(vals)
return True
@http.route(['/my/injury/notes'], type='http', auth='user', website=True)
def view_treatment_notes(self, injury_id=None, patient_id=None, **post):
"""View treatment notes for an injury or a patient"""
# Determine context - are we viewing injury-specific notes or all patient notes?
# At least one of injury_id or patient_id must be provided
if not injury_id and not patient_id:
return request.redirect('/my/players')
# Get user's role
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
if injury_id:
# Injury context
try:
injury = self._check_access_to_injury(injury_id)
patient = injury.patient_id
except UserError as e:
return request.render('portal.403', {'error': str(e)})
# Get notes for this injury
notes = request.env['sports.treatment.note'].sudo().search(
[('injury_id', '=', int(injury_id))],
order='date desc, id desc'
)
values = {
'injury': injury,
'notes': notes,
'patient': patient,
'is_treatment_prof': is_treatment_prof,
'page_name': 'injury_notes',
'error': post.get('error'),
'success': post.get('success'),
'context': 'injury',
}
else:
# Patient context
try:
patient = self._check_access_to_patient(patient_id)
except UserError as e:
return request.render('portal.403', {'error': str(e)})
# Get all notes for this patient
notes = request.env['sports.treatment.note'].sudo().search(
[('patient_id', '=', int(patient_id))],
order='date desc, id desc'
)
values = {
'injury': None,
'notes': notes,
'patient': patient,
'is_treatment_prof': is_treatment_prof,
'page_name': 'patient_notes',
'error': post.get('error'),
'success': post.get('success'),
'context': 'patient',
}
return request.render('bemade_sports_clinic.portal_treatment_notes', values)
@http.route(['/my/injury/note/add'], type='http', auth='user', website=True, methods=['POST'])
def add_treatment_note(self, **post):
"""Add a new treatment note to a patient, optionally linked to an injury"""
# Get context - are we adding a note to an injury or just to a patient?
injury_id = post.get('injury_id')
patient_id = post.get('patient_id')
# Either injury_id or patient_id must be provided
if not injury_id and not patient_id:
return request.redirect('/my/players')
# Check if user is a treatment professional
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
if not is_treatment_prof:
# Determine redirect URL based on context
if injury_id:
return request.redirect(f'/my/injury/notes?injury_id={injury_id}&error=permission_denied')
else:
return request.redirect(f'/my/injury/notes?patient_id={patient_id}&error=permission_denied')
# Get note content and validate
note_content = post.get('note')
if not note_content or not note_content.strip():
if injury_id:
return request.redirect(f'/my/injury/notes?injury_id={injury_id}&error=empty_note')
else:
return request.redirect(f'/my/injury/notes?patient_id={patient_id}&error=empty_note')
# Determine context and add the note
if injury_id:
# Injury context
try:
injury = self._check_access_to_injury(injury_id)
patient = injury.patient_id
self._add_treatment_note(patient, note_content, injury)
return request.redirect(f'/my/injury/notes?injury_id={injury_id}&success=note_added')
except UserError as e:
return request.render('portal.403', {'error': str(e)})
else:
# Patient context
try:
patient = self._check_access_to_patient(patient_id)
self._add_treatment_note(patient, note_content)
return request.redirect(f'/my/injury/notes?patient_id={patient_id}&success=note_added')
except UserError as e:
return request.render('portal.403', {'error': str(e)})
@http.route(['/my/injury/documents'], type='http', auth='user', website=True)
def view_injury_documents(self, injury_id=None, **post):
"""View documents attached to an injury"""
if not injury_id:
return request.redirect('/my/players')
try:
injury = self._check_access_to_injury(injury_id)
except UserError as e:
return request.render('portal.403', {'error': str(e)})
# Get documents for this injury
documents = request.env['sports.injury.document'].sudo().search(
[('injury_id', '=', int(injury_id))],
order='create_date desc'
)
# Get user's role
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
# Document categories
categories = [('medical', 'Medical'), ('xray', 'X-Ray'), ('mri', 'MRI'),
('prescription', 'Prescription'), ('other', 'Other')]
values = {
'injury': injury,
'documents': documents,
'patient': injury.patient_id,
'is_treatment_prof': is_treatment_prof,
'page_name': 'injury_documents',
'error': post.get('error'),
'success': post.get('success'),
'categories': categories,
}
return request.render('bemade_sports_clinic.portal_injury_documents', values)
@http.route(['/my/injury/document/upload'], type='http', auth='user', website=True, methods=['POST'])
def upload_injury_document(self, **post):
"""Upload a document for an injury"""
injury_id = post.get('injury_id')
if not injury_id:
return request.redirect('/my/players')
try:
injury = self._check_access_to_injury(injury_id)
except UserError as e:
return request.render('portal.403', {'error': str(e)})
# Check if file was uploaded
attachment = post.get('attachment')
if not attachment:
return request.redirect(f'/my/injury/documents?injury_id={injury_id}&error=no_file')
# Process the file
try:
name = attachment.filename
file_content = attachment.read()
file_size = len(file_content)
# Check file size (limit to 10MB)
if file_size > 10 * 1024 * 1024: # 10MB in bytes
return request.redirect(f'/my/injury/documents?injury_id={injury_id}&error=file_too_large')
# Create the document
document = request.env['sports.injury.document'].sudo().create({
'injury_id': int(injury_id),
'name': post.get('document_name', name),
'description': post.get('description', ''),
'category': post.get('category', 'other'),
'file_content': base64.b64encode(file_content),
'file_name': name,
'created_by_id': request.env.user.id,
})
# Redirect back to documents page with success message
return request.redirect(f'/my/injury/documents?injury_id={injury_id}&success=document_uploaded')
except Exception as e:
_logger.error(f"Error uploading document: {e}")
return request.redirect(f'/my/injury/documents?injury_id={injury_id}&error=upload_failed')
@http.route(['/my/injury/document/download/<int:document_id>'], type='http', auth='user')
def download_injury_document(self, document_id, **post):
"""Download a document attached to an injury"""
document = request.env['sports.injury.document'].sudo().browse(int(document_id))
if not document.exists():
return request.not_found()
try:
# Check access to the injury this document belongs to
injury = self._check_access_to_injury(document.injury_id.id)
except UserError:
return request.not_found()
# Return the file for download
return request.make_response(
base64.b64decode(document.file_content),
headers=[
('Content-Type', 'application/octet-stream'),
('Content-Disposition', f'attachment; filename="{document.file_name}"'),
]
)
@http.route(['/my/injury/document/delete/<int:document_id>'], type='http', auth='user', website=True)
def delete_injury_document(self, document_id, **post):
"""Delete a document attached to an injury"""
document = request.env['sports.injury.document'].sudo().browse(int(document_id))
if not document.exists():
return request.not_found()
try:
# Check access to the injury this document belongs to
injury = self._check_access_to_injury(document.injury_id.id)
except UserError:
return request.not_found()
# Check if user is a treatment professional (only they can delete documents)
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
if not is_treatment_prof:
return request.redirect(f'/my/injury/documents?injury_id={document.injury_id.id}&error=permission_denied')
# Delete the document
injury_id = document.injury_id.id
document.sudo().unlink()
# Redirect back to documents page with success message
return request.redirect(f'/my/injury/documents?injury_id={injury_id}&success=document_deleted')
@http.route(['/my/injury/verify'], type='http', auth='user', website=True, methods=['POST'])
def verify_injury(self, injury_id, **post):
"""Verify an injury (change status from unverified to active)"""
try:
injury = request.env['sports.patient.injury'].browse(int(injury_id))
# Check access - user must be a treatment professional or admin
if not (request.env.user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional') or
request.env.user.has_group('base.group_system')):
return request.redirect('/my')
# Verify the injury
injury.sudo().action_verify_injury()
# Redirect back to the player page
return request.redirect(f'/my/player?player_id={injury.patient_id.id}')
except Exception as e:
_logger.error(f"Error verifying injury: {e}")
return request.redirect('/my')