bemade_sports_clinic: enforce DOB range validation and improve form errors
- Add DOB validation (not in future, not >120y) in controllers:\n - controllers/team_management_portal.py: portal_create_player_full, portal_add_player_submit\n - controllers/player_management_portal.py: create_player_submit, edit_player_submit\n- Preserve form inputs and display clear error messages on invalid DOB\n- Add dateutil.relativedelta import for date range checks\n- Minor template/context tweaks for consistent error rendering
This commit is contained in:
parent
6872e5fac2
commit
a47e829935
5 changed files with 1335 additions and 297 deletions
|
|
@ -4,6 +4,7 @@ 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 dateutil.relativedelta import relativedelta
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -13,6 +14,343 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
# Access control methods now inherited from AccessControlMixin
|
||||
|
||||
@http.route(['/my/player/create'], type='http', auth='user', website=True)
|
||||
def create_player_form(self, **get):
|
||||
"""Standalone create player with search-first workflow.
|
||||
Shows a search UI first; only renders the full create form when a search
|
||||
is attempted and yields no matching players.
|
||||
"""
|
||||
user = request.env.user
|
||||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
|
||||
# Gather query params for search-first flow
|
||||
first_name = (get.get('first_name') or '').strip()
|
||||
last_name = (get.get('last_name') or '').strip()
|
||||
dob = (get.get('date_of_birth') or '').strip()
|
||||
attempted = any(k in get for k in ('first_name', 'last_name', 'date_of_birth'))
|
||||
searched = bool(first_name or last_name or dob)
|
||||
|
||||
# Perform search similar to team add/link, but without team context
|
||||
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)
|
||||
# Only TPs/admins can see archived
|
||||
if is_treatment_prof 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),
|
||||
}
|
||||
|
||||
# Staff-only teams for multi-select
|
||||
all_teams = request.env['sports.team'].search([
|
||||
('id', 'in', request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', user.partner_id.id)
|
||||
]).mapped('team_id').ids)
|
||||
], order='name')
|
||||
|
||||
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||
|
||||
values = self._prepare_portal_layout_values()
|
||||
values.update({
|
||||
'page_name': 'create_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||
# search-first context
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'date_of_birth': dob,
|
||||
'attempted': attempted,
|
||||
'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_create_player', values)
|
||||
|
||||
@http.route(['/my/player/create/save'], type='http', auth='user', website=True, methods=['POST'])
|
||||
def create_player_submit(self, **post):
|
||||
"""Handle standalone player creation submit."""
|
||||
user = request.env.user
|
||||
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
|
||||
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:
|
||||
request.session['notification'] = {
|
||||
'type': 'danger',
|
||||
'message': _('First name and last name are required')
|
||||
}
|
||||
return request.redirect('/my/player/create')
|
||||
|
||||
# Team selection (optional). Restrict to staff teams to avoid tampering
|
||||
try:
|
||||
selected_team_ids = [int(tid) for tid in request.httprequest.form.getlist('team_ids')]
|
||||
except Exception:
|
||||
selected_team_ids = []
|
||||
allowed_team_ids = request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', user.partner_id.id)
|
||||
]).mapped('team_id').ids
|
||||
selected_team_ids = [tid for tid in selected_team_ids if tid in allowed_team_ids]
|
||||
|
||||
# Require at least one team for treatment professionals to ensure access control
|
||||
if is_treatment_prof and not selected_team_ids:
|
||||
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||
all_teams = request.env['sports.team'].search([
|
||||
('id', 'in', request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', user.partner_id.id)
|
||||
]).mapped('team_id').ids)
|
||||
], order='name')
|
||||
|
||||
values = self._prepare_portal_layout_values()
|
||||
values.update({
|
||||
'page_name': 'create_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||
# keep the original search context so the create form shows
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'date_of_birth': dob,
|
||||
'attempted': True,
|
||||
'searched': True,
|
||||
'active_results': [],
|
||||
'archived_results': [],
|
||||
# provide posted values to prefill the form
|
||||
'form_data': dict(post),
|
||||
'error': _('Please select at least one team to assign to this player.'),
|
||||
})
|
||||
return request.render('bemade_sports_clinic.portal_create_player', values)
|
||||
|
||||
# Validate DOB format and range if provided
|
||||
if dob:
|
||||
try:
|
||||
# Will raise if the date is not a valid string for a Date field
|
||||
dob_date = fields.Date.to_date(dob)
|
||||
except Exception:
|
||||
_logger.info('Invalid date_of_birth provided on create: %s', dob)
|
||||
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||
all_teams = request.env['sports.team'].search([
|
||||
('id', 'in', request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', user.partner_id.id)
|
||||
]).mapped('team_id').ids)
|
||||
], order='name')
|
||||
|
||||
values = self._prepare_portal_layout_values()
|
||||
values.update({
|
||||
'page_name': 'create_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||
# keep the original search context so the create form shows
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'date_of_birth': dob,
|
||||
'attempted': True,
|
||||
'searched': True,
|
||||
'active_results': [],
|
||||
'archived_results': [],
|
||||
# provide posted values to prefill the form
|
||||
'form_data': dict(post),
|
||||
'error': _('Please enter a valid Date of Birth (YYYY-MM-DD).'),
|
||||
})
|
||||
return request.render('bemade_sports_clinic.portal_create_player', values)
|
||||
|
||||
# Range checks: not in the future, 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:
|
||||
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||
all_teams = request.env['sports.team'].search([
|
||||
('id', 'in', request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', user.partner_id.id)
|
||||
]).mapped('team_id').ids)
|
||||
], order='name')
|
||||
|
||||
values = self._prepare_portal_layout_values()
|
||||
values.update({
|
||||
'page_name': 'create_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||
# keep the original search context so the create form shows
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'date_of_birth': dob,
|
||||
'attempted': True,
|
||||
'searched': True,
|
||||
'active_results': [],
|
||||
'archived_results': [],
|
||||
# provide posted values to prefill the form
|
||||
'form_data': dict(post),
|
||||
'error': _('Date of Birth must not be in the future and not be more than 120 years ago.'),
|
||||
})
|
||||
return request.render('bemade_sports_clinic.portal_create_player', values)
|
||||
|
||||
vals = {
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
}
|
||||
if dob:
|
||||
vals['date_of_birth'] = dob
|
||||
# Optional contact and address fields
|
||||
for key in ['email', 'phone', 'street', 'street2', 'city', 'zip']:
|
||||
if key in post:
|
||||
vals[key] = post.get(key) or False
|
||||
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
|
||||
|
||||
# Additional fields for treatment professionals
|
||||
if is_treatment_prof:
|
||||
if selected_team_ids:
|
||||
vals['team_ids'] = [(6, 0, list(set(selected_team_ids)))]
|
||||
if 'allergies' in post:
|
||||
_all = (post.get('allergies') or '').strip()
|
||||
vals['allergies'] = _all if _all else False
|
||||
if 'team_info_notes' in post:
|
||||
_notes = (post.get('team_info_notes') or '').strip()
|
||||
vals['team_info_notes'] = _notes if _notes else 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')
|
||||
|
||||
# Use public create method with permission checks
|
||||
try:
|
||||
patient = request.env['sports.patient'].create_portal_patient(vals)
|
||||
except ValidationError as ve:
|
||||
# Re-render the create form (search-first view) with preserved input and a friendly message
|
||||
_logger.info('ValidationError during player create: %s', ve)
|
||||
# Rebuild the context needed by the search-first page
|
||||
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||
all_teams = request.env['sports.team'].search([
|
||||
('id', 'in', request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', user.partner_id.id)
|
||||
]).mapped('team_id').ids)
|
||||
], order='name')
|
||||
|
||||
values = self._prepare_portal_layout_values()
|
||||
values.update({
|
||||
'page_name': 'create_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||
# keep the original search context so the create form shows
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'date_of_birth': dob,
|
||||
'attempted': True,
|
||||
'searched': True,
|
||||
'active_results': [],
|
||||
'archived_results': [],
|
||||
# provide posted values to prefill the form
|
||||
'form_data': dict(post),
|
||||
'error': str(ve) or _('Invalid combination of match and practice status.'),
|
||||
})
|
||||
return request.render('bemade_sports_clinic.portal_create_player', values)
|
||||
except Exception as e:
|
||||
# Render the create form with a generic error and preserve inputs
|
||||
_logger.exception('Error creating patient from portal create')
|
||||
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||
all_teams = request.env['sports.team'].search([
|
||||
('id', 'in', request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', user.partner_id.id)
|
||||
]).mapped('team_id').ids)
|
||||
], order='name')
|
||||
|
||||
values = self._prepare_portal_layout_values()
|
||||
values.update({
|
||||
'page_name': 'create_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||
# keep the original search context so the create form shows
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'date_of_birth': dob,
|
||||
'attempted': True,
|
||||
'searched': True,
|
||||
'active_results': [],
|
||||
'archived_results': [],
|
||||
# provide posted values to prefill the form
|
||||
'form_data': dict(post),
|
||||
'error': str(e) or _('There was an error creating the player. Please review your inputs and try again.'),
|
||||
})
|
||||
return request.render('bemade_sports_clinic.portal_create_player', values)
|
||||
|
||||
# Optionally create a primary emergency contact if provided (TPs only)
|
||||
if is_treatment_prof:
|
||||
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 player create')
|
||||
|
||||
return request.redirect(f"/my/player?player_id={patient.id}")
|
||||
|
||||
@http.route(['/my/player/edit'], type='http', auth='user', website=True)
|
||||
def edit_player_form(self, patient_id, **post):
|
||||
"""Show form to edit player information"""
|
||||
|
|
@ -28,6 +366,12 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
|
||||
teams = patient.team_ids
|
||||
# All teams for multi-select limited to current user's staff teams
|
||||
all_teams = request.env['sports.team'].search([
|
||||
('id', 'in', request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', user.partner_id.id)
|
||||
]).mapped('team_id').ids)
|
||||
], order='name')
|
||||
|
||||
# Create a dictionary with patient info for protected fields
|
||||
patient_info = {}
|
||||
|
|
@ -61,13 +405,24 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||
# Determine primary emergency contact (lowest sequence)
|
||||
primary_contact = request.env['sports.patient.contact'].search([
|
||||
('patient_id', '=', patient.id)
|
||||
], order='sequence,id', limit=1)
|
||||
|
||||
values = {
|
||||
'patient': patient, # Keep original patient
|
||||
'patient_info': patient_info, # Add patient_info for protected fields
|
||||
'teams': teams,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||
# Primary emergency contact defaults for prefill
|
||||
'ec_name': primary_contact.name if primary_contact else '',
|
||||
'ec_contact_type': primary_contact.contact_type if primary_contact else '',
|
||||
'ec_mobile': primary_contact.mobile if primary_contact else '',
|
||||
'ec_email': primary_contact.email if primary_contact else '',
|
||||
'return_url': return_url,
|
||||
'page_name': 'edit_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
|
|
@ -91,6 +446,69 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
# Check if user is a treatment professional
|
||||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
|
||||
# If DOB is being changed/provided, validate format upfront to avoid unhelpful errors
|
||||
dob_str = (post.get('date_of_birth') or '').strip()
|
||||
if dob_str:
|
||||
try:
|
||||
dob_date = fields.Date.to_date(dob_str)
|
||||
except Exception:
|
||||
# Rebuild dropdowns and re-render with preserved inputs
|
||||
user = request.env.user
|
||||
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||
all_teams = request.env['sports.team'].search([
|
||||
('id', 'in', request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', user.partner_id.id)
|
||||
]).mapped('team_id').ids)
|
||||
], order='name')
|
||||
|
||||
values = {
|
||||
'patient': patient,
|
||||
'patient_info': {},
|
||||
'teams': patient.team_ids,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||
'return_url': post.get('return_url', f'/my/player?player_id={patient_id}'),
|
||||
'page_name': 'edit_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'error': _('Please enter a valid Date of Birth (YYYY-MM-DD).'),
|
||||
'form_data': dict(post),
|
||||
}
|
||||
return request.render('bemade_sports_clinic.portal_edit_player', values)
|
||||
|
||||
# Range checks: not in the future, 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:
|
||||
user = request.env.user
|
||||
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||
all_teams = request.env['sports.team'].search([
|
||||
('id', 'in', request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', user.partner_id.id)
|
||||
]).mapped('team_id').ids)
|
||||
], order='name')
|
||||
|
||||
values = {
|
||||
'patient': patient,
|
||||
'patient_info': {},
|
||||
'teams': patient.team_ids,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||
'return_url': post.get('return_url', f'/my/player?player_id={patient_id}'),
|
||||
'page_name': 'edit_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'error': _('Date of Birth must not be in the future and not be more than 120 years ago.'),
|
||||
'form_data': dict(post),
|
||||
}
|
||||
return request.render('bemade_sports_clinic.portal_edit_player', values)
|
||||
|
||||
# Prepare values for patient update
|
||||
vals = {}
|
||||
|
||||
|
|
@ -135,6 +553,18 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
# Additional fields that only treatment professionals can update
|
||||
if is_treatment_prof:
|
||||
# Team assignments via multi-select
|
||||
try:
|
||||
team_id_list = [int(tid) for tid in request.httprequest.form.getlist('team_ids')]
|
||||
except Exception:
|
||||
team_id_list = []
|
||||
if team_id_list:
|
||||
# Defense-in-depth: restrict to teams where current user is staff
|
||||
allowed_team_ids = request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', request.env.user.partner_id.id)
|
||||
]).mapped('team_id').ids
|
||||
filtered_team_ids = [tid for tid in team_id_list if tid in allowed_team_ids]
|
||||
vals['team_ids'] = [(6, 0, list(set(filtered_team_ids)))]
|
||||
if post.get('date_of_birth'):
|
||||
vals.update({
|
||||
'date_of_birth': post.get('date_of_birth'),
|
||||
|
|
@ -142,13 +572,15 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
# Medical information
|
||||
if 'allergies' in post:
|
||||
_all = (post.get('allergies') or '').strip()
|
||||
vals.update({
|
||||
'allergies': post.get('allergies') or False,
|
||||
'allergies': _all if _all else False,
|
||||
})
|
||||
|
||||
if 'team_info_notes' in post:
|
||||
_notes = (post.get('team_info_notes') or '').strip()
|
||||
vals.update({
|
||||
'team_info_notes': post.get('team_info_notes') or False,
|
||||
'team_info_notes': _notes if _notes else False,
|
||||
})
|
||||
|
||||
# Status fields
|
||||
|
|
@ -164,8 +596,114 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
# Update the patient - no sudo needed as field-level security is in place
|
||||
if vals:
|
||||
patient.write(vals)
|
||||
|
||||
try:
|
||||
patient.write(vals)
|
||||
except ValidationError as ve:
|
||||
# Re-render edit form with preserved input and error
|
||||
user = request.env.user
|
||||
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||
# All teams for multi-select limited to current user's staff teams
|
||||
all_teams = request.env['sports.team'].search([
|
||||
('id', 'in', request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', user.partner_id.id)
|
||||
]).mapped('team_id').ids)
|
||||
], order='name')
|
||||
|
||||
values = {
|
||||
'patient': patient,
|
||||
'patient_info': {},
|
||||
'teams': patient.team_ids,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||
'return_url': post.get('return_url', f'/my/player?player_id={patient_id}'),
|
||||
'page_name': 'edit_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'error': str(ve) or _('Invalid combination of match and practice status.'),
|
||||
'form_data': dict(post),
|
||||
}
|
||||
return request.render('bemade_sports_clinic.portal_edit_player', values)
|
||||
except Exception as e:
|
||||
# Generic exception: re-render with generic error and preserved inputs
|
||||
user = request.env.user
|
||||
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||
all_teams = request.env['sports.team'].search([
|
||||
('id', 'in', request.env['sports.team.staff'].search([
|
||||
('partner_id', '=', user.partner_id.id)
|
||||
]).mapped('team_id').ids)
|
||||
], order='name')
|
||||
|
||||
values = {
|
||||
'patient': patient,
|
||||
'patient_info': {},
|
||||
'teams': patient.team_ids,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||
'return_url': post.get('return_url', f'/my/player?player_id={patient_id}'),
|
||||
'page_name': 'edit_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'error': str(e) or _('An unexpected error occurred.'),
|
||||
'form_data': dict(post),
|
||||
}
|
||||
return request.render('bemade_sports_clinic.portal_edit_player', values)
|
||||
|
||||
# Update or create primary emergency contact (TPs only), regardless of patient field changes
|
||||
if is_treatment_prof:
|
||||
ec_name = (post.get('ec_name') or '').strip()
|
||||
ec_type = (post.get('ec_contact_type') or '').strip()
|
||||
ec_mobile = (post.get('ec_mobile') or '').strip()
|
||||
ec_email = (post.get('ec_email') or '').strip()
|
||||
|
||||
# If any EC field provided, attempt update/create
|
||||
if any([ec_name, ec_type, ec_mobile, ec_email]):
|
||||
primary_contact = request.env['sports.patient.contact'].search([
|
||||
('patient_id', '=', patient.id)
|
||||
], order='sequence,id', limit=1)
|
||||
|
||||
if primary_contact:
|
||||
update_vals = {}
|
||||
# Only update name/type if provided (avoid clearing required selection)
|
||||
if ec_name:
|
||||
update_vals['name'] = ec_name
|
||||
if ec_type:
|
||||
update_vals['contact_type'] = ec_type
|
||||
# For mobile/email, explicitly set to value or False to clear
|
||||
if 'ec_mobile' in post:
|
||||
update_vals['mobile'] = ec_mobile or False
|
||||
if 'ec_email' in post:
|
||||
update_vals['email'] = ec_email or False
|
||||
if update_vals:
|
||||
try:
|
||||
primary_contact.sudo().write(update_vals)
|
||||
except Exception:
|
||||
_logger.exception('Failed to update primary emergency contact for patient %s', patient.id)
|
||||
else:
|
||||
# Create only if we have minimally required fields
|
||||
if ec_name and ec_type:
|
||||
create_vals = {
|
||||
'patient_id': patient.id,
|
||||
'name': ec_name,
|
||||
'contact_type': ec_type,
|
||||
'sequence': 0,
|
||||
}
|
||||
if 'ec_mobile' in post:
|
||||
create_vals['mobile'] = ec_mobile or False
|
||||
if 'ec_email' in post:
|
||||
create_vals['email'] = ec_email or False
|
||||
try:
|
||||
request.env['sports.patient.contact'].sudo().create(create_vals)
|
||||
except Exception:
|
||||
_logger.exception('Failed to create primary emergency contact for patient %s', patient.id)
|
||||
|
||||
return request.redirect(f'/my/player?player_id={patient_id}')
|
||||
|
||||
@http.route(['/my/player/contact/add'], type='http', auth='user', website=True)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
from odoo import http, _
|
||||
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__)
|
||||
|
||||
|
|
@ -310,6 +312,17 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
'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')
|
||||
|
|
@ -326,6 +339,233 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
}
|
||||
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):
|
||||
|
|
@ -345,7 +585,35 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
# 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
|
||||
|
|
@ -395,6 +663,12 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
'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)
|
||||
|
|
@ -556,6 +830,11 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
}
|
||||
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)
|
||||
|
||||
|
|
@ -622,6 +901,11 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -839,6 +839,15 @@ class Patient(models.Model):
|
|||
'phone': vals.get('phone', False),
|
||||
'type': 'contact',
|
||||
}
|
||||
# Optional address fields coming from portal form
|
||||
# These are res.partner fields, so capture them here
|
||||
for key in ['street', 'street2', 'city', 'zip']:
|
||||
if key in vals:
|
||||
partner_vals[key] = vals.get(key) or False
|
||||
if vals.get('state_id'):
|
||||
partner_vals['state_id'] = vals.get('state_id')
|
||||
if vals.get('country_id'):
|
||||
partner_vals['country_id'] = vals.get('country_id')
|
||||
partner = (
|
||||
self.env['res.partner']
|
||||
.sudo()
|
||||
|
|
@ -858,7 +867,17 @@ class Patient(models.Model):
|
|||
patient_vals['team_ids'] = vals['team_ids']
|
||||
if 'date_of_birth' in vals and vals['date_of_birth']:
|
||||
patient_vals['date_of_birth'] = vals['date_of_birth']
|
||||
|
||||
# Status fields from portal (treatment professionals only)
|
||||
if vals.get('match_status'):
|
||||
patient_vals['match_status'] = vals.get('match_status')
|
||||
if vals.get('practice_status'):
|
||||
patient_vals['practice_status'] = vals.get('practice_status')
|
||||
# Other optional patient fields
|
||||
if 'allergies' in vals:
|
||||
patient_vals['allergies'] = vals.get('allergies') or False
|
||||
if 'team_info_notes' in vals:
|
||||
patient_vals['team_info_notes'] = vals.get('team_info_notes') or False
|
||||
|
||||
# Create patient with tracking disabled to avoid triggering mail/report side-effects
|
||||
# Also disable auto-subscriptions on creation
|
||||
patient = self.sudo().with_context(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,266 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Reusable partial: Player Create/Edit Form Body (used by standalone and team create views) -->
|
||||
<template id="portal_player_form_body" name="Player Form Body">
|
||||
<!-- Player Details Card -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Player Details</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>First Name <span class="text-danger">*</span></label>
|
||||
<input type="text" name="first_name" class="form-control" required="required"
|
||||
t-att-value="(form_data and form_data.get('first_name')) or (patient and patient.first_name) or first_name or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Last Name <span class="text-danger">*</span></label>
|
||||
<input type="text" name="last_name" class="form-control" required="required"
|
||||
t-att-value="(form_data and form_data.get('last_name')) or (patient and patient.last_name) or last_name or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" name="email" class="form-control"
|
||||
t-att-value="(form_data and form_data.get('email')) or (patient and patient.email) or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Phone</label>
|
||||
<input type="tel" name="phone" class="form-control"
|
||||
t-att-value="(form_data and form_data.get('phone')) or (patient and patient.phone) or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Date of Birth <span class="text-danger">*</span></label>
|
||||
<input type="date" name="date_of_birth" class="form-control" required="required"
|
||||
t-att-value="(form_data and form_data.get('date_of_birth')) or (patient_info and patient_info.get('date_of_birth')) or (patient and patient.date_of_birth) or date_of_birth or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TP-only additional info placed within Player Details -->
|
||||
<t t-if="is_treatment_prof">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<label for="allergies">Allergies</label>
|
||||
<textarea name="allergies" id="allergies" class="form-control" rows="2" placeholder="Enter allergies information here"><t t-esc="(form_data and form_data.get('allergies')) or (patient_info and patient_info.get('allergies')) or ''"/></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<label for="team_info_notes">Internal Notes</label>
|
||||
<textarea name="team_info_notes" id="team_info_notes" class="form-control" rows="3" placeholder="Enter internal notes here"><t t-esc="(form_data and form_data.get('team_info_notes')) or (patient_info and patient_info.get('team_info_notes')) or ''"/></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Card (TP only) -->
|
||||
<t t-if="is_treatment_prof">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Status</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Match Status</label>
|
||||
<select name="match_status" class="form-control" id="create_match_status">
|
||||
<option value="yes" t-att-selected="'selected' if ((form_data and form_data.get('match_status') == 'yes') or (not form_data and patient_info and patient_info.get('match_status') == 'yes')) else None">Yes</option>
|
||||
<option value="no" t-att-selected="'selected' if ((form_data and form_data.get('match_status') == 'no') or (not form_data and patient_info and patient_info.get('match_status') == 'no')) else None">No</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Practice Status</label>
|
||||
<select name="practice_status" class="form-control" id="create_practice_status">
|
||||
<option value="yes" t-att-selected="'selected' if ((form_data and form_data.get('practice_status') == 'yes') or (not form_data and patient_info and patient_info.get('practice_status') == 'yes')) else None">Yes</option>
|
||||
<option value="no_contact" t-att-selected="'selected' if ((form_data and form_data.get('practice_status') == 'no_contact') or (not form_data and patient_info and patient_info.get('practice_status') == 'no_contact')) else None">Yes, no contact</option>
|
||||
<option value="no" t-att-selected="'selected' if ((form_data and form_data.get('practice_status') == 'no') or (not form_data and patient_info and patient_info.get('practice_status') == 'no')) else None">No</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Address Card -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Address Information</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label>Street Address</label>
|
||||
<input type="text" name="street" class="form-control"
|
||||
t-att-value="(form_data and form_data.get('street')) or (patient and patient.street) or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label>Street Address 2</label>
|
||||
<input type="text" name="street2" class="form-control"
|
||||
t-att-value="(form_data and form_data.get('street2')) or (patient and patient.street2) or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>City</label>
|
||||
<input type="text" name="city" class="form-control"
|
||||
t-att-value="(form_data and form_data.get('city')) or (patient and patient.city) or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label>ZIP/Postal Code</label>
|
||||
<input type="text" name="zip" class="form-control"
|
||||
t-att-value="(form_data and form_data.get('zip')) or (patient and patient.zip) or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label>Province/Territory</label>
|
||||
<select name="state_id" class="form-control">
|
||||
<option value="">Select Province/Territory...</option>
|
||||
<t t-foreach="states" t-as="state">
|
||||
<option t-att-value="state.id"
|
||||
t-att-selected="'selected' if (form_data and form_data.get('state_id') and int(form_data.get('state_id')) == state.id) or (not form_data and patient and patient.state_id and patient.state_id.id == state.id) else None"
|
||||
t-esc="state.name"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Country</label>
|
||||
<select name="country_id" class="form-control">
|
||||
<t t-if="countries">
|
||||
<option t-att-value="countries.id"
|
||||
t-att-selected="'selected' if (form_data and form_data.get('country_id') and int(form_data.get('country_id')) == countries.id) or (not form_data and patient and patient.country_id and patient.country_id.id == countries.id) else None"
|
||||
t-esc="countries.name"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<option value="">No country available</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Emergency Contact (TP only) -->
|
||||
<t t-if="is_treatment_prof">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Primary Emergency Contact</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Contact Name</label>
|
||||
<input type="text" name="ec_name" class="form-control" placeholder="e.g., Jane Doe"
|
||||
t-att-value="(form_data and form_data.get('ec_name')) or ec_name or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Relationship</label>
|
||||
<select name="ec_contact_type" class="form-select">
|
||||
<option value="">Select...</option>
|
||||
<t t-foreach="relationship_types" t-as="rel">
|
||||
<option t-att-value="rel[0]"
|
||||
t-att-selected="'selected' if ((form_data and form_data.get('ec_contact_type') == rel[0]) or (not form_data and ec_contact_type and ec_contact_type == rel[0])) else None"
|
||||
t-esc="rel[1]"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Mobile</label>
|
||||
<input type="tel" name="ec_mobile" class="form-control" placeholder="e.g., +1 555-555-1234"
|
||||
t-att-value="(form_data and form_data.get('ec_mobile')) or ec_mobile or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" name="ec_email" class="form-control" placeholder="name@example.com"
|
||||
t-att-value="(form_data and form_data.get('ec_email')) or ec_email or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Assign Teams (TP only) -->
|
||||
<t t-if="is_treatment_prof">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Assign Teams</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Teams</label>
|
||||
<div class="row row-cols-1 row-cols-md-2 g-2">
|
||||
<t t-foreach="all_teams" t-as="t">
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
t-att-id="'team_ids_' + str(t.id)"
|
||||
name="team_ids" t-att-value="t.id"
|
||||
t-att-checked="'checked' if ((form_data and form_data.get('team_ids') and (str(t.id) in (form_data.get('team_ids') if isinstance(form_data.get('team_ids'), list) else [form_data.get('team_ids')]))) or (not form_data and patient and (t.id in patient.team_ids.ids)) or (prefill_team_id and t.id == prefill_team_id)) else None"/>
|
||||
<label class="form-check-label" t-att-for="'team_ids_' + str(t.id)">
|
||||
<t t-esc="t.name"/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<!-- Template for editing player information -->
|
||||
<template id="portal_edit_player" name="Edit Player">
|
||||
<t t-call="portal.portal_layout">
|
||||
|
|
@ -8,9 +269,12 @@
|
|||
<div class="col-12">
|
||||
<h2>Edit Player Information</h2>
|
||||
|
||||
<t t-if="error" t-out="error" class="alert alert-danger"/>
|
||||
<t t-if="error">
|
||||
<div class="alert alert-danger" t-esc="error"/>
|
||||
</t>
|
||||
<div id="edit-player-client-error" class="alert alert-danger d-none"></div>
|
||||
|
||||
<form action="/my/player/save" method="post">
|
||||
<form action="/my/player/save" method="post" id="edit-player-form">
|
||||
<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"/>
|
||||
|
|
@ -28,265 +292,54 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Team membership actions: Remove/Request Removal -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h5>Team Membership</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Team</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="teams" t-as="team">
|
||||
<tr>
|
||||
<td>
|
||||
<a t-attf-href="/my/team?team_id={{ team.id }}">
|
||||
<t t-esc="team.name"/>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<t t-set="is_treatment_prof" t-value="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or request.env.user.has_group('base.group_system')"/>
|
||||
<t t-set="is_team_staff" t-value="request.env.user.partner_id in team.staff_ids.user_ids.partner_id"/>
|
||||
<!-- Direct remove for treatment professionals -->
|
||||
<t t-if="is_treatment_prof">
|
||||
<button type="submit"
|
||||
class="btn btn-danger btn-sm"
|
||||
t-att-formaction="'/my/team/' + str(team.id) + '/player/' + str(patient.id) + '/remove'"
|
||||
formmethod="post"
|
||||
formnovalidate="formnovalidate"
|
||||
onclick="return confirm('Are you sure you want to remove this player from the team?');">
|
||||
<i class="fa fa-user-times"/> Remove
|
||||
</button>
|
||||
</t>
|
||||
<!-- Request removal for team staff (e.g., coaches) -->
|
||||
<t t-elif="is_team_staff">
|
||||
<button type="button"
|
||||
class="btn bg-o-color-2 text-white btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
t-att-data-bs-target="'#requestRemovalModalEdit' + str(team.id)">
|
||||
<i class="fa fa-flag"/> Request Removal
|
||||
</button>
|
||||
<!-- Request Removal Modal -->
|
||||
<div t-att-id="'requestRemovalModalEdit' + str(team.id)" class="modal fade" tabindex="-1" t-attf-aria-labelledby="requestRemovalModalEditLabel#{team.id}" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" t-attf-id="requestRemovalModalEditLabel#{team.id}">Request Player Removal</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/{{ patient.id }}/request_removal" method="post">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="modal-body">
|
||||
<p>Please provide a reason for removing this player from the team. This will create a task for the head therapist to review.</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Reason</label>
|
||||
<textarea name="reason" class="form-control" rows="3" required="required" placeholder="Please explain why this player should be removed from the team"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn bg-o-color-2 text-white" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn bg-o-color-1 text-white">Submit Request</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="first_name">First Name <span class="text-danger">*</span></label>
|
||||
<input type="text" name="first_name" id="first_name" class="form-control"
|
||||
required="required" t-att-value="patient.first_name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="last_name">Last Name <span class="text-danger">*</span></label>
|
||||
<input type="text" name="last_name" id="last_name" class="form-control"
|
||||
required="required" t-att-value="patient.last_name"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" name="email" id="email" class="form-control"
|
||||
t-att-value="patient.email"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="phone">Phone</label>
|
||||
<input type="tel" name="phone" id="phone" class="form-control"
|
||||
t-att-value="patient.phone"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Information -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<h5>Address Information</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label for="street">Street Address</label>
|
||||
<input type="text" name="street" id="street" class="form-control"
|
||||
t-att-value="patient.street"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label for="street2">Street Address 2</label>
|
||||
<input type="text" name="street2" id="street2" class="form-control"
|
||||
t-att-value="patient.street2"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="city">City</label>
|
||||
<input type="text" name="city" id="city" class="form-control"
|
||||
t-att-value="patient.city"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="zip">ZIP/Postal Code</label>
|
||||
<input type="text" name="zip" id="zip" class="form-control"
|
||||
t-att-value="patient.zip"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="state_id">Province/Territory</label>
|
||||
<select name="state_id" id="state_id" class="form-control">
|
||||
<option value="">Select Province/Territory...</option>
|
||||
<t t-foreach="states" t-as="state">
|
||||
<option t-att-value="state.id"
|
||||
t-att-selected="'selected' if patient.state_id and patient.state_id.id == state.id else None"
|
||||
t-esc="state.name"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="country_id">Country</label>
|
||||
<select name="country_id" id="country_id" class="form-control">
|
||||
<t t-if="countries">
|
||||
<option t-att-value="countries.id" selected="selected" t-esc="countries.name"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<option value="">No country available</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional fields accessible only to treatment professionals -->
|
||||
<!-- Unified form content via shared partial -->
|
||||
<t t-call="bemade_sports_clinic.portal_player_form_body"/>
|
||||
|
||||
<!-- Client-side validation for status fields -->
|
||||
<t t-if="is_treatment_prof">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="date_of_birth">Date of Birth</label>
|
||||
<input type="date" name="date_of_birth" id="date_of_birth" class="form-control"
|
||||
t-att-value="patient_info.get('date_of_birth')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="age">Age</label>
|
||||
<input type="text" name="age" id="age" class="form-control" readonly="readonly"
|
||||
t-att-value="patient_info.get('age')"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<label for="allergies">Allergies</label>
|
||||
<textarea name="allergies" id="allergies" class="form-control"
|
||||
rows="2" placeholder="Enter allergies information here"
|
||||
t-esc="patient_info.get('allergies') or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<label for="team_info_notes">Team Notes</label>
|
||||
<textarea name="team_info_notes" id="team_info_notes" class="form-control"
|
||||
rows="4" placeholder="Enter team notes here"
|
||||
t-esc="patient_info.get('team_info_notes') or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status fields -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="match_status">Match Status</label>
|
||||
<select name="match_status" id="match_status" class="form-control">
|
||||
<option value="yes" t-att-selected="patient_info.get('match_status') == 'yes'">Yes</option>
|
||||
<option value="no" t-att-selected="patient_info.get('match_status') == 'no'">No</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="practice_status">Practice Status</label>
|
||||
<select name="practice_status" id="practice_status" class="form-control">
|
||||
<option value="yes" t-att-selected="patient_info.get('practice_status') == 'yes'">Yes</option>
|
||||
<option value="no_contact" t-att-selected="patient_info.get('practice_status') == 'no_contact'">Yes, no contact</option>
|
||||
<option value="no" t-att-selected="patient_info.get('practice_status') == 'no'">No</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Injury information -->
|
||||
<div class="row mb-3" t-if="patient_info.get('injured_since')">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<label>Injured Since</label>
|
||||
<p class="form-control-static" t-esc="patient_info.get('injured_since')"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
(function() {
|
||||
function showError(msg){
|
||||
var el = document.getElementById('edit-player-client-error');
|
||||
if(!el) return;
|
||||
el.textContent = msg;
|
||||
el.classList.remove('d-none');
|
||||
}
|
||||
function clearError(){
|
||||
var el = document.getElementById('edit-player-client-error');
|
||||
if(!el) return;
|
||||
el.textContent = '';
|
||||
el.classList.add('d-none');
|
||||
}
|
||||
function isValidCombo(matchVal, practiceVal){
|
||||
var valid = {
|
||||
'yes': {'yes': true},
|
||||
'no': {'yes': true, 'no_contact': true, 'no': true}
|
||||
};
|
||||
return !!(valid[matchVal] && valid[matchVal][practiceVal]);
|
||||
}
|
||||
var form = document.getElementById('edit-player-form');
|
||||
if(form){
|
||||
form.addEventListener('submit', function(e){
|
||||
var mEl = document.getElementById('match_status') || document.getElementById('create_match_status');
|
||||
var pEl = document.getElementById('practice_status') || document.getElementById('create_practice_status');
|
||||
var m = (mEl||{}).value;
|
||||
var p = (pEl||{}).value;
|
||||
clearError();
|
||||
if(m && p && !isValidCombo(m, p)){
|
||||
e.preventDefault();
|
||||
showError('Invalid combination of match and practice status.');
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
</t>
|
||||
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-6">
|
||||
<a t-att-href="return_url" class="btn bg-o-color-2 text-white">Cancel</a>
|
||||
|
|
@ -482,6 +535,13 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Error alert (e.g., invalid DOB on create_full) -->
|
||||
<div t-if="error" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fa fa-exclamation-triangle me-2"/>
|
||||
<span t-esc="error"/>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</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">
|
||||
|
|
@ -574,30 +634,16 @@
|
|||
<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">
|
||||
<form t-attf-action="/my/team/{{ team.id }}/player/create_full" method="post">
|
||||
<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>
|
||||
|
||||
<!-- Reusable form body -->
|
||||
<t t-set="prefill_team_id" t-value="team.id"/>
|
||||
<t t-call="bemade_sports_clinic.portal_player_form_body"/>
|
||||
<div class="row mt-3">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -607,4 +653,123 @@
|
|||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="portal_create_player" name="Portal Create Player">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="title" t-value="'Create Player'"/>
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10 col-lg-9">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="mb-0" t-esc="title"/>
|
||||
<a href="/my/players" class="btn bg-o-color-2 text-white"><i class="fa fa-arrow-left me-1"/> Back to Players</a>
|
||||
</div>
|
||||
|
||||
<t t-if="error">
|
||||
<div class="alert alert-danger" t-esc="error"/>
|
||||
</t>
|
||||
<div id="create-player-client-error" class="alert alert-danger d-none"></div>
|
||||
|
||||
<!-- Search Form -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<form action="/my/player/create" 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>
|
||||
|
||||
<!-- Search Results -->
|
||||
<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">
|
||||
<t t-if="has_active">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-success text-white">Active Matches</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>
|
||||
<div class="fw-bold" t-esc="p['name']"/>
|
||||
<small class="text-muted">DOB: <t t-esc="p['date_of_birth'] or '-'"/></small>
|
||||
</div>
|
||||
<a t-attf-href="/my/player?player_id={{ p['id'] }}" class="btn btn-outline-primary btn-sm">View</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="has_archived">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-secondary text-white">Archived Matches</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>
|
||||
<div class="fw-bold" t-esc="p['name']"/>
|
||||
<small class="text-muted">DOB: <t t-esc="p['date_of_birth'] or '-'"/></small>
|
||||
</div>
|
||||
<a t-attf-href="/my/player?player_id={{ p['id'] }}" class="btn btn-outline-primary btn-sm">View</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Only show create when no matches -->
|
||||
<t t-if="not has_active and not has_archived">
|
||||
<div class="alert alert-info">No matching players found. You can create a new one below.</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!-- Create Form: shown only when searched and there are no matches -->
|
||||
<t t-if="searched and not (len(active_results) or len(archived_results))">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">New Player Details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form t-attf-action="/my/player/create/save" method="post" id="create-player-form">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
|
||||
<!-- Reusable form body -->
|
||||
<t t-set="prefill_team_id" t-value="None"/>
|
||||
<t t-call="bemade_sports_clinic.portal_player_form_body"/>
|
||||
<div class="row mt-3">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -427,6 +427,13 @@
|
|||
<t t-call="portal.portal_layout">
|
||||
<h1>Your Players</h1>
|
||||
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<a t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')"
|
||||
href="/my/player/create" class="btn bg-o-color-1 text-white">
|
||||
<i class="fa fa-plus"/> Create Player
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="container-fluid mb-3">
|
||||
<div class="row">
|
||||
|
|
@ -635,7 +642,7 @@
|
|||
<div class="col-md-3" t-if="player.stage">
|
||||
<h6 class="mb-1">Status</h6>
|
||||
<span t-if="player.stage == 'healthy'" class="text-success font-weight-bold h5">
|
||||
<i class="fa fa-heart"></i> Play OK
|
||||
<i class="fa fa-thumbs-up"></i> Play OK
|
||||
</span>
|
||||
<span t-elif="player.stage == 'practice_ok'" class="text-warning font-weight-bold h5">
|
||||
<i class="fa fa-running"></i> Practice OK
|
||||
|
|
@ -647,7 +654,7 @@
|
|||
<i class="fa fa-question-circle"></i> <span t-esc="player.stage"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-3" t-if="player.allergies">
|
||||
<div class="col-md-3" t-if="(player.allergies or '').replace('\xa0','').strip()">
|
||||
<h6 class="mb-1">Allergies</h6>
|
||||
<div class="alert alert-warning py-2 mb-0">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
|
|
@ -819,8 +826,8 @@
|
|||
<td><strong>Last Consultation:</strong></td>
|
||||
<td t-esc="player.last_consultation_date"/>
|
||||
</tr>
|
||||
<tr t-if="is_treatment_prof and player.allergies">
|
||||
<td><strong>Allergies:</strong></td>
|
||||
<tr t-if="is_treatment_prof and (player.allergies or '').replace('\xa0','').strip()">
|
||||
<td><strong>Allergies:</strong></td>
|
||||
<td>
|
||||
<div class="alert alert-warning mb-0 p-2">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
|
|
@ -937,7 +944,7 @@
|
|||
<tr t-if="player.stage">
|
||||
<td><strong>Status:</strong></td>
|
||||
<td>
|
||||
<span t-if="player.stage == 'healthy'" class="text-success font-weight-bold"><i class="fa fa-heart"></i> Play OK</span>
|
||||
<span t-if="player.stage == 'healthy'" class="text-success font-weight-bold"><i class="fa fa-thumbs-up"></i> Play OK</span>
|
||||
<span t-elif="player.stage == 'practice_ok'" class="text-warning font-weight-bold"><i class="fa fa-running"></i> Practice OK</span>
|
||||
<span t-elif="player.stage == 'no_play'" class="text-danger font-weight-bold"><i class="fa fa-ban"></i> Injured</span>
|
||||
<span t-else="" class="text-muted" t-esc="player.stage"/>
|
||||
|
|
@ -1049,7 +1056,7 @@
|
|||
<!-- Medical Information Tab (Treatment Professionals Only) -->
|
||||
<div class="tab-pane fade" id="medical-info" role="tabpanel" aria-labelledby="medical-info-tab" t-if="is_treatment_prof">
|
||||
<div class="row">
|
||||
<div class="col-md-6" t-if="patient_info.get('allergies')">
|
||||
<div class="col-md-6" t-if="(patient_info.get('allergies') or '').replace('\xa0','').strip()">
|
||||
<div class="card border-warning">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<i class="fa fa-exclamation-triangle"></i> <strong>Allergies</strong>
|
||||
|
|
@ -1069,7 +1076,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12" t-if="not patient_info.get('allergies') and not patient_info.get('team_info_notes')">
|
||||
<div class="col-12" t-if="not (patient_info.get('allergies') or '').replace('\xa0','').strip() and not (patient_info.get('team_info_notes') or '').replace('\xa0','').strip()">
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fa fa-notes-medical fa-3x mb-3"></i>
|
||||
<p>No medical information available</p>
|
||||
|
|
@ -1183,18 +1190,22 @@
|
|||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Success/Error Messages -->
|
||||
<t t-if="error" class="alert alert-danger" role="alert">
|
||||
<div t-if="error" class="alert alert-danger" role="alert">
|
||||
<i class="fa fa-exclamation-triangle me-2"/>
|
||||
<span t-esc="error"/>
|
||||
</t>
|
||||
<t t-if="success" class="alert alert-success" role="alert">
|
||||
</div>
|
||||
<div t-if="success" class="alert alert-success" role="alert">
|
||||
<i class="fa fa-check-circle me-2"/>
|
||||
<span t-esc="success"/>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<form t-attf-action="/my/team/{{ team.id }}/add_player/submit" method="post" class="mt-3">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" name="team_id" t-att-value="team.id"/>
|
||||
<!-- Player Details -->
|
||||
<div class="mb-2">
|
||||
<h5 class="mb-0">Player Details</h5>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="first_name" class="form-label">First Name *</label>
|
||||
|
|
@ -1226,6 +1237,27 @@
|
|||
t-att-value="date_of_birth or ''" required="required"/>
|
||||
</div>
|
||||
|
||||
<!-- Status fields (Treatment Professionals only) -->
|
||||
<t t-if="user_has_group('bemade_sports_clinic.group_portal_treatment_professional') or user_has_group('base.group_system')">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="match_status" class="form-label">Match Status</label>
|
||||
<select class="form-select" id="match_status" name="match_status">
|
||||
<option value="yes" t-att-selected="(match_status or '') == 'yes' and 'selected' or None">Yes</option>
|
||||
<option value="no" t-att-selected="(match_status or '') == 'no' and 'selected' or None">No</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="practice_status" class="form-label">Practice Status</label>
|
||||
<select class="form-select" id="practice_status" name="practice_status">
|
||||
<option value="yes" t-att-selected="(practice_status or '') == 'yes' and 'selected' or None">Yes</option>
|
||||
<option value="no_contact" t-att-selected="(practice_status or '') == 'no_contact' and 'selected' or None">Yes, no contact</option>
|
||||
<option value="no" t-att-selected="(practice_status or '') == 'no' and 'selected' or None">No</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a t-attf-href="/my/team?team_id={{ team.id }}" class="btn bg-o-color-2 text-white">
|
||||
<i class="fa fa-times me-1"/> Cancel
|
||||
|
|
|
|||
Loading…
Reference in a new issue