- Added integrated mail.activity system for task management - Implemented portal access to activities for treatment professionals - Added activity creation, completion, and reassignment functionality - Enhanced player management with Canadian address validation - Improved injury tracking with parental consent and document attachments - Implemented layered security architecture (ACL + Record Rules + Controller filtering) - Added French Canadian (fr_CA) localization support - Enhanced portal UI with activity counts and navigation - Implemented RPC security protection with buddy method pattern - Added comprehensive demo data and integration features - Updated manifest to reflect all new capabilities and improvements
418 lines
17 KiB
Python
418 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 sent about tracking injury details
|
|
|
|
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",
|
|
|
|
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
|
|
# NOTE: These fields are currently retained for potential future use.
|
|
# They provide structured injury categorization that may be valuable for
|
|
# reporting, analytics, or enhanced injury tracking features.
|
|
body_location = fields.Char(
|
|
string="Body Location",
|
|
help="The anatomical location of the injury (retained for future use)",
|
|
tracking=True,
|
|
)
|
|
|
|
injury_type = fields.Char(
|
|
string="Injury Type",
|
|
help="The type of injury (e.g., sprain, fracture, strain) - retained for future use",
|
|
tracking=True,
|
|
)
|
|
|
|
severity = fields.Selection(
|
|
selection=[
|
|
("mild", "Mild"),
|
|
("moderate", "Moderate"),
|
|
("severe", "Severe"),
|
|
],
|
|
string="Severity",
|
|
help="The assessed severity of the injury (retained for future use)",
|
|
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
|