410 lines
14 KiB
Python
410 lines
14 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",
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
object_id = models.UUIDField(db_index=True, null=True, blank=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="rca_approved_items"
|
|
)
|
|
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=["department", "status", "-created_at"]),
|
|
models.Index(fields=["assigned_to", "status", "-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"""
|
|
if self.content_type:
|
|
return self.content_type.model
|
|
return None
|
|
|
|
def get_related_item_display(self):
|
|
"""Get display name of related item"""
|
|
try:
|
|
return str(self.related_item)
|
|
except Exception:
|
|
return f"Item {self.object_id}"
|
|
|
|
def get_related_item_url(self):
|
|
"""Get URL for the related item"""
|
|
if not self.content_type or not self.object_id:
|
|
return None
|
|
try:
|
|
obj = self.related_item
|
|
if obj and hasattr(obj, "get_absolute_url"):
|
|
return obj.get_absolute_url()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
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}"
|