- Stack action buttons onto a second line in list/detail views\n- Remove redundant team/venue under event title (retained in details card)\n- Minor styling/consistency tweaks
1026 lines
50 KiB
Python
1026 lines
50 KiB
Python
from odoo import http, fields, _
|
|
from odoo.http import request
|
|
from odoo.addons.portal.controllers.portal import CustomerPortal
|
|
from odoo.exceptions import AccessError, MissingError, UserError, ValidationError
|
|
from .access_control_mixin import AccessControlMixin
|
|
import logging
|
|
from datetime import date
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|
|
|
def _prepare_home_portal_values(self, counters):
|
|
values = super()._prepare_home_portal_values(counters)
|
|
return values
|
|
|
|
# Access control methods now inherited from AccessControlMixin
|
|
|
|
@http.route(['/my/team/<int:team_id>/player/<int:player_id>/request_removal'],
|
|
type='http', auth="user", website=True, methods=['POST'])
|
|
def portal_request_player_removal(self, team_id, player_id, **post):
|
|
"""
|
|
Request removal of a player from the team.
|
|
This is used by coaches to request removal, which creates a task for the head therapist.
|
|
"""
|
|
try:
|
|
team = self._check_team_access(team_id, check_staff=True)
|
|
patient = request.env['sports.patient'].browse(int(player_id))
|
|
|
|
if not patient.exists():
|
|
raise MissingError(_("Player not found"))
|
|
|
|
if team not in patient.team_ids:
|
|
raise ValidationError(_("Player is not a member of this team"))
|
|
|
|
# Check if there's already a pending removal
|
|
if patient.pending_removal:
|
|
raise ValidationError(_("A removal request is already pending for this player"))
|
|
|
|
# Get and validate reason
|
|
reason = (post.get('reason') or '').strip()
|
|
if not reason:
|
|
raise ValidationError(_("Please provide a reason for the removal request"))
|
|
|
|
# Request removal (this will handle the activity creation and logging)
|
|
# No sudo() needed as proper permission checks are in request_team_removal
|
|
patient._request_team_removal(team.id, reason=reason)
|
|
|
|
# Store success message in session for display after redirect
|
|
request.session['notification'] = {
|
|
'type': 'success',
|
|
'title': _('Removal Request Submitted'),
|
|
'message': _('Your request to remove %s from the team has been submitted for review.') % patient.name,
|
|
'sticky': False,
|
|
}
|
|
|
|
return request.redirect(f"/my/team/{team_id}")
|
|
|
|
except Exception as e:
|
|
_logger.error("Error requesting player removal: %s", str(e), exc_info=True)
|
|
error_message = _("Error requesting removal: %s") % str(e)
|
|
return request.redirect(f"/my/team/{team_id}?error={error_message}".replace(' ', '+'))
|
|
|
|
@http.route(['/my/team/<int:team_id>/player/<int:player_id>/remove'],
|
|
type='http', auth="user", website=True, methods=['POST'])
|
|
def portal_remove_player(self, team_id, player_id, **post):
|
|
"""
|
|
Directly remove a player from the team.
|
|
Only accessible by treatment professionals or team staff.
|
|
"""
|
|
try:
|
|
team = self._check_team_access(team_id)
|
|
|
|
# Only treatment professionals or team staff can directly remove
|
|
if not (self._check_treatment_professional_access() or self._check_team_staff_access(team)):
|
|
raise AccessError(_("You don't have permission to remove players from this team."))
|
|
|
|
patient = request.env['sports.patient'].browse(int(player_id))
|
|
if not patient.exists():
|
|
raise MissingError(_("Player not found"))
|
|
|
|
if team not in patient.team_ids:
|
|
raise ValidationError(_("Player is not a member of this team"))
|
|
|
|
# Check if this is a pending removal that's being approved
|
|
is_approving_pending = patient.pending_removal and self._check_treatment_professional_access()
|
|
|
|
# Process removal with the appropriate action - no sudo needed as remove_from_team has built-in permission checks
|
|
result = patient._remove_from_team(team.id, clear_pending=True)
|
|
|
|
# Store success message in session for display after redirect
|
|
request.session['notification'] = {
|
|
'type': 'success',
|
|
'title': _('Player Removed'),
|
|
'message': _('%s has been successfully removed from the team.') % patient.name,
|
|
'sticky': False,
|
|
}
|
|
|
|
return request.redirect(f"/my/team/{team_id}")
|
|
|
|
except Exception as e:
|
|
_logger.error("Error removing player: %s", str(e), exc_info=True)
|
|
error_message = _("Error removing player: %s") % str(e)
|
|
return request.redirect(f"/my/team/{team_id}?error={error_message}".replace(' ', '+'))
|
|
|
|
@http.route(['/my/team/<int:team_id>/add_player'],
|
|
type='http', auth="user", website=True)
|
|
def portal_add_player_form(self, team_id, **kw):
|
|
"""Display the form to add a new player to a team."""
|
|
try:
|
|
team = self._check_team_access(team_id)
|
|
values = self._prepare_portal_layout_values()
|
|
|
|
# Check for success/error messages
|
|
success = request.httprequest.args.get('success')
|
|
if success == 'player_reactivated':
|
|
values['success'] = _("An archived player was found and reactivated, and has been added to this team.")
|
|
elif success == 'player_added_to_team':
|
|
values['success'] = _("An existing player was found and has been added to this team.")
|
|
elif success == 'player_created':
|
|
values['success'] = _("A new player has been created and added to the team.")
|
|
|
|
values.update({
|
|
'team': team,
|
|
'page_name': 'add_player',
|
|
'error': request.httprequest.args.get('error'),
|
|
})
|
|
|
|
# Preserve form data if there was an error
|
|
if kw.get('error'):
|
|
values.update({
|
|
'first_name': kw.get('first_name', ''),
|
|
'last_name': kw.get('last_name', ''),
|
|
'email': kw.get('email', ''),
|
|
'phone': kw.get('phone', ''),
|
|
'date_of_birth': kw.get('date_of_birth', ''),
|
|
})
|
|
|
|
return request.render("bemade_sports_clinic.portal_add_player", values)
|
|
|
|
except (AccessError, MissingError) as e:
|
|
return request.redirect('/my')
|
|
except Exception as e:
|
|
_logger.exception("Error in portal_add_player_form")
|
|
values = request.params.copy()
|
|
values['error'] = _("An error occurred while loading the form. Please try again.")
|
|
return request.render("bemade_sports_clinic.portal_add_player", values)
|
|
|
|
@http.route(['/my/team', '/my/team/<int:team_id>'], type='http', auth="user", website=True)
|
|
def portal_team_players(self, team_id=None, **kw):
|
|
"""Display the list of players for a team."""
|
|
try:
|
|
if not team_id:
|
|
# If no team_id is provided, try to get it from the query string
|
|
team_id = request.httprequest.args.get('team_id')
|
|
if not team_id:
|
|
# If still no team_id, redirect to the teams list
|
|
return request.redirect('/my/teams')
|
|
|
|
team = self._check_team_access(team_id)
|
|
|
|
# Get all players for the team
|
|
players = request.env['sports.patient'].search([
|
|
('team_ids', 'in', [team.id]),
|
|
('active', '=', True)
|
|
], order='last_name, first_name')
|
|
|
|
# Check user permissions for UI elements
|
|
is_treatment_prof = request.env.user.has_group(
|
|
'bemade_sports_clinic.group_portal_treatment_professional')
|
|
is_admin = request.env.user.has_group('base.group_system')
|
|
is_team_staff = team.staff_ids.filtered(
|
|
lambda s: request.env.user.partner_id in s.user_ids.partner_id
|
|
)
|
|
|
|
values = {
|
|
'page_name': 'team_players',
|
|
'team': team,
|
|
'players': players,
|
|
'default_url': f'/my/team/{team.id}',
|
|
'user_has_group': request.env.user.has_group, # Pass the has_group method to template
|
|
'user': request.env.user,
|
|
'is_treatment_prof': is_treatment_prof or is_admin,
|
|
'is_team_staff': bool(is_team_staff),
|
|
}
|
|
|
|
# Add success/error messages if present in the URL
|
|
success = request.httprequest.args.get('success')
|
|
error = request.httprequest.args.get('error')
|
|
|
|
if success == 'player_removed':
|
|
values['success'] = _("Player has been successfully removed from the team.")
|
|
elif success == 'removal_requested':
|
|
values['success'] = _("A request to remove this player has been submitted to the head therapist.")
|
|
elif success == 'player_reactivated':
|
|
values['success'] = _("An archived player was found and reactivated, and has been added to this team.")
|
|
elif success == 'player_added_to_team':
|
|
values['success'] = _("Player has been added to the team.")
|
|
|
|
if error:
|
|
values['error'] = error
|
|
|
|
return request.render('bemade_sports_clinic.portal_my_team_players', values)
|
|
|
|
except (AccessError, MissingError) as e:
|
|
return request.redirect('/my/teams?error=%s' % str(e))
|
|
|
|
def _find_existing_patient(self, first_name, last_name, email=None, phone=None):
|
|
"""Search for an existing patient by name and contact information."""
|
|
domain = [
|
|
('first_name', '=ilike', first_name.strip()),
|
|
('last_name', '=ilike', last_name.strip()),
|
|
'|',
|
|
('active', '=', True),
|
|
('active', '=', False), # Include inactive to handle archived players
|
|
]
|
|
|
|
# Additional search criteria if email or phone is provided
|
|
if email and email.strip():
|
|
domain = ['|'] + domain + [
|
|
'&',
|
|
('partner_id.email', '=ilike', email.strip()),
|
|
('partner_id.email', '!=', False)
|
|
]
|
|
if phone and phone.strip():
|
|
domain = ['|'] + domain + [
|
|
'&',
|
|
('partner_id.phone', '=', phone.strip()),
|
|
('partner_id.phone', '!=', False)
|
|
]
|
|
|
|
# Search active records first (portal users always have access to active records)
|
|
active_patient = request.env['sports.patient'].search(domain + [('active', '=', True)], limit=1)
|
|
if active_patient:
|
|
return active_patient
|
|
|
|
# If no active patient found, check if user has permission to see inactive records
|
|
# Only treatment professionals or admins should see inactive/archived patients
|
|
# Use request.env.user.has_group() directly to avoid security violations
|
|
if request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
|
request.env.user.has_group('base.group_system'):
|
|
return request.env['sports.patient'].search(domain + [('active', '=', False)], limit=1)
|
|
|
|
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],
|
|
# Extra context to support inline full create form
|
|
'is_treatment_prof': request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or request.env.user.has_group('base.group_system'),
|
|
'states': request.env['res.country.state'].search([('country_id.code', '=', 'CA')], order='name'),
|
|
'countries': request.env['res.country'].search([('code', '=', 'CA')], limit=1) or request.env['res.country'].search([], order='name'),
|
|
'all_teams': request.env['sports.team'].search([
|
|
('id', 'in', request.env['sports.team.staff'].search([
|
|
('partner_id', '=', request.env.user.partner_id.id)
|
|
]).mapped('team_id').ids)
|
|
], order='name'),
|
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
|
'prefill_team_id': team.id,
|
|
})
|
|
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>/player/create_full'], type='http', auth='user', website=True, methods=['POST'], csrf=True)
|
|
def portal_create_player_full(self, team_id, **post):
|
|
"""Create a brand new player with full details (inline edit form) and link to team.
|
|
This is used when the search yields no results and we render the full form inline.
|
|
Only treatment professionals/admins may create directly.
|
|
"""
|
|
try:
|
|
team = self._check_team_access(team_id)
|
|
|
|
# Permission: only therapists/admins can create directly
|
|
is_tp = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
|
request.env.user.has_group('base.group_system')
|
|
if not is_tp:
|
|
raise AccessError(_("You don't have permission to create players."))
|
|
|
|
# Required basics
|
|
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 not last_name:
|
|
raise UserError(_('First name and last name are required'))
|
|
if not dob:
|
|
raise UserError(_('Date of birth is required'))
|
|
|
|
# Validate DOB format explicitly to avoid ValueError down in ORM
|
|
try:
|
|
dob_date = fields.Date.to_date(dob)
|
|
except Exception:
|
|
values = self._prepare_portal_layout_values()
|
|
values.update({
|
|
'page_name': 'add_link_player',
|
|
'team': team,
|
|
'first_name': post.get('first_name') or '',
|
|
'last_name': post.get('last_name') or '',
|
|
'date_of_birth': dob,
|
|
'searched': True,
|
|
'active_results': [],
|
|
'archived_results': [],
|
|
'is_treatment_prof': request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or request.env.user.has_group('base.group_system'),
|
|
'states': request.env['res.country.state'].search([('country_id.code', '=', 'CA')], order='name'),
|
|
'countries': request.env['res.country'].search([('code', '=', 'CA')], limit=1) or request.env['res.country'].search([], order='name'),
|
|
'all_teams': request.env['sports.team'].search([
|
|
('id', 'in', request.env['sports.team.staff'].search([
|
|
('partner_id', '=', request.env.user.partner_id.id)
|
|
]).mapped('team_id').ids)
|
|
], order='name'),
|
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
|
'form_data': dict(post),
|
|
'error': _('Please enter a valid Date of Birth (YYYY-MM-DD).'),
|
|
'prefill_team_id': team.id,
|
|
})
|
|
return request.render('bemade_sports_clinic.portal_add_link_player_page', values)
|
|
|
|
# Enforce DOB not in future and not older than 120 years
|
|
today = fields.Date.context_today(request.env.user) or fields.Date.today()
|
|
min_dob = today - relativedelta(years=120)
|
|
if dob_date > today or dob_date < min_dob:
|
|
values = self._prepare_portal_layout_values()
|
|
values.update({
|
|
'page_name': 'add_link_player',
|
|
'team': team,
|
|
'first_name': post.get('first_name') or '',
|
|
'last_name': post.get('last_name') or '',
|
|
'date_of_birth': dob,
|
|
'searched': True,
|
|
'active_results': [],
|
|
'archived_results': [],
|
|
'is_treatment_prof': request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or request.env.user.has_group('base.group_system'),
|
|
'states': request.env['res.country.state'].search([('country_id.code', '=', 'CA')], order='name'),
|
|
'countries': request.env['res.country'].search([('code', '=', 'CA')], limit=1) or request.env['res.country'].search([], order='name'),
|
|
'all_teams': request.env['sports.team'].search([
|
|
('id', 'in', request.env['sports.team.staff'].search([
|
|
('partner_id', '=', request.env.user.partner_id.id)
|
|
]).mapped('team_id').ids)
|
|
], order='name'),
|
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
|
'form_data': dict(post),
|
|
'error': _('Date of Birth must not be in the future and not be more than 120 years ago.'),
|
|
'prefill_team_id': team.id,
|
|
})
|
|
return request.render('bemade_sports_clinic.portal_add_link_player_page', values)
|
|
|
|
# Build values similar to edit form, honoring permissions
|
|
# Collect team_ids from multi-select; ensure current team is included by default
|
|
try:
|
|
selected_team_ids = [int(tid) for tid in request.httprequest.form.getlist('team_ids')]
|
|
except Exception:
|
|
selected_team_ids = []
|
|
if not selected_team_ids:
|
|
selected_team_ids = [team.id]
|
|
if team.id not in selected_team_ids:
|
|
selected_team_ids.append(team.id)
|
|
# Restrict to teams where current user is staff (defense-in-depth against tampering)
|
|
allowed_team_ids = request.env['sports.team.staff'].search([
|
|
('partner_id', '=', request.env.user.partner_id.id)
|
|
]).mapped('team_id').ids
|
|
selected_team_ids = [tid for tid in selected_team_ids if tid in allowed_team_ids]
|
|
vals = {
|
|
'first_name': first_name,
|
|
'last_name': last_name,
|
|
'date_of_birth': dob,
|
|
'team_ids': [(6, 0, list(set(selected_team_ids)))],
|
|
}
|
|
|
|
# Contact info
|
|
email = (post.get('email') or '').strip()
|
|
phone = (post.get('phone') or '').strip()
|
|
if email:
|
|
vals['email'] = email
|
|
if phone:
|
|
vals['phone'] = phone
|
|
|
|
# Address info
|
|
for f in ['street', 'street2', 'city', 'zip']:
|
|
if f in post:
|
|
vals[f] = post.get(f) or False
|
|
# State/Country
|
|
if post.get('state_id'):
|
|
try:
|
|
vals['state_id'] = int(post.get('state_id'))
|
|
except Exception:
|
|
pass
|
|
if post.get('country_id'):
|
|
try:
|
|
vals['country_id'] = int(post.get('country_id'))
|
|
except Exception:
|
|
pass
|
|
|
|
# Extra medical/status fields (only for TP/admin)
|
|
if is_tp:
|
|
if 'allergies' in post:
|
|
vals['allergies'] = post.get('allergies') or False
|
|
if 'team_info_notes' in post:
|
|
vals['team_info_notes'] = post.get('team_info_notes') or False
|
|
if post.get('match_status'):
|
|
vals['match_status'] = post.get('match_status')
|
|
if post.get('practice_status'):
|
|
vals['practice_status'] = post.get('practice_status')
|
|
|
|
# Create through secured helper
|
|
patient = request.env['sports.patient']._create_portal_patient(vals)
|
|
|
|
# Optionally create a primary emergency contact if provided (TP/admin only)
|
|
if is_tp:
|
|
ec_name = (post.get('ec_name') or '').strip()
|
|
ec_type = (post.get('ec_contact_type') or '').strip()
|
|
if ec_name and ec_type:
|
|
contact_vals = {
|
|
'patient_id': patient.id,
|
|
'name': ec_name,
|
|
'contact_type': ec_type,
|
|
}
|
|
if post.get('ec_mobile'):
|
|
contact_vals['mobile'] = post.get('ec_mobile')
|
|
if post.get('ec_email'):
|
|
contact_vals['email'] = post.get('ec_email')
|
|
try:
|
|
request.env['sports.patient.contact'].sudo().create(contact_vals)
|
|
except Exception:
|
|
_logger.exception('Failed to create primary emergency contact during inline player create')
|
|
|
|
request.session['notification'] = {
|
|
'type': 'success',
|
|
'title': _('Player Created'),
|
|
'message': _('%s has been created and added to the team.') % patient.name,
|
|
'sticky': False,
|
|
}
|
|
# After one-step create, return to team page
|
|
return request.redirect(f"/my/team/{team.id}")
|
|
|
|
except (AccessError, UserError, ValidationError) as e:
|
|
# Re-render the add/link page with preserved form values and error
|
|
values = self._prepare_portal_layout_values()
|
|
team = self._check_team_access(team_id)
|
|
values.update({
|
|
'page_name': 'add_link_player',
|
|
'team': team,
|
|
'first_name': post.get('first_name') or '',
|
|
'last_name': post.get('last_name') or '',
|
|
'date_of_birth': post.get('date_of_birth') or '',
|
|
'searched': True,
|
|
'active_results': [],
|
|
'archived_results': [],
|
|
'is_treatment_prof': request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or request.env.user.has_group('base.group_system'),
|
|
'states': request.env['res.country.state'].search([('country_id.code', '=', 'CA')], order='name'),
|
|
'countries': request.env['res.country'].search([('code', '=', 'CA')], limit=1) or request.env['res.country'].search([], order='name'),
|
|
'all_teams': request.env['sports.team'].search([
|
|
('id', 'in', request.env['sports.team.staff'].search([
|
|
('partner_id', '=', request.env.user.partner_id.id)
|
|
]).mapped('team_id').ids)
|
|
], order='name'),
|
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
|
'form_data': dict(post),
|
|
'error': str(e),
|
|
'prefill_team_id': team.id,
|
|
})
|
|
return request.render('bemade_sports_clinic.portal_add_link_player_page', values)
|
|
except Exception:
|
|
_logger.exception('Error creating player from inline full form')
|
|
# Render with generic error and preserved inputs
|
|
values = self._prepare_portal_layout_values()
|
|
team = self._check_team_access(team_id)
|
|
values.update({
|
|
'page_name': 'add_link_player',
|
|
'team': team,
|
|
'first_name': post.get('first_name') or '',
|
|
'last_name': post.get('last_name') or '',
|
|
'date_of_birth': post.get('date_of_birth') or '',
|
|
'searched': True,
|
|
'active_results': [],
|
|
'archived_results': [],
|
|
'is_treatment_prof': request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or request.env.user.has_group('base.group_system'),
|
|
'states': request.env['res.country.state'].search([('country_id.code', '=', 'CA')], order='name'),
|
|
'countries': request.env['res.country'].search([('code', '=', 'CA')], limit=1) or request.env['res.country'].search([], order='name'),
|
|
'all_teams': request.env['sports.team'].search([
|
|
('id', 'in', request.env['sports.team.staff'].search([
|
|
('partner_id', '=', request.env.user.partner_id.id)
|
|
]).mapped('team_id').ids)
|
|
], order='name'),
|
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
|
'form_data': dict(post),
|
|
'error': _('An unexpected error occurred.'),
|
|
'prefill_team_id': team.id,
|
|
})
|
|
return request.render('bemade_sports_clinic.portal_add_link_player_page', values)
|
|
|
|
@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):
|
|
"""Handle the form submission to add a new player."""
|
|
try:
|
|
team = self._check_team_access(team_id)
|
|
|
|
# Basic validation
|
|
first_name = (post.get('first_name') or '').strip()
|
|
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"))
|
|
|
|
# Validate DOB format explicitly
|
|
try:
|
|
dob_date = fields.Date.to_date(dob)
|
|
except Exception:
|
|
values = {
|
|
'error': _('Please enter a valid Date of Birth (YYYY-MM-DD).'),
|
|
'team': team,
|
|
'page_name': 'add_player',
|
|
}
|
|
values.update(post)
|
|
return request.render("bemade_sports_clinic.portal_add_player", values)
|
|
|
|
# Enforce DOB not in future and not older than 120 years
|
|
today = fields.Date.context_today(request.env.user) or fields.Date.today()
|
|
min_dob = today - relativedelta(years=120)
|
|
if dob_date > today or dob_date < min_dob:
|
|
values = {
|
|
'error': _('Date of Birth must not be in the future and not be more than 120 years ago.'),
|
|
'team': team,
|
|
'page_name': 'add_player',
|
|
}
|
|
values.update(post)
|
|
return request.render("bemade_sports_clinic.portal_add_player", values)
|
|
|
|
# Determine if current user is allowed to set medical/status fields
|
|
is_tp_or_admin = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
|
request.env.user.has_group('base.group_system')
|
|
|
|
# Check for existing player
|
|
existing_patient = self._find_existing_patient(
|
|
first_name, last_name, email, phone
|
|
)
|
|
|
|
if existing_patient:
|
|
# Determine the action taken for logging and messaging
|
|
action_taken = []
|
|
|
|
# Reactivate if archived
|
|
if not existing_patient.active:
|
|
existing_patient.write({'active': True})
|
|
action_taken.append("reactivated")
|
|
_logger.debug(
|
|
"Reactivated archived player %s for team %s by user %s",
|
|
existing_patient.name, team.name, request.env.user.name
|
|
)
|
|
|
|
# Add to team if not already a member
|
|
if team not in existing_patient.team_ids:
|
|
existing_patient.write({
|
|
'team_ids': [(4, team.id)],
|
|
})
|
|
action_taken.append("added to team")
|
|
_logger.debug(
|
|
"Added existing player %s to team %s by user %s",
|
|
existing_patient.name, team.name, request.env.user.name
|
|
)
|
|
|
|
# Determine the appropriate success message
|
|
if "reactivated" in action_taken:
|
|
success_param = "player_reactivated"
|
|
else:
|
|
success_param = "player_added_to_team"
|
|
|
|
# Redirect to team page with appropriate message
|
|
return request.redirect(
|
|
f"/my/team?team_id={team.id}&success={success_param}"
|
|
)
|
|
|
|
# No existing player found, create a new one
|
|
patient_vals = {
|
|
'first_name': first_name,
|
|
'last_name': last_name,
|
|
'team_ids': [(4, team.id)],
|
|
'email': email or False,
|
|
'phone': phone or False,
|
|
'date_of_birth': dob,
|
|
}
|
|
# Pass through status fields if permitted
|
|
if is_tp_or_admin:
|
|
if post.get('match_status'):
|
|
patient_vals['match_status'] = post.get('match_status')
|
|
if post.get('practice_status'):
|
|
patient_vals['practice_status'] = post.get('practice_status')
|
|
|
|
# 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(
|
|
"Created new player %s and added to team %s by user %s",
|
|
patient.name, team.name, request.env.user.name
|
|
)
|
|
|
|
# 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 = {
|
|
'error': str(e),
|
|
'team': team,
|
|
'page_name': 'add_player',
|
|
}
|
|
values.update(post)
|
|
return request.render("bemade_sports_clinic.portal_add_player", values)
|
|
|
|
except (AccessError, MissingError) as e:
|
|
return request.redirect('/my')
|
|
|
|
except Exception as e:
|
|
_logger.exception("Error in portal_add_player_submit")
|
|
values = {
|
|
'error': _("An error occurred while adding the player. Please try again later."),
|
|
'team': team,
|
|
'page_name': 'add_player',
|
|
}
|
|
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
|
|
# Pass through status fields (route already restricts to TP/admin)
|
|
if post.get('match_status'):
|
|
vals['match_status'] = post.get('match_status')
|
|
if post.get('practice_status'):
|
|
vals['practice_status'] = post.get('practice_status')
|
|
|
|
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
|
|
# Pass through status fields
|
|
if post.get('match_status'):
|
|
vals['match_status'] = post.get('match_status')
|
|
if post.get('practice_status'):
|
|
vals['practice_status'] = post.get('practice_status')
|
|
|
|
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'))
|
|
# Require DOB and validate format for better triage by therapists
|
|
if not dob:
|
|
raise UserError(_('Date of birth is required'))
|
|
try:
|
|
# Validate date format (YYYY-MM-DD); will raise if invalid
|
|
fields.Date.to_date(dob)
|
|
except Exception:
|
|
raise UserError(_('Please enter a valid Date of Birth (YYYY-MM-DD).'))
|
|
|
|
# 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)
|
|
|
|
# In Odoo 18, mail.activity requires res_model_id (m2o), not res_model (char)
|
|
# Portal users typically cannot read ir.model; use sudo safely.
|
|
model_rec = request.env['ir.model'].sudo().search([('model', '=', 'sports.team')], limit=1)
|
|
if not model_rec:
|
|
raise UserError(_('Internal error: target model not found. Please contact an administrator.'))
|
|
model_id = model_rec.id
|
|
activity_vals = {
|
|
'res_model_id': model_id,
|
|
'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
|
|
|
|
# Ensure activity is created as a To Do
|
|
todo_type = request.env.ref('mail.mail_activity_data_todo', raise_if_not_found=False)
|
|
if todo_type:
|
|
activity_vals['activity_type_id'] = todo_type.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}")
|