520 lines
15 KiB
Python
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}" |