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 .access_control_mixin import AccessControlMixin from datetime import datetime _logger = logging.getLogger(__name__) class PatientInjuryPortal(CustomerPortal, AccessControlMixin): """Controller for all injury reporting functionality in the portal""" # Access control methods now inherited from AccessControlMixin @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('http_routing.http_error', { 'status_code': 403, 'status_message': 'Forbidden', 'error_message': str(e) }) return_url = post.get('return_url', f'/my/player?player_id={patient_id}') # Check if user is a treatment professional # Use request.env.user.has_group() directly to avoid security violations is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') user = request.env.user values = { 'patient': patient, '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('http_routing.http_error', { 'status_code': 403, 'status_message': 'Forbidden', 'error_message': str(e) }) # Since team_id is no longer in the portal form, we'll use the patient's first team # or None if the patient has multiple teams (let the model handle assignment) patient_teams = patient.team_ids team_id = patient_teams[0].id if len(patient_teams) == 1 else None # Check if the current user is a treatment professional is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') # Prepare values for injury creation vals = { 'patient_id': patient.id, 'diagnosis': post.get('diagnosis', ''), 'injury_date': post.get('injury_date'), 'external_notes': post.get('external_notes', ''), 'stage': 'active' if is_treatment_prof else 'unverified', } # Only add team_id if we have a single team for the patient if team_id: vals['team_id'] = int(team_id) # Handle optional fields if post.get('parental_consent'): vals['parental_consent'] = post.get('parental_consent') if post.get('predicted_resolution_date'): vals['predicted_resolution_date'] = post.get('predicted_resolution_date') # 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 # 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 # If user is a treatment professional, add them to the treatment professionals # Only check group membership, not computed field if is_treatment_prof: # Add current user as treatment professional injury.write({ 'treatment_professional_ids': [(4, user.id)] }) else: # User is not a treatment professional pass # Get current treatment professionals treatment_profs = injury.treatment_professional_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 # Process team staff to find therapists for staff in team_staff: if not staff.user_ids: # Try to find a user directly associated with this partner users = request.env['res.users'].sudo().search([('partner_id', '=', staff.partner_id.id)]) # Filter by role head_therapists = team_staff.filtered(lambda s: s.role == 'head_therapist') therapists = team_staff.filtered(lambda s: s.role == 'therapist') # Separate head therapists from regular 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'].search([('partner_id', '=', head_therapist.partner_id.id)]) if users: # Assign head therapist to injury injury.write({ 'treatment_professional_ids': [(4, users[0].id)] }) treatment_pros_assigned = True else: # No user account found for head therapist pass # 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: # Assign therapist to injury injury.write({ 'treatment_professional_ids': [(4, users[0].id)] }) treatment_pros_assigned = True else: # No user account found for therapist pass # 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._compute_is_injured() patient._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) # _check_access_to_injury method now inherited from AccessControlMixin @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('http_routing.http_error', { 'status_code': 403, 'status_message': 'Forbidden', 'error_message': 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 = [] # Use request.env.user.has_group() directly to avoid security violations is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') user = request.env.user if is_treatment_prof: stage_selection = request.env['sports.patient.injury']._fields['stage'].selection stages = [(k, v) for k, v in stage_selection] # Get treatment professionals for the multi-select field treatment_professionals = request.env['res.users'].search([ ('groups_id', 'in', request.env.ref('bemade_sports_clinic.group_portal_treatment_professional').id) ]) # 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, 'treatment_professionals': treatment_professionals, '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('http_routing.http_error', { 'status_code': 403, 'status_message': 'Forbidden', 'error_message': str(e) }) # Get user's role # Use request.env.user.has_group() directly to avoid security violations is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') user = request.env.user # 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 ''), }) # Handle injury date and N/A checkbox if post.get('injury_date_na'): vals['injury_date_na'] = True vals['injury_date'] = False # Clear the date if N/A is checked else: vals['injury_date_na'] = False if post.get('injury_date'): vals['injury_date'] = post.get('injury_date') # Handle resolution dates if post.get('predicted_resolution_date'): vals['predicted_resolution_date'] = post.get('predicted_resolution_date') if post.get('resolution_date'): vals['resolution_date'] = post.get('resolution_date') # Handle treatment professionals (multi-select) if post.get('treatment_professional_ids'): # Convert to list if it's a single value prof_ids = post.get('treatment_professional_ids') if isinstance(prof_ids, str): prof_ids = [prof_ids] elif not isinstance(prof_ids, list): prof_ids = [prof_ids] # Convert to integers and set using Odoo's many2many syntax prof_ids = [int(pid) for pid in prof_ids if pid] vals['treatment_professional_ids'] = [(6, 0, prof_ids)] # Fields only treatment professionals can update if is_treatment_prof: 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: # Add treatment note for injury self._add_treatment_note(injury.patient_id, post.get('treatment_note'), injury) # 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 # Validate patient parameter # 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, } # Create treatment note with prepared values # 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_portal_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('http_routing.http_error', { 'status_code': 403, 'status_message': 'Forbidden', 'error_message': 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('http_routing.http_error', { 'status_code': 403, 'status_message': 'Forbidden', 'error_message': 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_portal_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('http_routing.http_error', { 'status_code': 403, 'status_message': 'Forbidden', 'error_message': str(e) }) else: # Patient context try: patient = self._check_access_to_patient(patient_id) except UserError as e: return request.render('http_routing.http_error', { 'status_code': 403, 'status_message': 'Forbidden', 'error_message': str(e) }) self._add_treatment_note(patient, note_content) return request.redirect(f'/my/injury/notes?patient_id={patient_id}&success=note_added') @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('http_routing.http_error', { 'status_code': 403, 'status_message': 'Forbidden', 'error_message': 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_portal_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('http_routing.http_error', { 'status_code': 403, 'status_message': 'Forbidden', 'error_message': 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/'], 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(): raise 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: raise 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/'], 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(): raise 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: raise 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_portal_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_portal_treatment_professional') or request.env.user.has_group('base.group_system')): return request.redirect('/my') # Verify the injury injury.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')