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