894 lines
27 KiB
Python
894 lines
27 KiB
Python
"""
|
||
Occupational Therapy (OT) models for the Tenhal Multidisciplinary Healthcare Platform.
|
||
|
||
This module handles OT consultations and session notes based on OT-F-1 and OT-F-3 forms.
|
||
Enhanced with comprehensive field-level data capture and scoring system.
|
||
"""
|
||
|
||
from django.db import models
|
||
from django.utils.translation import gettext_lazy as _
|
||
from simple_history.models import HistoricalRecords
|
||
|
||
from core.models import (
|
||
UUIDPrimaryKeyMixin,
|
||
TimeStampedMixin,
|
||
TenantOwnedMixin,
|
||
ClinicallySignableMixin,
|
||
)
|
||
|
||
|
||
class OTConsult(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin, ClinicallySignableMixin):
|
||
"""
|
||
OT consultation form (OT-F-1).
|
||
Initial assessment and evaluation with comprehensive data capture.
|
||
"""
|
||
|
||
class ReferralReason(models.TextChoices):
|
||
DIAGNOSIS = 'Diagnosis', _('Multi-disciplinary Team Diagnosis')
|
||
CONSULTATION = 'Consultation', _('Consultation')
|
||
ASSESSMENT = 'Assessment', _('Assessment')
|
||
INTERVENTION = 'Intervention', _('Intervention')
|
||
PARENT_TRAINING = 'ParentTraining', _('Parent Training')
|
||
|
||
class Recommendation(models.TextChoices):
|
||
CONTINUE = 'CONTINUE', _('Continue Treatment')
|
||
DISCHARGE = 'DISCHARGE', _('Discharge')
|
||
REFER_TO_OTHER = 'REFER_TO_OTHER', _('Refer to Other Service')
|
||
|
||
# Core Relationships
|
||
patient = models.ForeignKey(
|
||
'core.Patient',
|
||
on_delete=models.CASCADE,
|
||
related_name='ot_consults',
|
||
verbose_name=_("Patient")
|
||
)
|
||
appointment = models.ForeignKey(
|
||
'appointments.Appointment',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='ot_consults',
|
||
verbose_name=_("Appointment")
|
||
)
|
||
consultation_date = models.DateField(
|
||
verbose_name=_("Consultation Date")
|
||
)
|
||
provider = models.ForeignKey(
|
||
'core.User',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
related_name='ot_consults_provided',
|
||
verbose_name=_("Provider")
|
||
)
|
||
|
||
# Section 2: Reasons for Referral
|
||
referral_reason = models.CharField(
|
||
max_length=50,
|
||
choices=ReferralReason.choices,
|
||
blank=True,
|
||
verbose_name=_("Referral Reason")
|
||
)
|
||
|
||
# Section 4: Developmental History - Motor Learning & Regression
|
||
motor_learning_difficulty = models.BooleanField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name=_("Difficulty Learning New Motor Skills")
|
||
)
|
||
motor_learning_details = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Motor Learning Details")
|
||
)
|
||
motor_skill_regression = models.BooleanField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name=_("Lost Previously Gained Motor Skills")
|
||
)
|
||
regression_details = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Regression Details")
|
||
)
|
||
|
||
# Section 6: Eating/Feeding
|
||
eats_healthy_variety = models.BooleanField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name=_("Eats Healthy Variety of Food")
|
||
)
|
||
eats_variety_textures = models.BooleanField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name=_("Eats Variety of Textures and Flavors")
|
||
)
|
||
participates_family_meals = models.BooleanField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name=_("Participates in Family Meals")
|
||
)
|
||
eating_comments = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Eating Comments")
|
||
)
|
||
|
||
# Section 7: Behavior Comments
|
||
infant_behavior_comments = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Infant Behavior Comments")
|
||
)
|
||
current_behavior_comments = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Current Behavior Comments")
|
||
)
|
||
|
||
# Section 8: Recommendation
|
||
recommendation = models.CharField(
|
||
max_length=20,
|
||
choices=Recommendation.choices,
|
||
blank=True,
|
||
verbose_name=_("Recommendation")
|
||
)
|
||
recommendation_notes = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Recommendation Notes")
|
||
)
|
||
|
||
# Section 10: Clinician Signature
|
||
clinician_name = models.CharField(
|
||
max_length=200,
|
||
blank=True,
|
||
verbose_name=_("Clinician Name")
|
||
)
|
||
clinician_signature = models.CharField(
|
||
max_length=200,
|
||
blank=True,
|
||
verbose_name=_("Clinician Signature")
|
||
)
|
||
|
||
# Scoring fields (calculated)
|
||
self_help_score = models.PositiveIntegerField(
|
||
default=0,
|
||
verbose_name=_("Self-Help Score")
|
||
)
|
||
behavior_score = models.PositiveIntegerField(
|
||
default=0,
|
||
verbose_name=_("Behavior Score")
|
||
)
|
||
developmental_score = models.PositiveIntegerField(
|
||
default=0,
|
||
verbose_name=_("Developmental Score")
|
||
)
|
||
eating_score = models.PositiveIntegerField(
|
||
default=0,
|
||
verbose_name=_("Eating Score")
|
||
)
|
||
total_score = models.PositiveIntegerField(
|
||
default=0,
|
||
verbose_name=_("Total Score")
|
||
)
|
||
score_interpretation = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
verbose_name=_("Score Interpretation")
|
||
)
|
||
|
||
history = HistoricalRecords()
|
||
|
||
class Meta:
|
||
verbose_name = _("OT Consultation")
|
||
verbose_name_plural = _("OT Consultations")
|
||
ordering = ['-consultation_date', '-created_at']
|
||
indexes = [
|
||
models.Index(fields=['patient', 'consultation_date']),
|
||
models.Index(fields=['provider', 'consultation_date']),
|
||
models.Index(fields=['tenant', 'consultation_date']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"OT Consultation - {self.patient} - {self.consultation_date}"
|
||
|
||
def calculate_scores(self):
|
||
"""Calculate all scores based on related data."""
|
||
# Self-Help Score (max 24)
|
||
self_help_score = 0
|
||
for skill in self.self_help_skills.all():
|
||
if skill.response == 'yes':
|
||
self_help_score += 2
|
||
|
||
# Behavior Score (max 48)
|
||
behavior_score = 0
|
||
for behavior in self.infant_behaviors.all():
|
||
if behavior.response == 'yes':
|
||
behavior_score += 2
|
||
elif behavior.response == 'sometimes':
|
||
behavior_score += 1
|
||
for behavior in self.current_behaviors.all():
|
||
if behavior.response == 'yes':
|
||
behavior_score += 2
|
||
elif behavior.response == 'sometimes':
|
||
behavior_score += 1
|
||
|
||
# Developmental Score (max 6)
|
||
developmental_score = 0
|
||
required_milestones = self.milestones.filter(is_required=True)
|
||
for milestone in required_milestones:
|
||
if milestone.age_achieved:
|
||
developmental_score += 2
|
||
|
||
# Eating Score (max 6)
|
||
eating_score = 0
|
||
if self.eats_healthy_variety:
|
||
eating_score += 2
|
||
if self.eats_variety_textures:
|
||
eating_score += 2
|
||
if self.participates_family_meals:
|
||
eating_score += 2
|
||
|
||
# Total Score
|
||
total_score = self_help_score + behavior_score + developmental_score + eating_score
|
||
|
||
# Interpretation
|
||
if total_score <= 30:
|
||
interpretation = "⚠️ Needs Immediate Attention"
|
||
elif total_score <= 60:
|
||
interpretation = "⚠ Moderate Difficulty - Follow-Up Needed"
|
||
else:
|
||
interpretation = "✅ Age-Appropriate Skills"
|
||
|
||
# Update fields
|
||
self.self_help_score = self_help_score
|
||
self.behavior_score = behavior_score
|
||
self.developmental_score = developmental_score
|
||
self.eating_score = eating_score
|
||
self.total_score = total_score
|
||
self.score_interpretation = interpretation
|
||
|
||
return {
|
||
'self_help': self_help_score,
|
||
'behavior': behavior_score,
|
||
'developmental': developmental_score,
|
||
'eating': eating_score,
|
||
'total': total_score,
|
||
'interpretation': interpretation
|
||
}
|
||
|
||
|
||
class OTDifficultyArea(UUIDPrimaryKeyMixin):
|
||
"""
|
||
Section 3: Areas of Difficulty (max 3 selections).
|
||
"""
|
||
|
||
AREA_CHOICES = [
|
||
('sensory', _('Sensory skills')),
|
||
('fineMotor', _('Fine motor skills')),
|
||
('grossMotor', _('Gross motor skills')),
|
||
('oralMotor', _('Oral motor / Feeding')),
|
||
('adl', _('ADL Activities')),
|
||
('handwriting', _('Handwriting')),
|
||
('play', _('Play')),
|
||
('social', _('Social skills')),
|
||
('selfInjury', _('Self-injurious behavior')),
|
||
('disorganized', _('Disorganized behaviors')),
|
||
('homeRec', _('Home recommendations')),
|
||
('parentEd', _('Parental education')),
|
||
]
|
||
|
||
consult = models.ForeignKey(
|
||
OTConsult,
|
||
on_delete=models.CASCADE,
|
||
related_name='difficulty_areas',
|
||
verbose_name=_("Consultation")
|
||
)
|
||
area = models.CharField(
|
||
max_length=50,
|
||
choices=AREA_CHOICES,
|
||
verbose_name=_("Difficulty Area")
|
||
)
|
||
details = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Details")
|
||
)
|
||
order = models.PositiveSmallIntegerField(
|
||
default=0,
|
||
verbose_name=_("Order")
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = _("OT Difficulty Area")
|
||
verbose_name_plural = _("OT Difficulty Areas")
|
||
ordering = ['consult', 'order']
|
||
unique_together = [['consult', 'area']]
|
||
|
||
def __str__(self):
|
||
return f"{self.get_area_display()} - {self.consult}"
|
||
|
||
|
||
class OTMilestone(UUIDPrimaryKeyMixin):
|
||
"""
|
||
Section 4: Developmental History - Motor Milestones.
|
||
"""
|
||
|
||
MILESTONE_CHOICES = [
|
||
('headControl', _('Controlling head')),
|
||
('reachObject', _('Reaching for object')),
|
||
('rollOver', _('Rolling over both ways')),
|
||
('fingerFeed', _('Finger feeding')),
|
||
('sitting', _('Sitting alone')),
|
||
('pullStand', _('Pulling to stand')),
|
||
('crawling', _('Creeping on all fours')),
|
||
('drawCircle', _('Drawing a circle')),
|
||
('spoon', _('Eating with spoon')),
|
||
('cutScissors', _('Cutting with scissors')),
|
||
('walking', _('Walking')),
|
||
('drinkCup', _('Drinking from a cup')),
|
||
('jump', _('Jumping')),
|
||
('hop', _('Hopping')),
|
||
('hopOneFoot', _('Hopping on one foot')),
|
||
('bike', _('Riding a bike')),
|
||
]
|
||
|
||
consult = models.ForeignKey(
|
||
OTConsult,
|
||
on_delete=models.CASCADE,
|
||
related_name='milestones',
|
||
verbose_name=_("Consultation")
|
||
)
|
||
milestone = models.CharField(
|
||
max_length=50,
|
||
choices=MILESTONE_CHOICES,
|
||
verbose_name=_("Milestone")
|
||
)
|
||
age_achieved = models.CharField(
|
||
max_length=50,
|
||
blank=True,
|
||
verbose_name=_("Age Achieved")
|
||
)
|
||
notes = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Notes")
|
||
)
|
||
is_required = models.BooleanField(
|
||
default=False,
|
||
verbose_name=_("Required Field")
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = _("OT Milestone")
|
||
verbose_name_plural = _("OT Milestones")
|
||
ordering = ['consult', 'id']
|
||
unique_together = [['consult', 'milestone']]
|
||
|
||
def __str__(self):
|
||
return f"{self.get_milestone_display()} - {self.age_achieved}"
|
||
|
||
|
||
class OTSelfHelpSkill(UUIDPrimaryKeyMixin):
|
||
"""
|
||
Section 5: Self-Help Skills by age range.
|
||
"""
|
||
|
||
AGE_RANGE_CHOICES = [
|
||
('8-9', _('8-9 months')),
|
||
('12-18', _('12-18 months')),
|
||
('18-24', _('18-24 months')),
|
||
('2-3', _('2-3 years')),
|
||
('3-4', _('3-4 years')),
|
||
('5-6', _('5-6 years')),
|
||
]
|
||
|
||
RESPONSE_CHOICES = [
|
||
('yes', _('Yes')),
|
||
('no', _('No')),
|
||
]
|
||
|
||
consult = models.ForeignKey(
|
||
OTConsult,
|
||
on_delete=models.CASCADE,
|
||
related_name='self_help_skills',
|
||
verbose_name=_("Consultation")
|
||
)
|
||
age_range = models.CharField(
|
||
max_length=10,
|
||
choices=AGE_RANGE_CHOICES,
|
||
verbose_name=_("Age Range")
|
||
)
|
||
skill_name = models.CharField(
|
||
max_length=200,
|
||
verbose_name=_("Skill")
|
||
)
|
||
response = models.CharField(
|
||
max_length=10,
|
||
choices=RESPONSE_CHOICES,
|
||
blank=True,
|
||
verbose_name=_("Response")
|
||
)
|
||
comments = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Comments")
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = _("OT Self-Help Skill")
|
||
verbose_name_plural = _("OT Self-Help Skills")
|
||
ordering = ['consult', 'age_range', 'id']
|
||
|
||
def __str__(self):
|
||
return f"{self.age_range}: {self.skill_name}"
|
||
|
||
|
||
class OTInfantBehavior(UUIDPrimaryKeyMixin):
|
||
"""
|
||
Section 7: Infant Behavior (First 12 Months).
|
||
"""
|
||
|
||
BEHAVIOR_CHOICES = [
|
||
('cried', _('Cried a lot, fussy, irritable')),
|
||
('good', _('Was good, non-demanding')),
|
||
('alert', _('Was alert')),
|
||
('quiet', _('Was quiet')),
|
||
('passive', _('Was passive')),
|
||
('active', _('Was active')),
|
||
('likedHeld', _('Liked being held')),
|
||
('resistedHeld', _('Resisted being held')),
|
||
('floppy', _('Was floppy when held')),
|
||
('tense', _('Was tense when held')),
|
||
('sleepGood', _('Had good sleep patterns')),
|
||
('sleepIrregular', _('Had irregular sleep patterns')),
|
||
]
|
||
|
||
RESPONSE_CHOICES = [
|
||
('yes', _('Yes')),
|
||
('no', _('No')),
|
||
('sometimes', _('Sometimes')),
|
||
]
|
||
|
||
consult = models.ForeignKey(
|
||
OTConsult,
|
||
on_delete=models.CASCADE,
|
||
related_name='infant_behaviors',
|
||
verbose_name=_("Consultation")
|
||
)
|
||
behavior = models.CharField(
|
||
max_length=50,
|
||
choices=BEHAVIOR_CHOICES,
|
||
verbose_name=_("Behavior")
|
||
)
|
||
response = models.CharField(
|
||
max_length=20,
|
||
choices=RESPONSE_CHOICES,
|
||
blank=True,
|
||
verbose_name=_("Response")
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = _("OT Infant Behavior")
|
||
verbose_name_plural = _("OT Infant Behaviors")
|
||
ordering = ['consult', 'id']
|
||
unique_together = [['consult', 'behavior']]
|
||
|
||
def __str__(self):
|
||
return f"{self.get_behavior_display()}: {self.response}"
|
||
|
||
|
||
class OTCurrentBehavior(UUIDPrimaryKeyMixin):
|
||
"""
|
||
Section 7: Current Behavior.
|
||
"""
|
||
|
||
BEHAVIOR_CHOICES = [
|
||
('quiet', _('Is mostly quiet')),
|
||
('active', _('Is overly active')),
|
||
('tires', _('Tires easily')),
|
||
('talks', _('Talks constantly')),
|
||
('impulsive', _('Is impulsive')),
|
||
('restless', _('Is restless')),
|
||
('stubborn', _('Is stubborn')),
|
||
('resistant', _('Is resistant to change')),
|
||
('fights', _('Fights frequently')),
|
||
('tantrums', _('Exhibits frequent temper tantrums')),
|
||
('clumsy', _('Is clumsy')),
|
||
('frustrated', _('Is frustrated easily')),
|
||
]
|
||
|
||
RESPONSE_CHOICES = [
|
||
('yes', _('Yes')),
|
||
('no', _('No')),
|
||
('sometimes', _('Sometimes')),
|
||
]
|
||
|
||
consult = models.ForeignKey(
|
||
OTConsult,
|
||
on_delete=models.CASCADE,
|
||
related_name='current_behaviors',
|
||
verbose_name=_("Consultation")
|
||
)
|
||
behavior = models.CharField(
|
||
max_length=50,
|
||
choices=BEHAVIOR_CHOICES,
|
||
verbose_name=_("Behavior")
|
||
)
|
||
response = models.CharField(
|
||
max_length=20,
|
||
choices=RESPONSE_CHOICES,
|
||
blank=True,
|
||
verbose_name=_("Response")
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = _("OT Current Behavior")
|
||
verbose_name_plural = _("OT Current Behaviors")
|
||
ordering = ['consult', 'id']
|
||
unique_together = [['consult', 'behavior']]
|
||
|
||
def __str__(self):
|
||
return f"{self.get_behavior_display()}: {self.response}"
|
||
|
||
|
||
# ============================================================================
|
||
# OT Session Models (OT-F-3)
|
||
# ============================================================================
|
||
|
||
class OTSession(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin, ClinicallySignableMixin):
|
||
"""
|
||
OT session notes form (OT-F-3).
|
||
Progress tracking and session documentation.
|
||
"""
|
||
|
||
class SessionType(models.TextChoices):
|
||
CONSULT = 'CONSULT', _('Consultation')
|
||
INDIVIDUAL = 'INDIVIDUAL', _('Individual Session')
|
||
GROUP = 'GROUP', _('Group Session')
|
||
PARENT_TRAINING = 'PARENT_TRAINING', _('Parent Training')
|
||
|
||
# Core Relationships
|
||
patient = models.ForeignKey(
|
||
'core.Patient',
|
||
on_delete=models.CASCADE,
|
||
related_name='ot_sessions',
|
||
verbose_name=_("Patient")
|
||
)
|
||
appointment = models.ForeignKey(
|
||
'appointments.Appointment',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='ot_sessions',
|
||
verbose_name=_("Appointment")
|
||
)
|
||
|
||
# NEW: Link to centralized session (for scheduling/capacity management)
|
||
session = models.ForeignKey(
|
||
'appointments.Session',
|
||
on_delete=models.CASCADE,
|
||
null=True,
|
||
blank=True,
|
||
related_name='ot_notes',
|
||
verbose_name=_("Session"),
|
||
help_text=_("Link to centralized session for scheduling")
|
||
)
|
||
|
||
# NEW: Link to specific participant (for group sessions)
|
||
session_participant = models.ForeignKey(
|
||
'appointments.SessionParticipant',
|
||
on_delete=models.CASCADE,
|
||
null=True,
|
||
blank=True,
|
||
related_name='ot_notes',
|
||
verbose_name=_("Session Participant"),
|
||
help_text=_("For group sessions: which participant these notes are for")
|
||
)
|
||
|
||
session_date = models.DateField(
|
||
verbose_name=_("Session Date")
|
||
)
|
||
provider = models.ForeignKey(
|
||
'core.User',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
related_name='ot_sessions_provided',
|
||
verbose_name=_("Provider")
|
||
)
|
||
|
||
# Session Details
|
||
session_type = models.CharField(
|
||
max_length=20,
|
||
choices=SessionType.choices,
|
||
verbose_name=_("Session Type")
|
||
)
|
||
cooperative_level = models.PositiveSmallIntegerField(
|
||
null=True,
|
||
blank=True,
|
||
help_text=_("Cooperative level (1-4 scale)"),
|
||
verbose_name=_("Cooperative Level (1-4)")
|
||
)
|
||
distraction_tolerance = models.PositiveSmallIntegerField(
|
||
null=True,
|
||
blank=True,
|
||
help_text=_("Distraction tolerance (1-4 scale)"),
|
||
verbose_name=_("Distraction Tolerance (1-4)")
|
||
)
|
||
|
||
# Activities Checklist (TextField: "Today we work on...")
|
||
activities_checklist = models.TextField(
|
||
blank=True,
|
||
help_text=_("List of activities worked on during session"),
|
||
verbose_name=_("Activities Checklist")
|
||
)
|
||
|
||
# Session Notes
|
||
observations = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Observations")
|
||
)
|
||
activities_performed = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Activities Performed")
|
||
)
|
||
recommendations = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Recommendations")
|
||
)
|
||
|
||
history = HistoricalRecords()
|
||
|
||
class Meta:
|
||
verbose_name = _("OT Session")
|
||
verbose_name_plural = _("OT Sessions")
|
||
ordering = ['-session_date', '-created_at']
|
||
indexes = [
|
||
models.Index(fields=['patient', 'session_date']),
|
||
models.Index(fields=['provider', 'session_date']),
|
||
models.Index(fields=['tenant', 'session_date']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"OT Session - {self.patient} - {self.session_date}"
|
||
|
||
@property
|
||
def cooperative_level_display(self):
|
||
"""Return cooperative level with description."""
|
||
levels = {
|
||
1: _("Poor"),
|
||
2: _("Fair"),
|
||
3: _("Good"),
|
||
4: _("Excellent")
|
||
}
|
||
return levels.get(self.cooperative_level, "")
|
||
|
||
@property
|
||
def distraction_tolerance_display(self):
|
||
"""Return distraction tolerance with description."""
|
||
levels = {
|
||
1: _("Low"),
|
||
2: _("Moderate"),
|
||
3: _("Good"),
|
||
4: _("High")
|
||
}
|
||
return levels.get(self.distraction_tolerance, "")
|
||
|
||
|
||
class OTTargetSkill(UUIDPrimaryKeyMixin):
|
||
"""
|
||
Target skills with 0-10 scoring for OT sessions.
|
||
Tracks progress on specific therapeutic goals.
|
||
"""
|
||
|
||
session = models.ForeignKey(
|
||
OTSession,
|
||
on_delete=models.CASCADE,
|
||
related_name='target_skills',
|
||
verbose_name=_("Session")
|
||
)
|
||
skill_name = models.CharField(
|
||
max_length=200,
|
||
verbose_name=_("Skill Name")
|
||
)
|
||
score = models.PositiveSmallIntegerField(
|
||
help_text=_("Score from 0 (not achieved) to 10 (fully achieved)"),
|
||
verbose_name=_("Score (0-10)")
|
||
)
|
||
notes = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Notes")
|
||
)
|
||
order = models.PositiveIntegerField(
|
||
default=0,
|
||
help_text=_("Order of skills in the list"),
|
||
verbose_name=_("Order")
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = _("OT Target Skill")
|
||
verbose_name_plural = _("OT Target Skills")
|
||
ordering = ['session', 'order']
|
||
|
||
def __str__(self):
|
||
return f"{self.skill_name} - Score: {self.score}/10"
|
||
|
||
@property
|
||
def score_percentage(self):
|
||
"""Return score as percentage."""
|
||
return (self.score / 10) * 100
|
||
|
||
@property
|
||
def achievement_level(self):
|
||
"""Return achievement level description."""
|
||
if self.score == 0:
|
||
return _("Not Achieved")
|
||
elif self.score <= 3:
|
||
return _("Emerging")
|
||
elif self.score <= 6:
|
||
return _("Developing")
|
||
elif self.score <= 9:
|
||
return _("Proficient")
|
||
else:
|
||
return _("Mastered")
|
||
|
||
|
||
class OTProgressReport(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin, ClinicallySignableMixin):
|
||
"""
|
||
OT progress report summarizing treatment outcomes.
|
||
"""
|
||
|
||
patient = models.ForeignKey(
|
||
'core.Patient',
|
||
on_delete=models.CASCADE,
|
||
related_name='ot_progress_reports',
|
||
verbose_name=_("Patient")
|
||
)
|
||
report_date = models.DateField(
|
||
verbose_name=_("Report Date")
|
||
)
|
||
provider = models.ForeignKey(
|
||
'core.User',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
related_name='ot_progress_reports_provided',
|
||
verbose_name=_("Provider")
|
||
)
|
||
|
||
# Session Summary
|
||
sessions_scheduled = models.PositiveIntegerField(
|
||
default=0,
|
||
verbose_name=_("Sessions Scheduled")
|
||
)
|
||
sessions_attended = models.PositiveIntegerField(
|
||
default=0,
|
||
verbose_name=_("Sessions Attended")
|
||
)
|
||
|
||
# Progress Summary
|
||
goals_progress = models.TextField(
|
||
blank=True,
|
||
help_text=_("Progress on each goal"),
|
||
verbose_name=_("Goals Progress")
|
||
)
|
||
overall_progress = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Overall Progress")
|
||
)
|
||
|
||
# Recommendations
|
||
recommendations = models.TextField(
|
||
blank=True,
|
||
verbose_name=_("Recommendations")
|
||
)
|
||
continue_treatment = models.BooleanField(
|
||
default=True,
|
||
verbose_name=_("Continue Treatment")
|
||
)
|
||
|
||
history = HistoricalRecords()
|
||
|
||
class Meta:
|
||
verbose_name = _("OT Progress Report")
|
||
verbose_name_plural = _("OT Progress Reports")
|
||
ordering = ['-report_date', '-created_at']
|
||
indexes = [
|
||
models.Index(fields=['patient', 'report_date']),
|
||
models.Index(fields=['tenant', 'report_date']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"OT Progress Report - {self.patient} - {self.report_date}"
|
||
|
||
@property
|
||
def attendance_rate(self):
|
||
"""Calculate attendance rate percentage."""
|
||
if self.sessions_scheduled > 0:
|
||
return round((self.sessions_attended / self.sessions_scheduled) * 100, 2)
|
||
return 0
|
||
|
||
|
||
# ============================================================================
|
||
# Scoring Configuration Model
|
||
# ============================================================================
|
||
|
||
class OTScoringConfig(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin):
|
||
"""
|
||
Dynamic scoring configuration for OT consultations.
|
||
Allows customization of scoring thresholds and interpretations.
|
||
"""
|
||
|
||
name = models.CharField(
|
||
max_length=200,
|
||
verbose_name=_("Configuration Name")
|
||
)
|
||
is_active = models.BooleanField(
|
||
default=True,
|
||
verbose_name=_("Is Active")
|
||
)
|
||
|
||
# Maximum scores for each domain
|
||
self_help_max = models.PositiveIntegerField(
|
||
default=24,
|
||
verbose_name=_("Self-Help Max Score")
|
||
)
|
||
behavior_max = models.PositiveIntegerField(
|
||
default=48,
|
||
verbose_name=_("Behavior Max Score")
|
||
)
|
||
developmental_max = models.PositiveIntegerField(
|
||
default=6,
|
||
verbose_name=_("Developmental Max Score")
|
||
)
|
||
eating_max = models.PositiveIntegerField(
|
||
default=6,
|
||
verbose_name=_("Eating Max Score")
|
||
)
|
||
|
||
# Interpretation thresholds
|
||
immediate_attention_threshold = models.PositiveIntegerField(
|
||
default=30,
|
||
help_text=_("Scores at or below this require immediate attention"),
|
||
verbose_name=_("Immediate Attention Threshold")
|
||
)
|
||
moderate_difficulty_threshold = models.PositiveIntegerField(
|
||
default=60,
|
||
help_text=_("Scores at or below this indicate moderate difficulty"),
|
||
verbose_name=_("Moderate Difficulty Threshold")
|
||
)
|
||
|
||
# Interpretation labels
|
||
immediate_attention_label = models.CharField(
|
||
max_length=200,
|
||
default="⚠️ Needs Immediate Attention",
|
||
verbose_name=_("Immediate Attention Label")
|
||
)
|
||
moderate_difficulty_label = models.CharField(
|
||
max_length=200,
|
||
default="⚠ Moderate Difficulty - Follow-Up Needed",
|
||
verbose_name=_("Moderate Difficulty Label")
|
||
)
|
||
age_appropriate_label = models.CharField(
|
||
max_length=200,
|
||
default="✅ Age-Appropriate Skills",
|
||
verbose_name=_("Age-Appropriate Label")
|
||
)
|
||
|
||
# Recommendation templates
|
||
immediate_attention_recommendation = models.TextField(
|
||
default="The child presents significant delays or difficulties across multiple developmental domains. Immediate referral to Occupational Therapy and interdisciplinary evaluation is recommended.",
|
||
verbose_name=_("Immediate Attention Recommendation")
|
||
)
|
||
moderate_difficulty_recommendation = models.TextField(
|
||
default="The child shows moderate concerns that warrant intervention. Recommend starting OT sessions and monitoring progress within 2–4 months.",
|
||
verbose_name=_("Moderate Difficulty Recommendation")
|
||
)
|
||
age_appropriate_recommendation = models.TextField(
|
||
default="Child demonstrates age-appropriate functioning in assessed areas. Recommend regular developmental screening as part of preventive care.",
|
||
verbose_name=_("Age-Appropriate Recommendation")
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = _("OT Scoring Configuration")
|
||
verbose_name_plural = _("OT Scoring Configurations")
|
||
ordering = ['-is_active', '-created_at']
|
||
|
||
def __str__(self):
|
||
return f"{self.name} {'(Active)' if self.is_active else '(Inactive)'}"
|
||
|
||
@property
|
||
def total_max_score(self):
|
||
"""Calculate total maximum possible score."""
|
||
return self.self_help_max + self.behavior_max + self.developmental_max + self.eating_max
|