- Replace unreliable data-dismiss='modal' with direct JavaScript closeModal() calls - Add robust closeModal() function with Bootstrap detection and DOM fallback - Fix Complete, Reschedule, and Cancel modal dismiss button functionality - Wrap JavaScript in CDATA section to resolve XML parsing errors with unescaped ampersands - Add copyFeedbackToHidden() and copyDateToHidden() functions for proper form data transfer - Ensure all modal action buttons work consistently across portal UI All portal activity modal interactions now functional with console logging for debugging.
419 lines
17 KiB
Python
419 lines
17 KiB
Python
from odoo import models, fields, api, _
|
|
from datetime import datetime, date
|
|
import pytz
|
|
from odoo.exceptions import ValidationError, UserError, AccessError
|
|
import logging
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
external_tracking_fields = {
|
|
"diagnosis",
|
|
"predicted_resolution_date",
|
|
"resolution_date",
|
|
"external_notes",
|
|
}
|
|
|
|
# Include only fields not already included in external_tracking_fields here
|
|
internal_tracking_fields = {
|
|
"internal_notes",
|
|
"parental_consent",
|
|
}
|
|
|
|
|
|
class PatientInjury(models.Model):
|
|
_name = "sports.patient.injury"
|
|
_description = "Patient Injury"
|
|
_inherit = ["mail.thread", "mail.activity.mixin"]
|
|
_rec_name = "diagnosis"
|
|
_order = "create_date desc, id desc"
|
|
|
|
@api.model
|
|
def _today(self):
|
|
"""Get the current date in the user's time zone."""
|
|
return datetime.now(pytz.timezone(self.env.user.tz or "GMT"))
|
|
|
|
# TODO: Find a way to improve notifications send about tracking injury details
|
|
# TODO: Add field consentement_parental = fields.Selection(oui, non, non-applicable)
|
|
|
|
patient_id = fields.Many2one(
|
|
comodel_name="sports.patient",
|
|
string="Patient",
|
|
readonly=True,
|
|
required=True,
|
|
ondelete="cascade",
|
|
)
|
|
patient_name = fields.Char(related="patient_id.name")
|
|
team_id = fields.Many2one(
|
|
comodel_name="sports.team",
|
|
string="Team",
|
|
help="The team for which this injury was reported, especially important when a player belongs to multiple teams.",
|
|
)
|
|
diagnosis = fields.Char(tracking=True)
|
|
|
|
injury_date = fields.Date(
|
|
string="Date of Injury",
|
|
default=_today,
|
|
)
|
|
injury_date_na = fields.Boolean(string="N/A", default=False)
|
|
internal_notes = fields.Text(tracking=True)
|
|
external_notes = fields.Text(tracking=True)
|
|
treatment_professional_ids = fields.Many2many(
|
|
comodel_name="res.users",
|
|
relation="patient_injury_treatment_pro_rel",
|
|
column1="patient_injury_id",
|
|
column2="treatment_pro_id",
|
|
string="Treatment Professionals",
|
|
domain=lambda self: [('groups_id', 'in', [
|
|
self.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional').id,
|
|
self.env.ref('bemade_sports_clinic.group_portal_treatment_professional').id
|
|
])],
|
|
tracking=True,
|
|
)
|
|
predicted_resolution_date = fields.Date(tracking=True)
|
|
resolution_date = fields.Date(
|
|
tracking=True, help="The date when the injury was actually resolved."
|
|
)
|
|
stage = fields.Selection(
|
|
selection=[
|
|
("unverified", "Unverified"),
|
|
("active", "Active"),
|
|
("resolved", "Resolved")
|
|
],
|
|
string="Status",
|
|
default="unverified",
|
|
tracking=True,
|
|
copy=False,
|
|
help="""
|
|
- Unverified: Injury has been reported but not yet verified by a treatment professional
|
|
- Active: Injury has been verified and is being treated
|
|
- Resolved: Injury has been resolved
|
|
"""
|
|
)
|
|
parental_consent = fields.Selection(
|
|
string="Consent for Disclosure to Parent",
|
|
selection=[("yes", "Yes"), ("no", "No"), ("na", "Not Applicable")],
|
|
help="Whether the patient has given their consent to share injury details with their parents.",
|
|
tracking=True,
|
|
)
|
|
|
|
# Fields for injury categorization - using Char instead of foreign keys
|
|
body_location = fields.Char(
|
|
string="Body Location",
|
|
help="The anatomical location of the injury",
|
|
tracking=True,
|
|
)
|
|
|
|
injury_type = fields.Char(
|
|
string="Injury Type",
|
|
help="The type of injury (e.g., sprain, fracture, strain)",
|
|
tracking=True,
|
|
)
|
|
|
|
severity = fields.Selection(
|
|
selection=[
|
|
("mild", "Mild"),
|
|
("moderate", "Moderate"),
|
|
("severe", "Severe"),
|
|
],
|
|
string="Severity",
|
|
help="The assessed severity of the injury",
|
|
tracking=True,
|
|
)
|
|
|
|
# Relations to new models
|
|
# Now treatment notes are linked to patient primarily, but can be optionally linked to injuries
|
|
treatment_note_ids = fields.One2many(
|
|
comodel_name='sports.treatment.note',
|
|
inverse_name='injury_id',
|
|
string='Treatment Notes',
|
|
help='Treatment notes specifically linked to this injury'
|
|
)
|
|
treatment_note_count = fields.Integer(
|
|
string='Treatment Note Count',
|
|
compute='_compute_treatment_note_count'
|
|
)
|
|
document_ids = fields.One2many(
|
|
comodel_name='sports.injury.document',
|
|
inverse_name='injury_id',
|
|
string='Documents'
|
|
)
|
|
document_count = fields.Integer(
|
|
string='Document Count',
|
|
compute='_compute_document_count'
|
|
)
|
|
activity_count = fields.Integer(
|
|
string='Activity Count',
|
|
compute='_compute_activity_count'
|
|
)
|
|
|
|
@api.depends('treatment_note_ids')
|
|
def _compute_treatment_note_count(self):
|
|
for record in self:
|
|
record.treatment_note_count = len(record.treatment_note_ids)
|
|
|
|
@api.depends('document_ids')
|
|
def _compute_document_count(self):
|
|
for record in self:
|
|
record.document_count = len(record.document_ids)
|
|
|
|
def _compute_activity_count(self):
|
|
for rec in self:
|
|
rec.activity_count = self.env['mail.activity'].search_count([
|
|
('res_model', '=', 'sports.patient.injury'),
|
|
('res_id', '=', rec.id)
|
|
])
|
|
|
|
@api.constrains("injury_date_na", "injury_date")
|
|
def constrain_date_blank_only_if_na(self):
|
|
for rec in self:
|
|
if not rec.injury_date_na and not rec.injury_date:
|
|
raise ValidationError(
|
|
_("If injury date is not set, the N/A box must be checked.")
|
|
)
|
|
|
|
@api.onchange("injury_date_na")
|
|
def _onchange_injury_date_na(self):
|
|
for rec in self:
|
|
if rec.injury_date_na:
|
|
rec.injury_date = None
|
|
|
|
@api.onchange("injury_date")
|
|
def _onchange_injury_date(self):
|
|
for rec in self:
|
|
if rec.injury_date:
|
|
rec.injury_date_na = False
|
|
|
|
def action_verify_injury(self):
|
|
"""Verify an injury, changing its status from unverified to active.
|
|
Only treatment professionals or internal users with appropriate rights can verify injuries."""
|
|
self.ensure_one()
|
|
if self.stage != "unverified":
|
|
raise UserError(_("Only unverified injuries can be verified."))
|
|
|
|
# Check if current user is a treatment professional or has appropriate rights
|
|
if not (self.env.user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional') or
|
|
self.env.user.has_group('base.group_system')):
|
|
raise AccessError(_("Only treatment professionals can verify injuries."))
|
|
|
|
self.write({'stage': 'active'})
|
|
message = _("Injury verified by %s") % self.env.user.name
|
|
self.message_post(body=message)
|
|
return True
|
|
|
|
def action_resolve_injury(self):
|
|
"""Mark an injury as resolved."""
|
|
self.ensure_one()
|
|
if self.stage == "resolved":
|
|
return True
|
|
|
|
self.write({
|
|
'stage': 'resolved',
|
|
'resolution_date': fields.Date.context_today(self)
|
|
})
|
|
message = _("Injury marked as resolved by %s") % self.env.user.name
|
|
self.message_post(body=message)
|
|
return True
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
res = super().create(vals_list)
|
|
for rec in res.sudo():
|
|
# Subscribe the patient's partners to this injury
|
|
rec.message_subscribe(rec.patient_id.message_partner_ids)
|
|
|
|
# Manage treatment professional subscriptions
|
|
rec._manage_treatment_professional_subscriptions()
|
|
|
|
# Post a message about the new injury
|
|
msg_body = _("A new injury was created for this patient.")
|
|
if rec.diagnosis:
|
|
msg_body += _(" Diagnosis: %s." % rec.diagnosis)
|
|
rec.patient_id.message_post(body=msg_body, message_type="comment")
|
|
return res
|
|
|
|
def write(self, vals):
|
|
"""Override write to update subscriptions if treatment professionals change"""
|
|
# Store current treatment professional IDs before update
|
|
old_treatment_prof_ids = {}
|
|
if 'treatment_professional_ids' in vals:
|
|
for rec in self:
|
|
old_treatment_prof_ids[rec.id] = rec.treatment_professional_ids.ids
|
|
|
|
res = super().write(vals)
|
|
|
|
# If treatment professionals changed, update subscriptions
|
|
if 'treatment_professional_ids' in vals:
|
|
for rec in self:
|
|
# Only run subscription manager if treatment professionals actually changed
|
|
if rec.id in old_treatment_prof_ids and set(old_treatment_prof_ids[rec.id]) != set(rec.treatment_professional_ids.ids):
|
|
rec._manage_treatment_professional_subscriptions()
|
|
|
|
# Log the change in treatment professionals
|
|
new_profs = rec.treatment_professional_ids - self.env['res.users'].browse(old_treatment_prof_ids[rec.id])
|
|
removed_profs = self.env['res.users'].browse(old_treatment_prof_ids[rec.id]) - rec.treatment_professional_ids
|
|
|
|
# Create user-friendly message
|
|
msg = ''
|
|
if new_profs:
|
|
prof_names = ', '.join(new_profs.mapped('name'))
|
|
msg += _('Added treatment professional(s): %s. ') % prof_names
|
|
if removed_profs:
|
|
prof_names = ', '.join(removed_profs.mapped('name'))
|
|
msg += _('Removed treatment professional(s): %s.') % prof_names
|
|
|
|
if msg:
|
|
rec.message_post(body=msg)
|
|
|
|
# Also update subscriptions if internal_notes changes
|
|
if 'internal_notes' in vals:
|
|
for rec in self:
|
|
rec._manage_treatment_professional_subscriptions()
|
|
|
|
return res
|
|
|
|
def _manage_treatment_professional_subscriptions(self):
|
|
"""Subscribe treatment professionals to both regular and internal note updates
|
|
while ensuring non-treatment professionals only subscribe to external updates."""
|
|
self.ensure_one()
|
|
|
|
# Get the message subtypes
|
|
external_subtype = self.env.ref('bemade_sports_clinic.subtype_patient_injury_external_update')
|
|
internal_subtype = self.env.ref('bemade_sports_clinic.subtype_patient_injury_internal_update')
|
|
|
|
# Get all followers
|
|
followers = self.env['mail.followers'].search([
|
|
('res_model', '=', 'sports.patient.injury'),
|
|
('res_id', '=', self.id)
|
|
])
|
|
|
|
for follower in followers:
|
|
partner = self.env['res.partner'].browse(follower.partner_id.id)
|
|
users = self.env['res.users'].search([('partner_id', '=', partner.id)])
|
|
|
|
# Check if any of the users is a treatment professional
|
|
# Note: We can't use has_group() on non-current users, so we check group membership directly
|
|
is_treatment_prof = False
|
|
treatment_prof_group = self.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
|
for user in users:
|
|
if treatment_prof_group in user.groups_id:
|
|
is_treatment_prof = True
|
|
break
|
|
|
|
# Update follower subtypes based on role
|
|
if is_treatment_prof:
|
|
# Treatment professionals get both types of notifications
|
|
follower.write({
|
|
'subtype_ids': [(6, 0, [external_subtype.id, internal_subtype.id])]
|
|
})
|
|
else:
|
|
# Regular users only get external notifications
|
|
follower.write({
|
|
'subtype_ids': [(6, 0, [external_subtype.id])]
|
|
})
|
|
|
|
# Make sure treatment professionals (if any) are subscribed
|
|
if self.treatment_professional_ids:
|
|
self.message_subscribe(
|
|
partner_ids=self.treatment_professional_ids.mapped('partner_id').ids,
|
|
subtype_ids=[external_subtype.id, internal_subtype.id]
|
|
)
|
|
|
|
def unlink(self):
|
|
for rec in self:
|
|
msg_body = _("An injury was deleted.")
|
|
if rec.diagnosis:
|
|
msg_body += _(" Diagnosis: %s." % rec.diagnosis)
|
|
rec.patient_id.message_post(body=msg_body, message_type="comment")
|
|
return super().unlink()
|
|
|
|
def action_view_injury_form(self):
|
|
self.ensure_one()
|
|
return {
|
|
"type": "ir.actions.act_window",
|
|
"view_mode": "form",
|
|
"res_model": "sports.patient.injury",
|
|
"res_id": self.id,
|
|
"context": self._context,
|
|
}
|
|
|
|
def _track_subtype(self, init_values):
|
|
return self.env.ref("mail.mt_note")
|
|
|
|
def _track_template(self, changes):
|
|
res = super()._track_template(changes)
|
|
params = set(changes)
|
|
external = bool(external_tracking_fields & params)
|
|
if external:
|
|
first_external_field = (external_tracking_fields & params).pop()
|
|
res[first_external_field] = (
|
|
self.env.ref(
|
|
"bemade_sports_clinic.mail_template_patient_injury_status_update"
|
|
),
|
|
{
|
|
"auto_delete": False,
|
|
"subtype_id": self.env.ref(
|
|
"bemade_sports_clinic.subtype_patient_injury_external_update"
|
|
).id,
|
|
"email_layout_xmlid": "mail.mail_notification_light",
|
|
},
|
|
)
|
|
if "internal_notes" in changes:
|
|
res["internal_notes"] = (
|
|
self.env.ref(
|
|
"bemade_sports_clinic.mail_template_patient_injury_new_internal_note"
|
|
),
|
|
{
|
|
"auto_delete": False,
|
|
"subtype_id": self.env.ref(
|
|
"bemade_sports_clinic.subtype_patient_injury_internal_update"
|
|
).id,
|
|
"email_layout_xmlid": "mail.mail_notification_light",
|
|
},
|
|
)
|
|
return res
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
res = super().create(vals_list)
|
|
|
|
for record in res:
|
|
# Check if the current user is a treatment professional or admin
|
|
is_treatment_professional = self.env.user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
|
is_admin = self.env.user.has_group('base.group_system')
|
|
|
|
if is_treatment_professional or is_admin:
|
|
# If created by a treatment professional or admin, set to active
|
|
record.write({'stage': 'active'})
|
|
else:
|
|
# Otherwise, set to unverified
|
|
record.write({'stage': 'unverified'})
|
|
# Automatically assign therapist when creating an injury
|
|
current_user = self.env.user
|
|
|
|
# If the injury creator is a treatment professional, assign them
|
|
if current_user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional'):
|
|
record.treatment_professional_ids = [(4, current_user.id)]
|
|
# Otherwise, if there's a team_id, find and assign team therapists
|
|
elif record.team_id:
|
|
# Find all team staff users
|
|
team_staff = self.env['sports.team.staff'].search([
|
|
('team_id', '=', record.team_id.id)
|
|
])
|
|
|
|
# Filter to only staff users who are treatment professionals
|
|
if team_staff:
|
|
treatment_professional_group = self.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
|
# Get all users from staff and filter them by group
|
|
# Use direct group membership check instead of has_group() to avoid security violations
|
|
staff_users = team_staff.mapped('user_ids')
|
|
treatment_prof_group = self.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
|
therapist_users = staff_users.filtered(
|
|
lambda user: treatment_prof_group in user.groups_id
|
|
)
|
|
|
|
if therapist_users:
|
|
record.treatment_professional_ids = [(6, 0, therapist_users.ids)]
|
|
|
|
record.patient_id.recompute_followers()
|
|
|
|
return res
|