HH/apps/rca/models.py
2026-04-08 17:13:35 +03:00

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}"