Portal: fix initial treatment note creation on injury creation; enforce DOB required on player creation (templates + server); injury form team-selection validation and cleanup
This commit is contained in:
parent
3ce8d051bc
commit
aea07cb5a5
8 changed files with 2057 additions and 151 deletions
|
|
@ -97,4 +97,8 @@ This module provides a complete sports medicine clinic management solution with
|
|||
"installable": True,
|
||||
"auto_install": False,
|
||||
"application": True,
|
||||
"assets": {
|
||||
"web.assets_frontend": [
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,21 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
})
|
||||
|
||||
return_url = post.get('return_url', f'/my/player?player_id={patient_id}')
|
||||
# Resolve team context
|
||||
patient_teams = patient.team_ids
|
||||
# Accept team context from both kwargs and request.params (GET)
|
||||
team_id_param = post.get('team_id') or request.params.get('team_id')
|
||||
selected_team_id = None
|
||||
if team_id_param:
|
||||
try:
|
||||
team_id_int = int(team_id_param)
|
||||
except Exception:
|
||||
team_id_int = None
|
||||
if team_id_int and team_id_int in patient_teams.ids:
|
||||
selected_team_id = team_id_int
|
||||
if not selected_team_id and len(patient_teams) == 1:
|
||||
selected_team_id = patient_teams[0].id
|
||||
require_team_selection = len(patient_teams) > 1 and not selected_team_id
|
||||
|
||||
# Check if user is a treatment professional
|
||||
# Use request.env.user.has_group() directly to avoid security violations
|
||||
|
|
@ -57,6 +72,10 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
'is_treatment_prof': is_treatment_prof, # Pass flag to template for conditional display
|
||||
'treatment_professionals': treatment_professionals,
|
||||
'parental_consent_options': parental_consent_options,
|
||||
'patient_teams': patient_teams,
|
||||
'selected_team_id': selected_team_id,
|
||||
'require_team_selection': require_team_selection,
|
||||
'error': post.get('error'),
|
||||
}
|
||||
|
||||
return request.render('bemade_sports_clinic.portal_create_injury', values)
|
||||
|
|
@ -78,10 +97,26 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
'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)
|
||||
# Resolve team from submission or context
|
||||
patient_teams = patient.team_ids
|
||||
team_id = patient_teams[0].id if len(patient_teams) == 1 else None
|
||||
submitted_team = post.get('team_id')
|
||||
team_id = None
|
||||
if submitted_team:
|
||||
try:
|
||||
submitted_team_int = int(submitted_team)
|
||||
except Exception:
|
||||
submitted_team_int = None
|
||||
if submitted_team_int and submitted_team_int in patient_teams.ids:
|
||||
team_id = submitted_team_int
|
||||
else:
|
||||
# Invalid team submitted; re-render form with error
|
||||
return request.redirect(f"/my/patient/injury/new?patient_id={patient.id}&error=invalid_team")
|
||||
else:
|
||||
if len(patient_teams) == 1:
|
||||
team_id = patient_teams[0].id
|
||||
elif len(patient_teams) > 1:
|
||||
# Team selection required when multiple teams and no selection provided
|
||||
return request.redirect(f"/my/patient/injury/new?patient_id={patient.id}&error=team_required")
|
||||
|
||||
# Check if the current user is a treatment professional
|
||||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
|
|
@ -102,7 +137,7 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
vals['injury_date'] = post.get('injury_date')
|
||||
vals['injury_date_na'] = False
|
||||
|
||||
# Only add team_id if we have a single team for the patient
|
||||
# Add team_id if resolved
|
||||
if team_id:
|
||||
vals['team_id'] = int(team_id)
|
||||
|
||||
|
|
@ -157,8 +192,9 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
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
|
||||
# Only when a single team context is determinable
|
||||
if team_id:
|
||||
# Use the resolved team_id (single team) for assignment
|
||||
selected_team_id = int(team_id)
|
||||
|
||||
# Find therapists specifically for this team
|
||||
|
|
@ -217,19 +253,21 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
# If no therapist was assigned, log a warning
|
||||
if not treatment_pros_assigned:
|
||||
_logger.warning("No valid therapists found to assign to the injury")
|
||||
_logger.warning("No valid therapists found to assign to the injury for team %s", selected_team_id)
|
||||
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'):
|
||||
treatment_note_text = post.get('treatment_note').strip()
|
||||
if treatment_note_text:
|
||||
try:
|
||||
# Create treatment note using the injury's _add_treatment_note method
|
||||
injury._add_treatment_note(
|
||||
patient=injury.patient_id,
|
||||
note=treatment_note_text,
|
||||
user=request.env.user
|
||||
)
|
||||
# Create treatment note using the controller helper to ensure correct permissions and linkage
|
||||
self._add_treatment_note(injury.patient_id, treatment_note_text, injury)
|
||||
_logger.info(f"Treatment note added to injury {injury.id} by user {request.env.user.id}")
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to create treatment note for injury {injury.id}: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -242,6 +242,90 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
return request.env['sports.patient'].browse([]) # Empty recordset if no matches
|
||||
|
||||
@http.route(['/my/team/<int:team_id>/player/add_link'], type='http', auth='user', website=True, methods=['GET'])
|
||||
def portal_add_link_player_page(self, team_id, **get):
|
||||
"""Render a dedicated page to search and add/link a player to the team.
|
||||
Server-rendered search results avoid fragile modal JS.
|
||||
"""
|
||||
try:
|
||||
team = self._check_team_access(team_id)
|
||||
|
||||
# Only therapists/admins can add directly; others should use request flow
|
||||
if not (request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or
|
||||
request.env.user.has_group('base.group_system')):
|
||||
request.session['notification'] = {
|
||||
'type': 'danger',
|
||||
'title': _('Access Denied'),
|
||||
'message': _("You don't have permission to add players. You may submit a request instead."),
|
||||
'sticky': False,
|
||||
}
|
||||
return request.redirect(f"/my/team/{team.id}")
|
||||
|
||||
# Gather query params
|
||||
first_name = (get.get('first_name') or '').strip()
|
||||
last_name = (get.get('last_name') or '').strip()
|
||||
dob = (get.get('date_of_birth') or '').strip()
|
||||
# Consider a search attempted if the query params include any of the fields,
|
||||
# even if empty (user pressed Search without filling fields)
|
||||
attempted = any(k in get for k in ('first_name', 'last_name', 'date_of_birth'))
|
||||
# Trigger search when at least one identifier is provided
|
||||
searched = bool(first_name or last_name or dob)
|
||||
|
||||
# Perform search when query present
|
||||
Patient = request.env['sports.patient']
|
||||
active_rs = Patient.browse([])
|
||||
archived_rs = Patient.browse([])
|
||||
if searched and (first_name or last_name):
|
||||
like_first = f"%{first_name}%" if first_name else "%"
|
||||
like_last = f"%{last_name}%" if last_name else "%"
|
||||
domain = [
|
||||
('first_name', 'ilike', like_first),
|
||||
('last_name', 'ilike', like_last),
|
||||
]
|
||||
if dob:
|
||||
domain.append(('date_of_birth', '=', dob))
|
||||
active_rs = Patient.search(domain + [('active', '=', True)], limit=20)
|
||||
if request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
request.env.user.has_group('base.group_system'):
|
||||
archived_rs = Patient.with_context(active_test=False).search(domain + [('active', '=', False)], limit=20)
|
||||
|
||||
def _to_dict(p):
|
||||
return {
|
||||
'id': p.id,
|
||||
'name': p.name,
|
||||
'first_name': p.first_name,
|
||||
'last_name': p.last_name,
|
||||
'date_of_birth': p.date_of_birth or '',
|
||||
'active': bool(p.active),
|
||||
'on_team': team in p.team_ids,
|
||||
}
|
||||
|
||||
values = self._prepare_portal_layout_values()
|
||||
values.update({
|
||||
'page_name': 'add_link_player',
|
||||
'team': team,
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'date_of_birth': dob,
|
||||
'searched': searched,
|
||||
'active_results': [_to_dict(p) for p in active_rs],
|
||||
'archived_results': [_to_dict(p) for p in archived_rs],
|
||||
})
|
||||
if attempted and not searched:
|
||||
values['error'] = _('Provide at least a first or last name to search')
|
||||
return request.render('bemade_sports_clinic.portal_add_link_player_page', values)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect('/my/teams')
|
||||
except Exception:
|
||||
_logger.exception('Error rendering add/link player page')
|
||||
request.session['notification'] = {
|
||||
'type': 'danger',
|
||||
'title': _('Error'),
|
||||
'message': _('Unable to open Add/Link Player page right now.'),
|
||||
'sticky': False,
|
||||
}
|
||||
return request.redirect(f"/my/team/{team_id}")
|
||||
|
||||
@http.route(['/my/team/<int:team_id>/add_player/submit'],
|
||||
type='http', auth="user", website=True, methods=['POST'], csrf=True)
|
||||
def portal_add_player_submit(self, team_id, **post):
|
||||
|
|
@ -254,9 +338,13 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
last_name = (post.get('last_name') or '').strip()
|
||||
email = (post.get('email') or '').strip()
|
||||
phone = (post.get('phone') or '').strip()
|
||||
dob = (post.get('date_of_birth') or '').strip()
|
||||
|
||||
if not first_name or not last_name:
|
||||
raise UserError(_("First name and last name are required"))
|
||||
# Enforce DOB for data integrity
|
||||
if not dob:
|
||||
raise UserError(_("Date of birth is required"))
|
||||
|
||||
# Check for existing player
|
||||
existing_patient = self._find_existing_patient(
|
||||
|
|
@ -305,13 +393,11 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
'team_ids': [(4, team.id)],
|
||||
'email': email or False,
|
||||
'phone': phone or False,
|
||||
'date_of_birth': dob,
|
||||
}
|
||||
|
||||
if post.get('date_of_birth'):
|
||||
patient_vals['date_of_birth'] = post.get('date_of_birth')
|
||||
|
||||
# Create patient through the portal_create_patient method which has proper access controls
|
||||
patient = request.env['sports.patient'].create_portal_patient(patient_vals)
|
||||
# Create patient through the private _create_portal_patient method which has proper access controls
|
||||
patient = request.env['sports.patient']._create_portal_patient(patient_vals)
|
||||
|
||||
# Log the action
|
||||
_logger.debug(
|
||||
|
|
@ -319,10 +405,9 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
patient.name, team.name, request.env.user.name
|
||||
)
|
||||
|
||||
# Redirect to team page with success message
|
||||
return request.redirect(
|
||||
f"/my/team?team_id={team.id}&success=player_created"
|
||||
)
|
||||
# Redirect to edit page with return_url back to team page for two-stage add flow
|
||||
edit_url = f"/my/player/edit?patient_id={patient.id}&return_url=/my/team/{team.id}"
|
||||
return request.redirect(edit_url)
|
||||
|
||||
except UserError as e:
|
||||
values = {
|
||||
|
|
@ -345,3 +430,294 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
}
|
||||
values.update(post)
|
||||
return request.render("bemade_sports_clinic.portal_add_player", values)
|
||||
|
||||
@http.route(['/my/team/<int:team_id>/player/search'], type='json', auth="user", methods=['POST'])
|
||||
def portal_search_player(self, team_id, **post):
|
||||
"""JSON endpoint to search players by name and optional date_of_birth.
|
||||
Includes archived records for treatment professionals/admins.
|
||||
"""
|
||||
try:
|
||||
team = self._check_team_access(team_id)
|
||||
# Basic inputs (confidential: only name + dob)
|
||||
first_name = (post.get('first_name') or '').strip()
|
||||
last_name = (post.get('last_name') or '').strip()
|
||||
dob = (post.get('date_of_birth') or '').strip()
|
||||
|
||||
if not (first_name or last_name):
|
||||
return {'ok': False, 'error': _('Provide at least a first or last name')}
|
||||
|
||||
# Base domain (use ilike for partial, case-insensitive matching)
|
||||
like_first = f"%{first_name}%" if first_name else "%"
|
||||
like_last = f"%{last_name}%" if last_name else "%"
|
||||
domain = [
|
||||
('first_name', 'ilike', like_first),
|
||||
('last_name', 'ilike', like_last),
|
||||
]
|
||||
if dob:
|
||||
domain.append(('date_of_birth', '=', dob))
|
||||
|
||||
# Always search active first
|
||||
Patient = request.env['sports.patient']
|
||||
active_rs = Patient.search(domain + [('active', '=', True)], limit=10)
|
||||
|
||||
# If user can view archived, include them too (need active_test=False)
|
||||
archived_rs = Patient.browse([])
|
||||
if request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
request.env.user.has_group('base.group_system'):
|
||||
archived_rs = Patient.with_context(active_test=False).search(domain + [('active', '=', False)], limit=10)
|
||||
|
||||
def _to_dict(p):
|
||||
return {
|
||||
'id': p.id,
|
||||
'name': p.name,
|
||||
'first_name': p.first_name,
|
||||
'last_name': p.last_name,
|
||||
'date_of_birth': p.date_of_birth or '',
|
||||
'active': bool(p.active),
|
||||
'on_team': team in p.team_ids,
|
||||
}
|
||||
|
||||
return {
|
||||
'ok': True,
|
||||
'active': [_to_dict(p) for p in active_rs],
|
||||
'archived': [_to_dict(p) for p in archived_rs],
|
||||
}
|
||||
except Exception as e:
|
||||
_logger.error('Player search failed: %s', e, exc_info=True)
|
||||
return {'ok': False, 'error': _('Search failed. Please try again.')}
|
||||
|
||||
@http.route(['/my/team/<int:team_id>/player/add'], type='http', auth="user", website=True, methods=['POST'], csrf=True)
|
||||
def portal_add_player_modal_submit(self, team_id, **post):
|
||||
"""Handle modal submission to link existing or create a new player.
|
||||
Only treatment professionals (and admins) can directly add/link.
|
||||
Coaches must use the request route.
|
||||
"""
|
||||
try:
|
||||
team = self._check_team_access(team_id)
|
||||
|
||||
# Enforce role: only treatment professionals/admin can add
|
||||
if not (request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or
|
||||
request.env.user.has_group('base.group_system')):
|
||||
raise AccessError(_("You don't have permission to add players. You may submit a request instead."))
|
||||
|
||||
first_name = (post.get('first_name') or '').strip()
|
||||
last_name = (post.get('last_name') or '').strip()
|
||||
dob = (post.get('date_of_birth') or '').strip()
|
||||
|
||||
Patient = request.env['sports.patient']
|
||||
existing = Patient.browse([])
|
||||
# Parse existing_id from POST if present (link flow)
|
||||
existing_id_str = post.get('existing_id')
|
||||
existing_id = int(existing_id_str) if (existing_id_str and str(existing_id_str).isdigit()) else False
|
||||
|
||||
# Only require basic identity fields when creating/searching, not when linking an explicit existing_id
|
||||
if not existing_id and not (first_name or last_name):
|
||||
raise UserError(_('Provide at least a first or last name'))
|
||||
if existing_id:
|
||||
existing = Patient.with_context(active_test=False).browse(existing_id)
|
||||
else:
|
||||
# Try to find existing by name + optional dob (allow partials on whichever provided)
|
||||
like_first = f"%{first_name}%" if first_name else "%"
|
||||
like_last = f"%{last_name}%" if last_name else "%"
|
||||
domain = [
|
||||
('first_name', 'ilike', like_first),
|
||||
('last_name', 'ilike', like_last),
|
||||
]
|
||||
if dob:
|
||||
domain.append(('date_of_birth', '=', dob))
|
||||
existing = Patient.search(domain + [('active', '=', True)], limit=1)
|
||||
if not existing:
|
||||
if request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
request.env.user.has_group('base.group_system'):
|
||||
existing = Patient.with_context(active_test=False).search(domain + [('active', '=', False)], limit=1)
|
||||
|
||||
if existing:
|
||||
action_taken = []
|
||||
if not existing.active:
|
||||
existing.write({'active': True})
|
||||
action_taken.append('reactivated')
|
||||
if team not in existing.team_ids:
|
||||
existing.write({'team_ids': [(4, team.id)]})
|
||||
action_taken.append('added to team')
|
||||
|
||||
request.session['notification'] = {
|
||||
'type': 'success',
|
||||
'title': _('Player Linked'),
|
||||
'message': _('Existing player %s has been %s.') % (existing.name, ', '.join(action_taken) or _('linked')),
|
||||
'sticky': False,
|
||||
}
|
||||
return request.redirect(f"/my/team/{team.id}")
|
||||
|
||||
# Create new patient (minimal data)
|
||||
vals = {
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'team_ids': [(4, team.id)],
|
||||
}
|
||||
if dob:
|
||||
vals['date_of_birth'] = dob
|
||||
|
||||
patient = request.env['sports.patient']._create_portal_patient(vals)
|
||||
|
||||
request.session['notification'] = {
|
||||
'type': 'success',
|
||||
'title': _('Player Created'),
|
||||
'message': _('%s has been created and added to the team.') % patient.name,
|
||||
'sticky': False,
|
||||
}
|
||||
# Redirect to edit page with return_url back to team page for two-stage add flow
|
||||
edit_url = f"/my/player/edit?patient_id={patient.id}&return_url=/my/team/{team.id}"
|
||||
return request.redirect(edit_url)
|
||||
|
||||
except (AccessError, UserError, ValidationError) as e:
|
||||
request.session['notification'] = {
|
||||
'type': 'danger',
|
||||
'title': _('Add Player Failed'),
|
||||
'message': str(e),
|
||||
'sticky': False,
|
||||
}
|
||||
return request.redirect(f"/my/team/{team_id}")
|
||||
except Exception as e:
|
||||
_logger.exception('Error adding player via modal')
|
||||
request.session['notification'] = {
|
||||
'type': 'danger',
|
||||
'title': _('Add Player Failed'),
|
||||
'message': _('An unexpected error occurred.'),
|
||||
'sticky': False,
|
||||
}
|
||||
return request.redirect(f"/my/team/{team_id}")
|
||||
|
||||
@http.route(['/my/team/<int:team_id>/player/create'], type='http', auth='user', website=True, methods=['POST'], csrf=True)
|
||||
def portal_create_player_submit(self, team_id, **post):
|
||||
"""Create a brand new player and link to team from the Add/Link page.
|
||||
Separate endpoint from link flow to simplify validation and UX.
|
||||
"""
|
||||
try:
|
||||
team = self._check_team_access(team_id)
|
||||
|
||||
# Only therapists/admins can create directly
|
||||
if not (request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or
|
||||
request.env.user.has_group('base.group_system')):
|
||||
raise AccessError(_("You don't have permission to create players."))
|
||||
|
||||
first_name = (post.get('first_name') or '').strip()
|
||||
last_name = (post.get('last_name') or '').strip()
|
||||
dob = (post.get('date_of_birth') or '').strip()
|
||||
email = (post.get('email') or '').strip()
|
||||
phone = (post.get('phone') or '').strip()
|
||||
|
||||
if not first_name or not last_name:
|
||||
raise UserError(_('First name and last name are required'))
|
||||
# Enforce DOB for data integrity
|
||||
if not dob:
|
||||
raise UserError(_('Date of birth is required'))
|
||||
|
||||
vals = {
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'team_ids': [(4, team.id)],
|
||||
'date_of_birth': dob,
|
||||
}
|
||||
if email:
|
||||
vals['email'] = email
|
||||
if phone:
|
||||
vals['phone'] = phone
|
||||
|
||||
patient = request.env['sports.patient']._create_portal_patient(vals)
|
||||
|
||||
request.session['notification'] = {
|
||||
'type': 'success',
|
||||
'title': _('Player Created'),
|
||||
'message': _('%s has been created and added to the team.') % patient.name,
|
||||
'sticky': False,
|
||||
}
|
||||
# Redirect to edit page with return_url back to team page for two-stage add flow
|
||||
edit_url = f"/my/player/edit?patient_id={patient.id}&return_url=/my/team/{team.id}"
|
||||
return request.redirect(edit_url)
|
||||
|
||||
except (AccessError, UserError, ValidationError) as e:
|
||||
request.session['notification'] = {
|
||||
'type': 'danger',
|
||||
'title': _('Create Player Failed'),
|
||||
'message': str(e),
|
||||
'sticky': False,
|
||||
}
|
||||
return request.redirect(f"/my/team/{team_id}")
|
||||
except Exception:
|
||||
_logger.exception('Error creating player from add/link page')
|
||||
request.session['notification'] = {
|
||||
'type': 'danger',
|
||||
'title': _('Create Player Failed'),
|
||||
'message': _('An unexpected error occurred.'),
|
||||
'sticky': False,
|
||||
}
|
||||
return request.redirect(f"/my/team/{team_id}")
|
||||
|
||||
@http.route(['/my/team/<int:team_id>/player/request_add'], type='http', auth='user', website=True, methods=['POST'], csrf=True)
|
||||
def portal_request_player_add(self, team_id, **post):
|
||||
"""Coaches submit a request to add a player. Creates a mail.activity assigned to head therapist or fallback admin."""
|
||||
try:
|
||||
team = self._check_team_access(team_id, check_staff=True)
|
||||
|
||||
# Only non-therapists need this route; therapists should use direct add
|
||||
if request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
request.env.user.has_group('base.group_system'):
|
||||
raise AccessError(_('You can add players directly.'))
|
||||
|
||||
first_name = (post.get('first_name') or '').strip()
|
||||
last_name = (post.get('last_name') or '').strip()
|
||||
dob = (post.get('date_of_birth') or '').strip()
|
||||
reason = (post.get('reason') or '').strip()
|
||||
|
||||
if not first_name or not last_name:
|
||||
raise UserError(_('First name and last name are required'))
|
||||
|
||||
# Find head therapist (reuse model helper on team if available)
|
||||
# Fallback to admin if none
|
||||
head_user = False
|
||||
if hasattr(team, 'head_therapist_id') and team.head_therapist_id:
|
||||
head_user = team.head_therapist_id.user_ids[:1]
|
||||
if not head_user:
|
||||
head_user = request.env.ref('base.user_admin', raise_if_not_found=False)
|
||||
|
||||
activity_vals = {
|
||||
'res_model': 'sports.team',
|
||||
'res_id': team.id,
|
||||
'summary': _('Coach requests player addition'),
|
||||
'note': _('Requested player: %s %s%s\nReason: %s') % (
|
||||
first_name,
|
||||
last_name,
|
||||
(f" (DOB: {dob})" if dob else ''),
|
||||
(reason or _('No reason provided')),
|
||||
),
|
||||
}
|
||||
if head_user:
|
||||
activity_vals['user_id'] = head_user.id
|
||||
|
||||
request.env['mail.activity'].sudo().create(activity_vals)
|
||||
|
||||
request.session['notification'] = {
|
||||
'type': 'success',
|
||||
'title': _('Request Submitted'),
|
||||
'message': _('Your request to add a player has been sent to the head therapist.'),
|
||||
'sticky': False,
|
||||
}
|
||||
return request.redirect(f"/my/team/{team.id}")
|
||||
|
||||
except (AccessError, UserError, ValidationError) as e:
|
||||
request.session['notification'] = {
|
||||
'type': 'danger',
|
||||
'title': _('Request Failed'),
|
||||
'message': str(e),
|
||||
'sticky': False,
|
||||
}
|
||||
return request.redirect(f"/my/team/{team_id}")
|
||||
except Exception as e:
|
||||
_logger.exception('Error requesting player addition')
|
||||
request.session['notification'] = {
|
||||
'type': 'danger',
|
||||
'title': _('Request Failed'),
|
||||
'message': _('An unexpected error occurred.'),
|
||||
'sticky': False,
|
||||
}
|
||||
return request.redirect(f"/my/team/{team_id}")
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -189,7 +189,10 @@ class Patient(models.Model):
|
|||
for row in vals_list:
|
||||
if "partner_id" not in row:
|
||||
row["partner_id"] = (
|
||||
self.env["res.partner"]
|
||||
self.env["res.partner"].with_context(
|
||||
tracking_disable=True,
|
||||
mail_create_nosubscribe=True,
|
||||
)
|
||||
.create(
|
||||
{
|
||||
"name": self._get_name_from_first_and_last(
|
||||
|
|
@ -200,7 +203,13 @@ class Patient(models.Model):
|
|||
.id
|
||||
)
|
||||
res = super().create(vals_list)
|
||||
res.sudo().recompute_followers()
|
||||
# Avoid triggering follower recomputation (which can create mail/follower
|
||||
# side-effects) when explicitly asked to skip, e.g., during portal creation.
|
||||
if not self.env.context.get("skip_recompute_followers"):
|
||||
res.sudo().with_context(
|
||||
tracking_disable=True,
|
||||
mail_create_nosubscribe=True,
|
||||
).recompute_followers()
|
||||
return res
|
||||
|
||||
@api.constrains("match_status", "practice_status")
|
||||
|
|
@ -830,7 +839,12 @@ class Patient(models.Model):
|
|||
'phone': vals.get('phone', False),
|
||||
'type': 'contact',
|
||||
}
|
||||
partner = self.env['res.partner'].with_context(mail_create_nosubscribe=True).create(partner_vals)
|
||||
partner = (
|
||||
self.env['res.partner']
|
||||
.sudo()
|
||||
.with_context(tracking_disable=True, mail_create_nosubscribe=True)
|
||||
.create(partner_vals)
|
||||
)
|
||||
|
||||
# Prepare patient values
|
||||
patient_vals = {
|
||||
|
|
@ -845,8 +859,23 @@ class Patient(models.Model):
|
|||
if 'date_of_birth' in vals and vals['date_of_birth']:
|
||||
patient_vals['date_of_birth'] = vals['date_of_birth']
|
||||
|
||||
# Create patient with normal permissions
|
||||
return self.create(patient_vals)
|
||||
# Create patient with tracking disabled to avoid triggering mail/report side-effects
|
||||
# Also disable auto-subscriptions on creation
|
||||
patient = self.sudo().with_context(
|
||||
tracking_disable=True,
|
||||
mail_create_nosubscribe=True,
|
||||
skip_recompute_followers=True,
|
||||
).create(patient_vals)
|
||||
|
||||
# Optionally recompute followers immediately if explicitly requested by context.
|
||||
# Disabled by default for portal flows to avoid mail/report side-effects (e.g., ir.actions.report ACL reads).
|
||||
if self.env.context.get('portal_recompute_followers_post_create'):
|
||||
patient.sudo().with_context(
|
||||
tracking_disable=True,
|
||||
mail_create_nosubscribe=True,
|
||||
).recompute_followers()
|
||||
|
||||
return patient
|
||||
|
||||
def recompute_followers(self):
|
||||
"""Recompute the followers for this patient (and its injuries) based on the
|
||||
|
|
|
|||
|
|
@ -470,4 +470,141 @@
|
|||
|
||||
<!-- Emergency contacts section now integrated directly into the portal_my_player_injuries template -->
|
||||
<!-- All buttons and emergency contacts are now integrated directly into the portal_my_player_injuries template -->
|
||||
|
||||
<!-- Add/Link Player dedicated page (server-rendered search) -->
|
||||
<template id="portal_add_link_player_page" name="Add or Link Player">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="container pt-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="mb-0">Add or Link Player</h2>
|
||||
<a t-attf-href="/my/team?team_id={{ team.id }}" class="btn bg-o-color-2 text-white">
|
||||
<i class="fa fa-arrow-left me-1"/> Back to Team
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<form t-attf-action="/my/team/{{ team.id }}/player/add_link" method="get" class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">First Name</label>
|
||||
<input type="text" name="first_name" class="form-control" t-att-value="first_name"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Last Name</label>
|
||||
<input type="text" name="last_name" class="form-control" t-att-value="last_name"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Date of Birth</label>
|
||||
<input type="date" name="date_of_birth" class="form-control" t-att-value="date_of_birth"/>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-end">
|
||||
<button type="submit" class="btn bg-o-color-1 text-white"><i class="fa fa-search me-1"/> Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-if="searched">
|
||||
<t t-set="has_active" t-value="len(active_results) > 0"/>
|
||||
<t t-set="has_archived" t-value="len(archived_results) > 0"/>
|
||||
|
||||
<t t-if="has_active or has_archived">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<h4>Search Results</h4>
|
||||
</div>
|
||||
|
||||
<div class="col-12" t-if="has_active">
|
||||
<div class="card">
|
||||
<div class="card-header bg-success text-white">Active Players</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<t t-foreach="active_results" t-as="p">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong t-esc="p['name']"/> <span class="text-muted" t-if="p['date_of_birth']">(DOB: <t t-esc="p['date_of_birth']"/>)</span>
|
||||
<span t-if="p['on_team']" class="badge bg-info ms-2">Already on Team</span>
|
||||
</div>
|
||||
<form t-if="not p['on_team']" t-attf-action="/my/team/{{ team.id }}/player/add" method="post" class="m-0">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" name="existing_id" t-att-value="p['id']"/>
|
||||
<button type="submit" class="btn bg-o-color-1 text-white btn-sm"><i class="fa fa-link me-1"/> Link to Team</button>
|
||||
</form>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12" t-if="has_archived">
|
||||
<div class="card">
|
||||
<div class="card-header bg-secondary text-white">Archived Players</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<t t-foreach="archived_results" t-as="p">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong t-esc="p['name']"/> <span class="text-muted" t-if="p['date_of_birth']">(DOB: <t t-esc="p['date_of_birth']"/>)</span>
|
||||
<span class="badge bg-secondary ms-2">Archived</span>
|
||||
<span t-if="p['on_team']" class="badge bg-info ms-2">Already on Team</span>
|
||||
</div>
|
||||
<form t-if="not p['on_team']" t-attf-action="/my/team/{{ team.id }}/player/add" method="post" class="m-0">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" name="existing_id" t-att-value="p['id']"/>
|
||||
<button type="submit" class="btn bg-o-color-1 text-white btn-sm"><i class="fa fa-link me-1"/> Link to Team</button>
|
||||
</form>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-else="">
|
||||
<div class="alert alert-info">
|
||||
<i class="fa fa-info-circle me-1"/> No matching players found. You can also create a new player below.
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!-- Show Create New Player form ONLY when a search was performed and no matches were found -->
|
||||
<t t-if="searched">
|
||||
<t t-set="has_active" t-value="len(active_results) > 0"/>
|
||||
<t t-set="has_archived" t-value="len(archived_results) > 0"/>
|
||||
<t t-if="not has_active and not has_archived">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Create New Player</div>
|
||||
<div class="card-body">
|
||||
<form t-attf-action="/my/team/{{ team.id }}/player/create" method="post" class="row g-3">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">First Name <span class="text-danger">*</span></label>
|
||||
<input type="text" name="first_name" class="form-control" required="required" t-att-value="first_name"/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Last Name <span class="text-danger">*</span></label>
|
||||
<input type="text" name="last_name" class="form-control" required="required" t-att-value="last_name"/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Date of Birth <span class="text-danger">*</span></label>
|
||||
<input type="date" name="date_of_birth" class="form-control" t-att-value="date_of_birth" required="required"/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" name="email" class="form-control"/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Phone</label>
|
||||
<input type="tel" name="phone" class="form-control"/>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-end">
|
||||
<button type="submit" class="btn bg-o-color-1 text-white"><i class="fa fa-save me-1"/> Create Player</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -215,11 +215,63 @@
|
|||
<a t-if="is_treatment_prof" t-attf-href="/my/activity/create?model=sports.team&res_id={{ team.id }}" class="btn bg-o-color-2 text-white me-2">
|
||||
<i class="fa fa-tasks"></i> Add Activity
|
||||
</a>
|
||||
<a t-attf-href="/my/team/{{ team.id }}/add_player" class="btn bg-o-color-1 text-white">
|
||||
<!-- Therapist: go to dedicated Add/Link Player page -->
|
||||
<a t-if="user_has_group('bemade_sports_clinic.group_portal_treatment_professional') or user_has_group('base.group_system')"
|
||||
t-attf-href="/my/team/{{ team.id }}/player/add_link" class="btn bg-o-color-1 text-white">
|
||||
<i class="fa fa-plus me-1"/> Add Player
|
||||
</a>
|
||||
<!-- Coach: open Request Add Player modal -->
|
||||
<button t-else="" type="button" class="btn bg-o-color-1 text-white"
|
||||
data-bs-toggle="modal" t-attf-data-bs-target="#requestAddPlayerModal{{ team.id }}">
|
||||
<i class="fa fa-plus me-1"/> Request Player Addition
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Therapist Add Player Modal removed in favor of dedicated page -->
|
||||
|
||||
<!-- Coach Request Add Player Modal -->
|
||||
<div t-if="not (user_has_group('bemade_sports_clinic.group_portal_treatment_professional') or user_has_group('base.group_system'))" t-attf-id="requestAddPlayerModal{{ team.id }}" class="modal fade" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Request Player Addition</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form t-attf-action="/my/team/{{ team.id }}/player/request_add" method="post">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">First Name</label>
|
||||
<input type="text" name="first_name" class="form-control" required="required"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Last Name</label>
|
||||
<input type="text" name="last_name" class="form-control" required="required"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Date of Birth</label>
|
||||
<input type="date" name="date_of_birth" class="form-control"/>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Reason</label>
|
||||
<textarea name="reason" class="form-control" rows="3" placeholder="Explain why this player should be added"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn bg-o-color-1 text-white">
|
||||
<i class="fa fa-paper-plane"></i> Submit Request
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Pending Removals Alert -->
|
||||
<t t-if="any(p.pending_removal for p in players) and user_has_group('bemade_sports_clinic.group_portal_treatment_professional')">
|
||||
|
|
@ -1071,9 +1123,9 @@
|
|||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="date_of_birth" class="form-label">Date of Birth</label>
|
||||
<label for="date_of_birth" class="form-label">Date of Birth *</label>
|
||||
<input type="date" class="form-control" id="date_of_birth" name="date_of_birth"
|
||||
t-att-value="date_of_birth or ''"/>
|
||||
t-att-value="date_of_birth or ''" required="required"/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@
|
|||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" name="patient_id" t-att-value="patient.id"/>
|
||||
<input type="hidden" name="return_url" t-att-value="return_url"/>
|
||||
<!-- Error messages -->
|
||||
<div t-if="error" class="alert alert-danger">
|
||||
<t t-if="error == 'team_required'">Please select a team for this injury.</t>
|
||||
<t t-elif="error == 'invalid_team'">The selected team is not valid for this player.</t>
|
||||
<t t-else="">An error occurred. Please review the form.</t>
|
||||
</div>
|
||||
|
||||
<!-- Top Action Buttons -->
|
||||
<div class="row mb-4">
|
||||
|
|
@ -52,6 +58,24 @@
|
|||
<h4 class="mb-0">Injury Details</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Team selection -->
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label for="team_id">Team</label>
|
||||
<select name="team_id" id="team_id" class="form-control"
|
||||
t-att-required="require_team_selection and 'required' or False">
|
||||
<option value="" t-if="require_team_selection">-- Select team --</option>
|
||||
<t t-foreach="patient_teams" t-as="t">
|
||||
<option t-att-value="t.id" t-att-selected="selected_team_id == t.id and 'selected' or False">
|
||||
<t t-esc="t.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<small class="text-muted">The selected team determines assigned therapists.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Basic injury information -->
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
|
|
|
|||
Loading…
Reference in a new issue