234 lines
7.1 KiB
Python
234 lines
7.1 KiB
Python
"""
|
|
Projects models - Quality Improvement (QI) projects tracking
|
|
|
|
This module implements QI project management:
|
|
- Project tracking
|
|
- Task management
|
|
- PDCA cycle phases
|
|
- Milestone tracking
|
|
- Outcome measurement
|
|
"""
|
|
|
|
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from apps.core.models import StatusChoices, TimeStampedModel, UUIDModel
|
|
|
|
|
|
class PDCAPhaseChoices(models.TextChoices):
|
|
PLAN = "plan", _("Plan")
|
|
DO = "do", _("Do")
|
|
CHECK = "check", _("Check")
|
|
ACT = "act", _("Act")
|
|
|
|
|
|
class FOCUSPhaseChoices(models.TextChoices):
|
|
FIND = "find", _("Find")
|
|
ORGANIZE = "organize", _("Organize")
|
|
CLARIFY = "clarify", _("Clarify")
|
|
UNDERSTAND = "understand", _("Understand")
|
|
SELECT = "select", _("Select")
|
|
|
|
|
|
class QIProject(UUIDModel, TimeStampedModel):
|
|
"""
|
|
Quality Improvement Project.
|
|
|
|
Tracks improvement initiatives driven by PX feedback.
|
|
Can also serve as a template for creating new projects.
|
|
"""
|
|
|
|
name = models.CharField(max_length=200)
|
|
name_ar = models.CharField(max_length=200, blank=True)
|
|
description = models.TextField()
|
|
|
|
# Template flag - if True, this is a template not an active project
|
|
is_template = models.BooleanField(
|
|
default=False, db_index=True, help_text="If True, this is a reusable template, not an active project"
|
|
)
|
|
|
|
# Organization
|
|
hospital = models.ForeignKey(
|
|
"organizations.Hospital",
|
|
on_delete=models.CASCADE,
|
|
related_name="qi_projects",
|
|
null=True,
|
|
blank=True,
|
|
help_text="Null for global templates available to all hospitals",
|
|
)
|
|
department = models.ForeignKey(
|
|
"organizations.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="qi_projects"
|
|
)
|
|
|
|
# Project lead
|
|
project_lead = models.ForeignKey("accounts.User", on_delete=models.SET_NULL, null=True, related_name="led_projects")
|
|
|
|
# Creator
|
|
created_by = models.ForeignKey(
|
|
"accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="created_qi_projects"
|
|
)
|
|
|
|
# Team members
|
|
team_members = models.ManyToManyField("accounts.User", blank=True, related_name="qi_projects")
|
|
|
|
# Status
|
|
status = models.CharField(
|
|
max_length=20, choices=StatusChoices.choices, default=StatusChoices.PENDING, db_index=True
|
|
)
|
|
|
|
# Dates
|
|
start_date = models.DateField(null=True, blank=True)
|
|
target_completion_date = models.DateField(null=True, blank=True, db_index=True)
|
|
actual_completion_date = models.DateField(null=True, blank=True)
|
|
|
|
# Linked to PX Actions (optional)
|
|
related_actions = models.ManyToManyField("px_action_center.PXAction", blank=True, related_name="qi_projects")
|
|
|
|
# Outcomes
|
|
outcome_description = models.TextField(blank=True)
|
|
success_metrics = models.JSONField(default=dict, blank=True, help_text="Success metrics and results")
|
|
|
|
# FOCUS methodology
|
|
focus_enabled = models.BooleanField(
|
|
default=True, help_text="Whether to use FOCUS methodology alongside PDCA"
|
|
)
|
|
|
|
# Metadata
|
|
metadata = models.JSONField(default=dict, blank=True)
|
|
|
|
class Meta:
|
|
ordering = ["-created_at"]
|
|
indexes = [
|
|
models.Index(fields=["hospital", "status", "-created_at"]),
|
|
models.Index(fields=["is_template", "hospital"]),
|
|
models.Index(fields=["project_lead", "status"]),
|
|
models.Index(fields=["status", "target_completion_date"]),
|
|
]
|
|
|
|
def __str__(self):
|
|
if self.is_template:
|
|
return f"[Template] {self.name}"
|
|
return f"{self.name} ({self.status})"
|
|
|
|
|
|
class QIProjectTask(UUIDModel, TimeStampedModel):
|
|
"""
|
|
Task within a QI project.
|
|
"""
|
|
|
|
project = models.ForeignKey(QIProject, on_delete=models.CASCADE, related_name="tasks")
|
|
pdca_phase = models.ForeignKey(
|
|
"PDCAPhase",
|
|
on_delete=models.CASCADE,
|
|
null=True,
|
|
blank=True,
|
|
related_name="tasks",
|
|
)
|
|
focus_phase = models.ForeignKey(
|
|
"FOCUSPhase",
|
|
on_delete=models.CASCADE,
|
|
null=True,
|
|
blank=True,
|
|
related_name="tasks",
|
|
)
|
|
|
|
title = models.CharField(max_length=500)
|
|
description = models.TextField(blank=True)
|
|
|
|
# Assignment
|
|
assigned_to = models.ForeignKey(
|
|
"accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="qi_tasks"
|
|
)
|
|
|
|
# Status
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.PENDING)
|
|
|
|
# Dates
|
|
due_date = models.DateField(null=True, blank=True)
|
|
completed_date = models.DateField(null=True, blank=True)
|
|
|
|
# Order
|
|
order = models.IntegerField(default=0)
|
|
|
|
class Meta:
|
|
ordering = ["project", "order"]
|
|
|
|
def __str__(self):
|
|
return f"{self.project.name} - {self.title}"
|
|
|
|
|
|
class PDCAPhase(UUIDModel, TimeStampedModel):
|
|
"""
|
|
PDCA (Plan-Do-Check-Act) phase within a QI project.
|
|
Each project can have up to 4 phases, one per PDCA stage.
|
|
"""
|
|
|
|
project = models.ForeignKey(QIProject, on_delete=models.CASCADE, related_name="pdca_phases")
|
|
phase = models.CharField(max_length=10, choices=PDCAPhaseChoices.choices)
|
|
|
|
title = models.CharField(max_length=300, blank=True)
|
|
description = models.TextField(blank=True)
|
|
|
|
owner = models.ForeignKey(
|
|
"accounts.User",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="owned_pdca_phases",
|
|
)
|
|
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.PENDING)
|
|
|
|
start_date = models.DateField(null=True, blank=True)
|
|
due_date = models.DateField(null=True, blank=True)
|
|
completed_date = models.DateField(null=True, blank=True)
|
|
|
|
findings = models.TextField(blank=True)
|
|
|
|
order = models.IntegerField(default=0)
|
|
|
|
class Meta:
|
|
unique_together = [("project", "phase")]
|
|
ordering = ["project", "order"]
|
|
|
|
def __str__(self):
|
|
return f"{self.project.name} - {self.get_phase_display()}"
|
|
|
|
|
|
class FOCUSPhase(UUIDModel, TimeStampedModel):
|
|
"""
|
|
FOCUS (Find, Organize, Clarify, Understand, Select) phase within a QI project.
|
|
Each project can have up to 5 phases, one per FOCUS stage.
|
|
"""
|
|
|
|
project = models.ForeignKey(QIProject, on_delete=models.CASCADE, related_name="focus_phases")
|
|
phase = models.CharField(max_length=15, choices=FOCUSPhaseChoices.choices)
|
|
|
|
title = models.CharField(max_length=300, blank=True)
|
|
description = models.TextField(blank=True)
|
|
|
|
owner = models.ForeignKey(
|
|
"accounts.User",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="owned_focus_phases",
|
|
)
|
|
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.PENDING)
|
|
|
|
start_date = models.DateField(null=True, blank=True)
|
|
due_date = models.DateField(null=True, blank=True)
|
|
completed_date = models.DateField(null=True, blank=True)
|
|
|
|
findings = models.TextField(blank=True)
|
|
|
|
order = models.IntegerField(default=0)
|
|
|
|
class Meta:
|
|
unique_together = [("project", "phase")]
|
|
ordering = ["project", "order"]
|
|
|
|
def __str__(self):
|
|
return f"{self.project.name} - {self.get_phase_display()}"
|