HH/apps/rca/models.py

520 lines
15 KiB
Python

"""
RCA (Root Cause Analysis) models
This module implements the Root Cause Analysis system that:
- Tracks RCAs for complaints, inquiries, observations, and feedback
- Manages root causes and contributing factors
- Tracks corrective actions and their effectiveness
- Maintains audit trail with notes and status changes
- Supports attachments and documentation
"""
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils import timezone
from apps.core.models import PriorityChoices, TimeStampedModel, UUIDModel
class RCAStatus(models.TextChoices):
"""RCA status choices"""
DRAFT = 'draft', 'Draft'
IN_PROGRESS = 'in_progress', 'In Progress'
REVIEW = 'review', 'Under Review'
APPROVED = 'approved', 'Approved'
CLOSED = 'closed', 'Closed'
class RCASeverity(models.TextChoices):
"""RCA severity choices"""
LOW = 'low', 'Low'
MEDIUM = 'medium', 'Medium'
HIGH = 'high', 'High'
CRITICAL = 'critical', 'Critical'
class RCAActionType(models.TextChoices):
"""Corrective action type choices"""
PREVENTIVE = 'preventive', 'Preventive'
CORRECTIVE = 'corrective', 'Corrective'
IMMEDIATE = 'immediate', 'Immediate Action'
LONG_TERM = 'long_term', 'Long-term Solution'
class RCAActionStatus(models.TextChoices):
"""Corrective action status choices"""
NOT_STARTED = 'not_started', 'Not Started'
IN_PROGRESS = 'in_progress', 'In Progress'
COMPLETED = 'completed', 'Completed'
CANCELLED = 'cancelled', 'Cancelled'
class RootCauseCategory(models.TextChoices):
"""Root cause category choices"""
PROCESS = 'process', 'Process/Procedure'
PEOPLE = 'people', 'People/Training'
EQUIPMENT = 'equipment', 'Equipment/Resources'
COMMUNICATION = 'communication', 'Communication'
POLICY = 'policy', 'Policy/Regulation'
ENVIRONMENT = 'environment', 'Environment'
TECHNOLOGY = 'technology', 'Technology/Systems'
OTHER = 'other', 'Other'
class RootCauseAnalysis(UUIDModel, TimeStampedModel):
"""
Root Cause Analysis model.
Links to complaints, inquiries, observations, or feedback via GenericForeignKey.
Tracks the complete RCA process from creation to closure.
"""
# Related item (can be Complaint, Inquiry, Observation, or Feedback)
content_type = models.ForeignKey(
ContentType,
on_delete=models.PROTECT,
related_name='related_rcas',
help_text="Type of the related item"
)
object_id = models.UUIDField(
db_index=True,
help_text="ID of the related item"
)
related_item = GenericForeignKey('content_type', 'object_id')
# Organization
hospital = models.ForeignKey(
'organizations.Hospital',
on_delete=models.CASCADE,
related_name='rcas'
)
department = models.ForeignKey(
'organizations.Department',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='rcas'
)
# RCA details
title = models.CharField(max_length=500)
description = models.TextField(help_text="Description of the incident/issue")
background = models.TextField(
blank=True,
help_text="Background information and context"
)
# Status and severity
status = models.CharField(
max_length=20,
choices=RCAStatus.choices,
default=RCAStatus.DRAFT,
db_index=True
)
severity = models.CharField(
max_length=20,
choices=RCASeverity.choices,
default=RCASeverity.MEDIUM,
db_index=True
)
# Priority
priority = models.CharField(
max_length=20,
choices=PriorityChoices.choices,
default=PriorityChoices.MEDIUM,
db_index=True
)
# Root cause analysis summary
root_cause_summary = models.TextField(
blank=True,
help_text="Summary of root cause analysis findings"
)
# Assignment
assigned_to = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='assigned_rcas',
help_text="Person responsible for RCA"
)
assigned_at = models.DateTimeField(null=True, blank=True)
# Created by
created_by = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_rcas'
)
# Approval
approved_by = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='approved_rcas'
)
approved_at = models.DateTimeField(null=True, blank=True)
approval_notes = models.TextField(blank=True)
# Closure
closed_by = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='closed_rcas'
)
closed_at = models.DateTimeField(null=True, blank=True)
closure_notes = models.TextField(blank=True)
# Dates
target_completion_date = models.DateField(
null=True,
blank=True,
help_text="Target date for RCA completion"
)
actual_completion_date = models.DateField(null=True, blank=True)
# Metadata
metadata = models.JSONField(default=dict, blank=True)
# Soft delete
is_deleted = models.BooleanField(default=False, db_index=True)
deleted_at = models.DateTimeField(null=True, blank=True)
deleted_by = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='deleted_rcas'
)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', '-created_at']),
models.Index(fields=['hospital', 'status', '-created_at']),
models.Index(fields=['severity', '-created_at']),
models.Index(fields=['is_deleted', '-created_at']),
models.Index(fields=['content_type', 'object_id']),
]
verbose_name = 'Root Cause Analysis'
verbose_name_plural = 'Root Cause Analyses'
def __str__(self):
return f"{self.title} ({self.get_status_display()})"
def get_related_item_type(self):
"""Get the type of related item"""
return self.content_type.model
def get_related_item_display(self):
"""Get display name of related item"""
try:
return str(self.related_item)
except:
return f"Item {self.object_id}"
def get_root_causes_count(self):
"""Get count of root causes"""
return self.root_causes.count()
def get_corrective_actions_count(self):
"""Get count of corrective actions"""
return self.corrective_actions.count()
def get_completed_actions_count(self):
"""Get count of completed corrective actions"""
return self.corrective_actions.filter(
status=RCAActionStatus.COMPLETED
).count()
def soft_delete(self, user=None):
"""Soft delete RCA"""
self.is_deleted = True
self.deleted_at = timezone.now()
self.deleted_by = user
self.save(update_fields=['is_deleted', 'deleted_at', 'deleted_by'])
class RCARootCause(UUIDModel, TimeStampedModel):
"""
Individual root cause identified in an RCA.
Multiple root causes can be identified per RCA.
"""
rca = models.ForeignKey(
RootCauseAnalysis,
on_delete=models.CASCADE,
related_name='root_causes'
)
# Root cause details
description = models.TextField(help_text="Description of root cause")
category = models.CharField(
max_length=50,
choices=RootCauseCategory.choices,
db_index=True
)
# Contributing factors
contributing_factors = models.TextField(
blank=True,
help_text="Factors that contributed to this root cause"
)
# Impact assessment
likelihood = models.IntegerField(
null=True,
blank=True,
help_text="Likelihood score (1-5)"
)
impact = models.IntegerField(
null=True,
blank=True,
help_text="Impact score (1-5)"
)
risk_score = models.IntegerField(
null=True,
blank=True,
help_text="Risk score (likelihood * impact)"
)
# Evidence
evidence = models.TextField(
blank=True,
help_text="Evidence supporting this root cause"
)
# Verification
is_verified = models.BooleanField(default=False)
verified_by = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='verified_root_causes'
)
verified_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['rca', '-created_at']),
models.Index(fields=['category', '-created_at']),
]
verbose_name = 'Root Cause'
verbose_name_plural = 'Root Causes'
def __str__(self):
return f"{self.rca.title} - {self.description[:50]}"
def save(self, *args, **kwargs):
"""Calculate risk score before saving"""
if self.likelihood and self.impact:
self.risk_score = self.likelihood * self.impact
super().save(*args, **kwargs)
class RCACorrectiveAction(UUIDModel, TimeStampedModel):
"""
Corrective action to address identified root causes.
Each action is tracked for completion and effectiveness.
"""
rca = models.ForeignKey(
RootCauseAnalysis,
on_delete=models.CASCADE,
related_name='corrective_actions'
)
root_cause = models.ForeignKey(
RCARootCause,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='corrective_actions',
help_text="Root cause this action addresses"
)
# Action details
description = models.TextField(help_text="Description of corrective action")
action_type = models.CharField(
max_length=20,
choices=RCAActionType.choices,
default=RCAActionType.CORRECTIVE,
db_index=True
)
# Responsibility
responsible_person = models.ForeignKey(
'organizations.Staff',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='corrective_actions',
help_text="Person responsible for implementing the action"
)
# Dates
target_date = models.DateField(
null=True,
blank=True,
help_text="Target date for completion"
)
completion_date = models.DateField(
null=True,
blank=True,
help_text="Actual completion date"
)
# Status
status = models.CharField(
max_length=20,
choices=RCAActionStatus.choices,
default=RCAActionStatus.NOT_STARTED,
db_index=True
)
# Effectiveness
effectiveness_measure = models.TextField(
blank=True,
help_text="How effectiveness will be measured"
)
effectiveness_assessment = models.TextField(
blank=True,
help_text="Assessment of action effectiveness"
)
effectiveness_score = models.IntegerField(
null=True,
blank=True,
help_text="Effectiveness score (1-5)"
)
# Obstacles
obstacles = models.TextField(
blank=True,
help_text="Obstacles encountered during implementation"
)
# Metadata
metadata = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ['target_date', '-created_at']
indexes = [
models.Index(fields=['rca', '-created_at']),
models.Index(fields=['status', 'target_date']),
models.Index(fields=['action_type', '-created_at']),
]
verbose_name = 'Corrective Action'
verbose_name_plural = 'Corrective Actions'
def __str__(self):
return f"{self.description[:50]} - {self.get_status_display()}"
class RCAAttachment(UUIDModel, TimeStampedModel):
"""Attachments for RCA documentation"""
rca = models.ForeignKey(
RootCauseAnalysis,
on_delete=models.CASCADE,
related_name='attachments'
)
file = models.FileField(upload_to='rca/%Y/%m/%d/')
filename = models.CharField(max_length=500)
file_type = models.CharField(max_length=100, blank=True)
file_size = models.IntegerField(help_text="File size in bytes")
uploaded_by = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='rca_attachments'
)
description = models.TextField(blank=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'RCA Attachment'
verbose_name_plural = 'RCA Attachments'
def __str__(self):
return f"{self.rca.title} - {self.filename}"
class RCANote(UUIDModel, TimeStampedModel):
"""Internal notes for RCA"""
rca = models.ForeignKey(
RootCauseAnalysis,
on_delete=models.CASCADE,
related_name='notes'
)
note = models.TextField()
created_by = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='rca_notes'
)
is_internal = models.BooleanField(
default=True,
help_text="Internal note (not visible in reports)"
)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['rca', '-created_at']),
]
verbose_name = 'RCA Note'
verbose_name_plural = 'RCA Notes'
def __str__(self):
return f"{self.rca.title} - {self.created_at.strftime('%Y-%m-%d %H:%M')}"
class RCAStatusLog(UUIDModel, TimeStampedModel):
"""Audit trail for RCA status changes"""
rca = models.ForeignKey(
RootCauseAnalysis,
on_delete=models.CASCADE,
related_name='status_logs'
)
old_status = models.CharField(max_length=20, blank=True)
new_status = models.CharField(max_length=20, db_index=True)
changed_by = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='rca_status_changes'
)
notes = models.TextField(blank=True)
metadata = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['rca', '-created_at']),
models.Index(fields=['new_status', '-created_at']),
]
verbose_name = 'RCA Status Log'
verbose_name_plural = 'RCA Status Logs'
def __str__(self):
return f"{self.rca.title}: {self.old_status}{self.new_status}"