portal: enable player removal from Edit Player view; fix QWeb group checks
- Replace nested remove form with a submit button using HTML5 formaction to post directly to /my/team/{team_id}/player/{player_id}/remove
- Prevent outer save form from intercepting submission; preserves CSRF; confirmation retained; redirects to team page
- Replace user_has_group helper with request.env.user.has_group in QWeb to prevent NoneType callable errors
Verification:
- Edit Player page renders without 500 error
- Clicking Remove removes the player and returns 303 redirect to /my/team/{team_id}
- Success notification displayed on team page
Notes:
- Removal route already implemented in controller (TeamManagementPortal.portal_remove_player)
This commit is contained in:
parent
78589bbcf3
commit
3ce8d051bc
12 changed files with 586 additions and 103 deletions
|
|
@ -92,7 +92,7 @@ class EventsPortal(CustomerPortal, AccessControlMixin):
|
||||||
|
|
||||||
@http.route(['/my/events', '/my/events/page/<int:page>'], type='http', auth='user', website=True)
|
@http.route(['/my/events', '/my/events/page/<int:page>'], type='http', auth='user', website=True)
|
||||||
def view_events(self, page=1, view_type='all', team_id=None, organization_id=None, assigned_user_id=None,
|
def view_events(self, page=1, view_type='all', team_id=None, organization_id=None, assigned_user_id=None,
|
||||||
date_from=None, date_to=None, sortby=None, search=None, **kw):
|
date_from=None, date_to=None, sortby=None, search=None, no_default_dates=None, **kw):
|
||||||
"""Main events view with filtering and pagination"""
|
"""Main events view with filtering and pagination"""
|
||||||
|
|
||||||
# Check access
|
# Check access
|
||||||
|
|
@ -104,6 +104,17 @@ class EventsPortal(CustomerPortal, AccessControlMixin):
|
||||||
if not (is_therapist or is_coach or user.has_group('base.group_system')):
|
if not (is_therapist or is_coach or user.has_group('base.group_system')):
|
||||||
raise AccessError(_("You don't have access to events."))
|
raise AccessError(_("You don't have access to events."))
|
||||||
|
|
||||||
|
# Default date filter: from yesterday, similar to internal "Upcoming" behavior
|
||||||
|
# Only apply when user did not specify any date filters AND no explicit clear flag
|
||||||
|
if not date_from and not date_to and not no_default_dates:
|
||||||
|
try:
|
||||||
|
# Use date (not datetime) input format 'YYYY-MM-DD' for the portal date picker
|
||||||
|
yesterday = (fields.Date.today() - timedelta(days=1))
|
||||||
|
date_from = fields.Date.to_string(yesterday)
|
||||||
|
except Exception:
|
||||||
|
# Fallback using datetime in rare cases
|
||||||
|
date_from = (datetime.today() - timedelta(days=1)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
# Prepare base domain
|
# Prepare base domain
|
||||||
domain = self._prepare_events_domain(view_type)
|
domain = self._prepare_events_domain(view_type)
|
||||||
|
|
||||||
|
|
@ -167,7 +178,7 @@ class EventsPortal(CustomerPortal, AccessControlMixin):
|
||||||
url='/my/events',
|
url='/my/events',
|
||||||
url_args={'view_type': view_type, 'team_id': team_id, 'organization_id': organization_id,
|
url_args={'view_type': view_type, 'team_id': team_id, 'organization_id': organization_id,
|
||||||
'assigned_user_id': assigned_user_id, 'date_from': date_from,
|
'assigned_user_id': assigned_user_id, 'date_from': date_from,
|
||||||
'date_to': date_to, 'sortby': sortby, 'search': search},
|
'date_to': date_to, 'sortby': sortby, 'search': search, 'no_default_dates': no_default_dates},
|
||||||
total=event_count,
|
total=event_count,
|
||||||
page=page,
|
page=page,
|
||||||
step=self._items_per_page,
|
step=self._items_per_page,
|
||||||
|
|
@ -215,6 +226,7 @@ class EventsPortal(CustomerPortal, AccessControlMixin):
|
||||||
'organizations': organizations,
|
'organizations': organizations,
|
||||||
'treatment_professionals': treatment_professionals,
|
'treatment_professionals': treatment_professionals,
|
||||||
'can_edit': can_edit,
|
'can_edit': can_edit,
|
||||||
|
'no_default_dates': no_default_dates,
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.request.render('bemade_sports_clinic.portal_events_list', values)
|
return http.request.render('bemade_sports_clinic.portal_events_list', values)
|
||||||
|
|
|
||||||
|
|
@ -556,9 +556,13 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
||||||
# Get user's role
|
# Get user's role
|
||||||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||||
|
|
||||||
# Document categories
|
# Document categories (aligned with model)
|
||||||
categories = [('medical', 'Medical'), ('xray', 'X-Ray'), ('mri', 'MRI'),
|
categories = [
|
||||||
('prescription', 'Prescription'), ('other', 'Other')]
|
('medical', 'Medical'),
|
||||||
|
('medical_imaging', 'Medical Imaging'),
|
||||||
|
('prescription', 'Prescription'),
|
||||||
|
('other', 'Other'),
|
||||||
|
]
|
||||||
|
|
||||||
values = {
|
values = {
|
||||||
'injury': injury,
|
'injury': injury,
|
||||||
|
|
@ -608,6 +612,7 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
||||||
# Create the document
|
# Create the document
|
||||||
document = request.env['sports.injury.document'].sudo().create({
|
document = request.env['sports.injury.document'].sudo().create({
|
||||||
'injury_id': int(injury_id),
|
'injury_id': int(injury_id),
|
||||||
|
'patient_id': injury.patient_id.id,
|
||||||
'name': post.get('document_name', name),
|
'name': post.get('document_name', name),
|
||||||
'description': post.get('description', ''),
|
'description': post.get('description', ''),
|
||||||
'category': post.get('category', 'other'),
|
'category': post.get('category', 'other'),
|
||||||
|
|
@ -632,8 +637,11 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
||||||
raise request.not_found()
|
raise request.not_found()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check access to the injury this document belongs to
|
# Prefer checking access via injury; if no injury, check via patient
|
||||||
injury = self._check_access_to_injury(document.injury_id.id)
|
if document.injury_id:
|
||||||
|
self._check_access_to_injury(document.injury_id.id)
|
||||||
|
else:
|
||||||
|
self._check_access_to_patient(document.patient_id.id)
|
||||||
except UserError:
|
except UserError:
|
||||||
raise request.not_found()
|
raise request.not_found()
|
||||||
|
|
||||||
|
|
@ -646,6 +654,72 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@http.route(['/my/patient/document/download/<int:document_id>'], type='http', auth='user')
|
||||||
|
def download_patient_document(self, document_id, **post):
|
||||||
|
"""Download a document linked to a patient (injury optional)."""
|
||||||
|
document = request.env['sports.injury.document'].sudo().browse(int(document_id))
|
||||||
|
if not document.exists():
|
||||||
|
raise request.not_found()
|
||||||
|
try:
|
||||||
|
# Access check based on patient (primary link)
|
||||||
|
self._check_access_to_patient(document.patient_id.id)
|
||||||
|
except UserError:
|
||||||
|
raise request.not_found()
|
||||||
|
return request.make_response(
|
||||||
|
base64.b64decode(document.file_content),
|
||||||
|
headers=[
|
||||||
|
('Content-Type', 'application/octet-stream'),
|
||||||
|
('Content-Disposition', f'attachment; filename="{document.file_name}"'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@http.route(['/my/patient/document/upload'], type='http', auth='user', website=True, methods=['POST'])
|
||||||
|
def upload_patient_document(self, **post):
|
||||||
|
"""Upload a document directly to a patient (injury optional)."""
|
||||||
|
patient_id = post.get('patient_id')
|
||||||
|
if not patient_id:
|
||||||
|
return request.redirect('/my/players')
|
||||||
|
|
||||||
|
try:
|
||||||
|
patient = self._check_access_to_patient(patient_id)
|
||||||
|
except UserError as e:
|
||||||
|
return request.render('http_routing.http_error', {
|
||||||
|
'status_code': 403,
|
||||||
|
'status_message': 'Forbidden',
|
||||||
|
'error_message': str(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
attachment = post.get('attachment')
|
||||||
|
if not attachment:
|
||||||
|
return request.redirect(f'/my/player?player_id={patient.id}&error=no_file')
|
||||||
|
|
||||||
|
try:
|
||||||
|
name = attachment.filename
|
||||||
|
file_content = attachment.read()
|
||||||
|
file_size = len(file_content)
|
||||||
|
|
||||||
|
# 10MB limit
|
||||||
|
if file_size > 10 * 1024 * 1024:
|
||||||
|
return request.redirect(f'/my/player?player_id={patient.id}&error=file_too_large')
|
||||||
|
|
||||||
|
# Create patient-linked document (injury optional)
|
||||||
|
request.env['sports.injury.document'].sudo().create({
|
||||||
|
'patient_id': patient.id,
|
||||||
|
'injury_id': int(post['injury_id']) if post.get('injury_id') else False,
|
||||||
|
'name': post.get('document_name', name),
|
||||||
|
'description': post.get('description', ''),
|
||||||
|
'category': post.get('category', 'other'),
|
||||||
|
'file_content': base64.b64encode(file_content),
|
||||||
|
'file_name': name,
|
||||||
|
'created_by_id': request.env.user.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return request.redirect(f'/my/player?player_id={patient.id}&success=document_uploaded')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Error uploading patient document: {e}")
|
||||||
|
return request.redirect(f'/my/player?player_id={patient.id}&error=upload_failed')
|
||||||
|
|
||||||
@http.route(['/my/injury/document/delete/<int:document_id>'], type='http', auth='user', website=True)
|
@http.route(['/my/injury/document/delete/<int:document_id>'], type='http', auth='user', website=True)
|
||||||
def delete_injury_document(self, document_id, **post):
|
def delete_injury_document(self, document_id, **post):
|
||||||
"""Delete a document attached to an injury"""
|
"""Delete a document attached to an injury"""
|
||||||
|
|
|
||||||
|
|
@ -159,13 +159,25 @@ class TeamStaffPortal(CustomerPortal):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Show all injuries to treatment professionals, but only active ones to coaches
|
# Show all injuries to treatment professionals, but only active ones to coaches
|
||||||
if is_treatment_prof:
|
if is_treatment_prof:
|
||||||
injuries = player.injury_ids
|
injuries = player.injury_ids
|
||||||
else:
|
else:
|
||||||
injuries = player.injury_ids.filtered(lambda r: r.stage == 'active')
|
injuries = player.injury_ids.filtered(lambda r: r.stage == 'active')
|
||||||
|
|
||||||
|
# Patient documents for Documents tab (primary association now on patient)
|
||||||
|
patient_documents = http.request.env['sports.injury.document'].search([
|
||||||
|
('patient_id', '=', player.id)
|
||||||
|
], order='create_date desc, id desc')
|
||||||
|
|
||||||
|
# Categories for patient document uploads
|
||||||
|
categories = [
|
||||||
|
('medical', 'Medical'),
|
||||||
|
('medical_imaging', 'Medical Imaging'),
|
||||||
|
('prescription', 'Prescription'),
|
||||||
|
('other', 'Other'),
|
||||||
|
]
|
||||||
|
|
||||||
# Create patient_info dictionary for protected fields (when user is a treatment professional)
|
# Create patient_info dictionary for protected fields (when user is a treatment professional)
|
||||||
# No need for sudo() now that we have proper field-level access rights
|
# No need for sudo() now that we have proper field-level access rights
|
||||||
patient_info = {}
|
patient_info = {}
|
||||||
|
|
@ -179,6 +191,8 @@ class TeamStaffPortal(CustomerPortal):
|
||||||
qcontext={
|
qcontext={
|
||||||
'player': player,
|
'player': player,
|
||||||
'injuries': injuries,
|
'injuries': injuries,
|
||||||
|
'patient_documents': patient_documents,
|
||||||
|
'categories': categories,
|
||||||
'team': team,
|
'team': team,
|
||||||
'page_name': 'my_player',
|
'page_name': 'my_player',
|
||||||
'is_treatment_prof': is_treatment_prof,
|
'is_treatment_prof': is_treatment_prof,
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,49 @@ class InjuryDocument(models.Model):
|
||||||
_order = 'create_date desc, id desc'
|
_order = 'create_date desc, id desc'
|
||||||
|
|
||||||
name = fields.Char(string='Name', required=True)
|
name = fields.Char(string='Name', required=True)
|
||||||
injury_id = fields.Many2one('sports.patient.injury', string='Injury', required=True, ondelete='cascade')
|
# Refactor: documents are tied to a patient with optional injury linkage
|
||||||
patient_id = fields.Many2one('sports.patient', string='Patient', related='injury_id.patient_id', store=True)
|
patient_id = fields.Many2one('sports.patient', string='Patient', required=True, ondelete='cascade', index=True)
|
||||||
|
injury_id = fields.Many2one('sports.patient.injury', string='Injury', required=False, ondelete='cascade', index=True)
|
||||||
description = fields.Text(string='Description')
|
description = fields.Text(string='Description')
|
||||||
file_content = fields.Binary(string='File Content', required=True, attachment=False)
|
file_content = fields.Binary(string='File Content', required=True, attachment=False)
|
||||||
file_name = fields.Char(string='File Name')
|
file_name = fields.Char(string='File Name')
|
||||||
file_size = fields.Integer(string='File Size', compute='_compute_file_size', store=True)
|
file_size = fields.Integer(string='File Size', compute='_compute_file_size', store=True)
|
||||||
|
# Note: 'xray' and 'mri' kept for backward compatibility; displayed as 'Medical Imaging'
|
||||||
category = fields.Selection([
|
category = fields.Selection([
|
||||||
('medical', 'Medical Report'),
|
('medical', 'Medical'),
|
||||||
('xray', 'X-Ray'),
|
('medical_imaging', 'Medical Imaging'),
|
||||||
('mri', 'MRI'),
|
('xray', 'Medical Imaging'), # deprecated
|
||||||
|
('mri', 'Medical Imaging'), # deprecated
|
||||||
('prescription', 'Prescription'),
|
('prescription', 'Prescription'),
|
||||||
('other', 'Other'),
|
('other', 'Other'),
|
||||||
], string='Category', default='other', required=True)
|
], string='Category', default='other', required=True)
|
||||||
created_by_id = fields.Many2one('res.users', string='Uploaded By', default=lambda self: self.env.user, required=True)
|
created_by_id = fields.Many2one('res.users', string='Uploaded By', default=lambda self: self.env.user, required=True)
|
||||||
create_date = fields.Datetime(string='Upload Date')
|
create_date = fields.Datetime(string='Upload Date')
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
"""Ensure backward compatibility: if an injury is provided but patient is missing,
|
||||||
|
set patient_id from the injury's patient."""
|
||||||
|
for vals in vals_list:
|
||||||
|
if not vals.get('patient_id') and vals.get('injury_id'):
|
||||||
|
injury = self.env['sports.patient.injury'].browse(vals['injury_id'])
|
||||||
|
vals['patient_id'] = injury.patient_id.id
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
@api.onchange('injury_id')
|
||||||
|
def _onchange_injury_id_sync_patient(self):
|
||||||
|
"""When selecting an injury, auto-set patient to the injury's patient."""
|
||||||
|
for rec in self:
|
||||||
|
if rec.injury_id:
|
||||||
|
rec.patient_id = rec.injury_id.patient_id
|
||||||
|
|
||||||
|
@api.constrains('injury_id', 'patient_id')
|
||||||
|
def _check_injury_belongs_to_patient(self):
|
||||||
|
"""Ensure the selected injury belongs to the chosen patient."""
|
||||||
|
for rec in self:
|
||||||
|
if rec.injury_id and rec.patient_id and rec.injury_id.patient_id != rec.patient_id:
|
||||||
|
raise ValidationError(_('The injury must belong to the selected patient.'))
|
||||||
|
|
||||||
@api.depends('file_content')
|
@api.depends('file_content')
|
||||||
def _compute_file_size(self):
|
def _compute_file_size(self):
|
||||||
"""Compute the file size in bytes"""
|
"""Compute the file size in bytes"""
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,16 @@ class Patient(models.Model):
|
||||||
last_consultation_date = fields.Date(tracking=True)
|
last_consultation_date = fields.Date(tracking=True)
|
||||||
active_injury_count = fields.Integer(compute="_compute_active_injury_count")
|
active_injury_count = fields.Integer(compute="_compute_active_injury_count")
|
||||||
activity_count = fields.Integer(compute="_compute_activity_count")
|
activity_count = fields.Integer(compute="_compute_activity_count")
|
||||||
|
# Documents linked to this patient (optionally to an injury)
|
||||||
|
document_ids = fields.One2many(
|
||||||
|
comodel_name="sports.injury.document",
|
||||||
|
inverse_name="patient_id",
|
||||||
|
string="Documents",
|
||||||
|
)
|
||||||
|
document_count = fields.Integer(
|
||||||
|
compute="_compute_document_count",
|
||||||
|
string="Document Count",
|
||||||
|
)
|
||||||
allergies = fields.Text(
|
allergies = fields.Text(
|
||||||
groups="bemade_sports_clinic.group_sports_clinic_treatment_professional,bemade_sports_clinic.group_portal_treatment_professional",
|
groups="bemade_sports_clinic.group_sports_clinic_treatment_professional,bemade_sports_clinic.group_portal_treatment_professional",
|
||||||
)
|
)
|
||||||
|
|
@ -287,6 +297,25 @@ class Patient(models.Model):
|
||||||
('res_id', '=', rec.id)
|
('res_id', '=', rec.id)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
def _compute_document_count(self):
|
||||||
|
for patient in self:
|
||||||
|
patient.document_count = self.env['sports.injury.document'].search_count([
|
||||||
|
('patient_id', '=', patient.id)
|
||||||
|
])
|
||||||
|
|
||||||
|
def action_view_documents(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': _('Documents'),
|
||||||
|
'res_model': 'sports.injury.document',
|
||||||
|
'view_mode': 'list,form',
|
||||||
|
'domain': [('patient_id', '=', self.id)],
|
||||||
|
'context': {
|
||||||
|
'default_patient_id': self.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
def action_view_patient_form(self):
|
def action_view_patient_form(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -29,26 +29,26 @@
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a t-attf-class="nav-link #{'active' if view_type == 'all' else ''}"
|
<a t-attf-class="nav-link #{'active' if view_type == 'all' else ''}"
|
||||||
t-attf-href="/my/events?view_type=all#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}">
|
t-attf-href="/my/events?view_type=all#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}#{no_default_dates and '&no_default_dates=1' or ''}">
|
||||||
All Events
|
All Events
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a t-attf-class="nav-link #{'active' if view_type == 'my' else ''}"
|
<a t-attf-class="nav-link #{'active' if view_type == 'my' else ''}"
|
||||||
t-attf-href="/my/events?view_type=my#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}">
|
t-attf-href="/my/events?view_type=my#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}#{no_default_dates and '&no_default_dates=1' or ''}">
|
||||||
My Events
|
My Events
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a t-attf-class="nav-link #{'active' if view_type == 'unassigned' else ''}"
|
<a t-attf-class="nav-link #{'active' if view_type == 'unassigned' else ''}"
|
||||||
t-attf-href="/my/events?view_type=unassigned#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}">
|
t-attf-href="/my/events?view_type=unassigned#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}#{no_default_dates and '&no_default_dates=1' or ''}">
|
||||||
Unassigned Events
|
Unassigned Events
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 text-end">
|
<div class="col-md-6 text-end">
|
||||||
<a href="/my/events?view_type=all" class="btn btn-outline-secondary btn-sm">
|
<a href="/my/events?view_type=all&no_default_dates=1" class="btn btn-outline-secondary btn-sm">
|
||||||
<i class="fa fa-times"/> Clear Filters
|
<i class="fa fa-times"/> Clear Filters
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -101,18 +101,33 @@
|
||||||
|
|
||||||
<!-- Second Row: Date Filters -->
|
<!-- Second Row: Date Filters -->
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
|
<input type="hidden" name="no_default_dates" t-att-value="no_default_dates or ''"/>
|
||||||
<!-- Date From Filter -->
|
<!-- Date From Filter -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="date_from" class="form-label text-muted small">From Date</label>
|
<label for="date_from" class="form-label text-muted small">From Date</label>
|
||||||
<input type="date" id="date_from" name="date_from" class="form-control"
|
<div class="input-group">
|
||||||
t-att-value="date_from" onchange="this.form.submit();"/>
|
<input type="date" id="date_from" name="date_from" class="form-control"
|
||||||
|
t-att-value="date_from" onchange="this.form.submit();"/>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
title="Clear From Date"
|
||||||
|
onclick="document.getElementById('date_from').value=''; this.form.no_default_dates.value='1'; this.form.submit();">
|
||||||
|
<i class="fa fa-times"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Date To Filter -->
|
<!-- Date To Filter -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="date_to" class="form-label text-muted small">To Date</label>
|
<label for="date_to" class="form-label text-muted small">To Date</label>
|
||||||
<input type="date" id="date_to" name="date_to" class="form-control"
|
<div class="input-group">
|
||||||
t-att-value="date_to" onchange="this.form.submit();"/>
|
<input type="date" id="date_to" name="date_to" class="form-control"
|
||||||
|
t-att-value="date_to" onchange="this.form.submit();"/>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
title="Clear To Date"
|
||||||
|
onclick="document.getElementById('date_to').value=''; this.form.no_default_dates.value='1'; this.form.submit();">
|
||||||
|
<i class="fa fa-times"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -199,7 +214,8 @@
|
||||||
<td>
|
<td>
|
||||||
<strong t-esc="event.name"/>
|
<strong t-esc="event.name"/>
|
||||||
<t t-if="event.description">
|
<t t-if="event.description">
|
||||||
<br/><small class="text-muted" t-esc="event.description[:100] + '...' if len(event.description) > 100 else event.description"/>
|
<br/>
|
||||||
|
<div t-field="event.description" class="text-muted oe_no_empty"/>
|
||||||
</t>
|
</t>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -581,17 +581,16 @@
|
||||||
<t t-foreach="documents" t-as="doc">
|
<t t-foreach="documents" t-as="doc">
|
||||||
<tr>
|
<tr>
|
||||||
<td><t t-esc="doc.name"/></td>
|
<td><t t-esc="doc.name"/></td>
|
||||||
<td>
|
<td>
|
||||||
<span t-if="doc.category == 'medical'" class="badge badge-info">Medical</span>
|
<span t-if="doc.category == 'medical'" class="badge badge-info">Medical</span>
|
||||||
<span t-elif="doc.category == 'xray'" class="badge badge-primary">X-Ray</span>
|
<span t-elif="doc.category == 'medical_imaging' or doc.category == 'xray' or doc.category == 'mri'" class="badge badge-primary">Medical Imaging</span>
|
||||||
<span t-elif="doc.category == 'mri'" class="badge badge-warning">MRI</span>
|
<span t-elif="doc.category == 'prescription'" class="badge badge-success">Prescription</span>
|
||||||
<span t-elif="doc.category == 'prescription'" class="badge badge-success">Prescription</span>
|
<span t-else="" class="badge badge-secondary">Other</span>
|
||||||
<span t-else="" class="badge badge-secondary">Other</span>
|
</td>
|
||||||
</td>
|
<td><t t-raw="doc.description or ''"/></td>
|
||||||
<td><t t-esc="doc.description"/></td>
|
<td><span t-field="doc.created_by_id"/></td>
|
||||||
<td><span t-field="doc.created_by_id"/></td>
|
<td><span t-field="doc.create_date" t-options='{"widget": "date"}'/></td>
|
||||||
<td><span t-field="doc.create_date" t-options='{"widget": "date"}'/></td>
|
<td>
|
||||||
<td>
|
|
||||||
<a t-att-href="'/my/injury/document/download/%s' % doc.id" class="btn btn-sm bg-o-color-1 text-white">
|
<a t-att-href="'/my/injury/document/download/%s' % doc.id" class="btn btn-sm bg-o-color-1 text-white">
|
||||||
<i class="fa fa-download"></i> Download
|
<i class="fa fa-download"></i> Download
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,82 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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="row mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|
|
||||||
|
|
@ -138,10 +138,30 @@
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="description" class="form-label">Description</label>
|
<label for="description_editor" class="form-label">Description</label>
|
||||||
<textarea class="form-control" id="description" name="description" rows="4"
|
<!-- Hidden field actually submitted -->
|
||||||
t-esc="event.description"></textarea>
|
<input type="hidden" id="description" name="description" t-att-value="event.description or ''"/>
|
||||||
|
<!-- Simple rich-text editor -->
|
||||||
|
<div id="description_editor"
|
||||||
|
class="form-control"
|
||||||
|
style="min-height: 150px; overflow:auto;"
|
||||||
|
contenteditable="true">
|
||||||
|
<div t-field="event.description" class="oe_no_empty"/>
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">You can paste formatted text. The formatted content will be saved.</small>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function() {
|
||||||
|
const form = document.currentScript && document.currentScript.closest('form');
|
||||||
|
if (!form) return;
|
||||||
|
const editor = form.querySelector('#description_editor');
|
||||||
|
const hidden = form.querySelector('#description');
|
||||||
|
if (!editor || !hidden) return;
|
||||||
|
form.addEventListener('submit', function() {
|
||||||
|
hidden.value = editor.innerHTML;
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,24 +7,26 @@
|
||||||
<t t-call="portal.portal_docs_entry">
|
<t t-call="portal.portal_docs_entry">
|
||||||
<t t-set="title">Teams</t>
|
<t t-set="title">Teams</t>
|
||||||
<t t-set="url">/my/teams</t>
|
<t t-set="url">/my/teams</t>
|
||||||
<t t-set="show_count" t-value="True"/>
|
<t t-set="placeholder_count">teams_count</t>
|
||||||
</t>
|
</t>
|
||||||
<t t-call="portal.portal_docs_entry">
|
<t t-call="portal.portal_docs_entry">
|
||||||
<t t-set="title">Players</t>
|
<t t-set="title">Players</t>
|
||||||
<t t-set="url">/my/players</t>
|
<t t-set="url">/my/players</t>
|
||||||
<t t-set="show_count" t-value="True"/>
|
<t t-set="placeholder_count">players_count</t>
|
||||||
</t>
|
</t>
|
||||||
<t t-call="portal.portal_docs_entry">
|
<t t-call="portal.portal_docs_entry">
|
||||||
<t t-set="title">Activities</t>
|
<t t-set="title">Activities</t>
|
||||||
<t t-set="url">/my/activities</t>
|
<t t-set="url">/my/activities</t>
|
||||||
<t t-set="show_count" t-value="True"/>
|
<t t-set="placeholder_count">activities_count</t>
|
||||||
<t t-set="config_card" t-value="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')"/>
|
<t t-set="config_card" t-value="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')"/>
|
||||||
</t>
|
</t>
|
||||||
<t t-call="portal.portal_docs_entry">
|
<t t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')">
|
||||||
<t t-set="title">Events</t>
|
<t t-call="portal.portal_docs_entry">
|
||||||
<t t-set="url">/my/events</t>
|
<t t-set="title">Events</t>
|
||||||
<t t-set="show_count" t-value="True"/>
|
<t t-set="url">/my/events</t>
|
||||||
<t t-set="config_card" t-value="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')"/>
|
<t t-set="placeholder_count">events_count</t>
|
||||||
|
<t t-set="config_card" t-value="True"/>
|
||||||
|
</t>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|
@ -92,46 +94,24 @@
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Remove problematic placeholder_count attributes for coaches and treatment professionals
|
// Scope any DOM tweaks to the portal home container and coaches only
|
||||||
if (document.body.classList.contains('o_portal_user_coach') ||
|
const home = document.querySelector('.o_portal_my_home');
|
||||||
document.body.classList.contains('o_portal_user_therapist')) {
|
if (!home) return;
|
||||||
|
const isCoach = home.classList.contains('o_portal_user_coach');
|
||||||
|
|
||||||
console.log('Portal spinner fix: Detected coach/therapist user');
|
if (!isCoach) {
|
||||||
|
// Do not alter placeholders or visibility for therapists or other users
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove ALL placeholder_count attributes that cause spinner issues
|
// Minimal safe tweak for coaches only: if Odoo leaves a long-running spinner, remove it after a grace period
|
||||||
const allCounterElements = document.querySelectorAll('[data-placeholder_count]');
|
setTimeout(function() {
|
||||||
console.log('Portal spinner fix: Found ' + allCounterElements.length + ' placeholder_count elements');
|
|
||||||
|
|
||||||
allCounterElements.forEach(function(el) {
|
|
||||||
const counterType = el.getAttribute('data-placeholder_count');
|
|
||||||
console.log('Portal spinner fix: Removing ' + counterType + ' counter');
|
|
||||||
el.removeAttribute('data-placeholder_count');
|
|
||||||
|
|
||||||
// Hide the parent card
|
|
||||||
const card = el.closest('.o_portal_index_card');
|
|
||||||
if (card && !card.classList.contains('d-none')) {
|
|
||||||
card.classList.add('d-none');
|
|
||||||
console.log('Portal spinner fix: Hiding card for ' + counterType);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Force remove spinner immediately
|
|
||||||
const spinner = document.querySelector('.o_portal_doc_spinner');
|
const spinner = document.querySelector('.o_portal_doc_spinner');
|
||||||
if (spinner) {
|
if (spinner) {
|
||||||
spinner.remove();
|
spinner.remove();
|
||||||
console.log('Portal spinner fix: Spinner removed');
|
console.log('Portal spinner fix: Spinner removed for coach');
|
||||||
}
|
}
|
||||||
|
}, 2500);
|
||||||
// Also remove any knowledge cards for therapists (they should only see activities)
|
|
||||||
const knowledgeCards = document.querySelectorAll('a[href*="/knowledge"]');
|
|
||||||
knowledgeCards.forEach(function(card) {
|
|
||||||
const parentCard = card.closest('.o_portal_index_card');
|
|
||||||
if (parentCard) {
|
|
||||||
parentCard.classList.add('d-none');
|
|
||||||
console.log('Portal spinner fix: Hiding knowledge card');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
]]>
|
]]>
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -561,6 +541,14 @@
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="documents-tab" data-bs-toggle="tab" data-bs-target="#documents" type="button" role="tab" aria-controls="documents" aria-selected="false">
|
||||||
|
<i class="fa fa-file"></i> Documents
|
||||||
|
<span class="badge badge-pill badge-secondary ms-1" t-if="patient_documents">
|
||||||
|
<t t-esc="len(patient_documents)"/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
@ -811,7 +799,7 @@
|
||||||
<h5 class="mt-4">Team Notes</h5>
|
<h5 class="mt-4">Team Notes</h5>
|
||||||
<div class="card border-info">
|
<div class="card border-info">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p t-esc="player.team_info_notes" class="mb-0"/>
|
<div t-raw="player.team_info_notes" class="mb-0 oe_no_empty"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
|
|
@ -819,6 +807,95 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Documents Tab -->
|
||||||
|
<div class="tab-pane fade" id="documents" role="tabpanel" aria-labelledby="documents-tab">
|
||||||
|
<t t-if="is_treatment_prof">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="fa fa-upload"></i> Upload Document
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="/my/patient/document/upload" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
|
<input type="hidden" name="patient_id" t-att-value="player.id"/>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Name</label>
|
||||||
|
<input type="text" name="document_name" class="form-control" placeholder="Document name"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Category</label>
|
||||||
|
<select name="category" class="form-select">
|
||||||
|
<t t-foreach="categories" t-as="cat">
|
||||||
|
<option t-att-value="cat[0]" t-esc="cat[1]"/>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Link to Injury (optional)</label>
|
||||||
|
<select name="injury_id" class="form-select">
|
||||||
|
<option value="">-- None --</option>
|
||||||
|
<t t-foreach="injuries" t-as="injury">
|
||||||
|
<option t-att-value="injury.id" t-esc="injury.display_name"/>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<textarea name="description" class="form-control" rows="2" placeholder="Optional description"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label">File</label>
|
||||||
|
<input type="file" name="attachment" class="form-control" required="required"/>
|
||||||
|
<small class="form-text text-muted">Max size 10MB</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 d-flex align-items-end justify-content-end">
|
||||||
|
<button type="submit" class="btn bg-o-color-1 text-white">
|
||||||
|
<i class="fa fa-upload"></i> Upload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-if="patient_documents">
|
||||||
|
<t t-call="portal.portal_table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>By</th>
|
||||||
|
<th class="text-end">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<t t-foreach="patient_documents" t-as="doc">
|
||||||
|
<tr>
|
||||||
|
<td><span t-esc="doc.name"/></td>
|
||||||
|
<td><span t-esc="doc.category"/></td>
|
||||||
|
<td><span t-raw="doc.description or ''"/></td>
|
||||||
|
<td><span t-field="doc.create_date" t-options="{'widget': 'datetime'}"/></td>
|
||||||
|
<td><span t-esc="doc.created_by_id.display_name"/></td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a t-att-href="'/my/patient/document/download/%s' % doc.id" class="btn btn-sm bg-o-color-1 text-white">
|
||||||
|
<i class="fa fa-download"></i> Download
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
</tbody>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
<div t-else="" class="text-center text-muted py-4">
|
||||||
|
<i class="fa fa-file fa-3x mb-3"></i>
|
||||||
|
<p>No documents uploaded</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Medical Information Tab (Treatment Professionals Only) -->
|
<!-- 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="tab-pane fade" id="medical-info" role="tabpanel" aria-labelledby="medical-info-tab" t-if="is_treatment_prof">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,72 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
|
<!-- Injury Document: backend views -->
|
||||||
|
<record id="view_injury_document_tree" model="ir.ui.view">
|
||||||
|
<field name="name">sports.injury.document.list</field>
|
||||||
|
<field name="model">sports.injury.document</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Injury Documents" create="true" delete="true">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="patient_id"/>
|
||||||
|
<field name="injury_id"/>
|
||||||
|
<field name="category"/>
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="created_by_id"/>
|
||||||
|
<field name="create_date"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_injury_document_form" model="ir.ui.view">
|
||||||
|
<field name="name">sports.injury.document.form</field>
|
||||||
|
<field name="model">sports.injury.document</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Injury Document">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="patient_id"/>
|
||||||
|
<field name="injury_id" domain="[('patient_id', '=', patient_id)]"/>
|
||||||
|
<field name="category"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="file_content" filename="file_name"/>
|
||||||
|
<field name="file_name"/>
|
||||||
|
<field name="file_size" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="description"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
<footer/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_injury_document_search" model="ir.ui.view">
|
||||||
|
<field name="name">sports.injury.document.search</field>
|
||||||
|
<field name="model">sports.injury.document</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="patient_id"/>
|
||||||
|
<field name="injury_id"/>
|
||||||
|
<field name="category"/>
|
||||||
|
<field name="created_by_id"/>
|
||||||
|
<filter string="My Uploads" name="my_uploads" domain="[('created_by_id', '=', uid)]"/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_sports_injury_documents" model="ir.actions.act_window">
|
||||||
|
<field name="name">Injury Documents</field>
|
||||||
|
<field name="res_model">sports.injury.document</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="context">{"search_default_injury_id": active_id, "default_injury_id": active_id}</field>
|
||||||
|
<field name="domain">[("injury_id", "=", active_id)]</field>
|
||||||
|
</record>
|
||||||
<record id="sports_patient_injury_view_form" model="ir.ui.view">
|
<record id="sports_patient_injury_view_form" model="ir.ui.view">
|
||||||
<field name="name">sports.patient.injury.view.form</field>
|
<field name="name">sports.patient.injury.view.form</field>
|
||||||
<field name="model">sports.patient.injury</field>
|
<field name="model">sports.patient.injury</field>
|
||||||
|
|
@ -7,6 +74,10 @@
|
||||||
<form>
|
<form>
|
||||||
<header>
|
<header>
|
||||||
<field name="stage" widget="statusbar"/>
|
<field name="stage" widget="statusbar"/>
|
||||||
|
<!-- Smart button: Documents -->
|
||||||
|
<button class="oe_stat_button" type="action" name="%(action_sports_injury_documents)d" icon="fa-file">
|
||||||
|
<field name="document_count" string="Documents" widget="statinfo"/>
|
||||||
|
</button>
|
||||||
<button name="action_verify_injury"
|
<button name="action_verify_injury"
|
||||||
type="object"
|
type="object"
|
||||||
class="oe_highlight"
|
class="oe_highlight"
|
||||||
|
|
@ -59,6 +130,38 @@
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
|
<page string="Documents">
|
||||||
|
<field name="document_ids">
|
||||||
|
<list string="Documents" editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="patient_id"/>
|
||||||
|
<field name="category"/>
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="created_by_id" readonly="1"/>
|
||||||
|
<field name="create_date" readonly="1"/>
|
||||||
|
</list>
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="patient_id"/>
|
||||||
|
<field name="injury_id" domain="[('patient_id', '=', patient_id)]"/>
|
||||||
|
<field name="category"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="file_content" filename="file_name"/>
|
||||||
|
<field name="file_name"/>
|
||||||
|
<field name="file_size" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="description"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
<chatter reload_on_post="True"/>
|
<chatter reload_on_post="True"/>
|
||||||
|
|
@ -80,6 +183,7 @@
|
||||||
<field name="treatment_professional_ids"
|
<field name="treatment_professional_ids"
|
||||||
widget="many2many_avatar_user"/>
|
widget="many2many_avatar_user"/>
|
||||||
<field name="activity_ids" widget="list_activity"/>
|
<field name="activity_ids" widget="list_activity"/>
|
||||||
|
<field name="document_count"/>
|
||||||
<field name="parental_consent"/>
|
<field name="parental_consent"/>
|
||||||
<button name="action_view_injury_form" type="object"
|
<button name="action_view_injury_form" type="object"
|
||||||
title="Details" string="Details"
|
title="Details" string="Details"
|
||||||
|
|
|
||||||
|
|
@ -113,15 +113,18 @@
|
||||||
<header>
|
<header>
|
||||||
<field name="stage" widget="statusbar" />
|
<field name="stage" widget="statusbar" />
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
<div class="oe_button_box" name="button_box">
|
<div class="oe_button_box" name="button_box">
|
||||||
<button name="action_report_injury" type="object" class="oe_stat_button" icon="fa-medkit">
|
<button name="action_report_injury" type="object" class="oe_stat_button" icon="fa-medkit">
|
||||||
<div class="o_stat_info">
|
<div class="o_stat_info">
|
||||||
<span class="o_stat_text">Report</span>
|
<span class="o_stat_text">Report</span>
|
||||||
<span class="o_stat_text">Injury</span>
|
<span class="o_stat_text">Injury</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button name="action_view_documents" type="object" class="oe_stat_button" icon="fa-file">
|
||||||
|
<field name="document_count" widget="statinfo" string="Documents"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="oe_title"><h1>Patient Record</h1></div>
|
<div class="oe_title"><h1>Patient Record</h1></div>
|
||||||
<group>
|
<group>
|
||||||
<group col="3">
|
<group col="3">
|
||||||
|
|
@ -148,11 +151,11 @@
|
||||||
<field name="return_date"/>
|
<field name="return_date"/>
|
||||||
<field name="team_info_notes"/>
|
<field name="team_info_notes"/>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page string="Injuries">
|
<page string="Injuries">
|
||||||
<field name="injury_ids"/>
|
<field name="injury_ids"/>
|
||||||
</page>
|
</page>
|
||||||
<page string="Treatment Notes">
|
<page string="Treatment Notes">
|
||||||
<field name="treatment_note_ids">
|
<field name="treatment_note_ids">
|
||||||
<list string="Treatment Notes" editable="bottom">
|
<list string="Treatment Notes" editable="bottom">
|
||||||
<field name="date"/>
|
<field name="date"/>
|
||||||
|
|
@ -162,8 +165,8 @@
|
||||||
<field name="user_id" readonly="1"/>
|
<field name="user_id" readonly="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
<page string="Contacts">
|
<page string="Contacts">
|
||||||
<group>
|
<group>
|
||||||
<group string="Patient Phone & Email">
|
<group string="Patient Phone & Email">
|
||||||
<field name="phone" widget="phone"/>
|
<field name="phone" widget="phone"/>
|
||||||
|
|
@ -190,10 +193,42 @@
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
<page string="Documents">
|
||||||
</group>
|
<field name="document_ids">
|
||||||
</sheet>
|
<list string="Documents" editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="injury_id" domain="[('patient_id', '=', parent.id)]"/>
|
||||||
|
<field name="category"/>
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="created_by_id" readonly="1"/>
|
||||||
|
<field name="create_date" readonly="1"/>
|
||||||
|
</list>
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="patient_id"/>
|
||||||
|
<field name="injury_id" domain="[('patient_id', '=', patient_id)]"/>
|
||||||
|
<field name="category"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="file_content" filename="file_name"/>
|
||||||
|
<field name="file_name"/>
|
||||||
|
<field name="file_size" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="description"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
<chatter reload_on_post="True"/>
|
<chatter reload_on_post="True"/>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue