637 lines
28 KiB
Python
637 lines
28 KiB
Python
from django.db import models
|
|
from django.conf import settings
|
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
|
from decimal import Decimal
|
|
|
|
|
|
class QualityIndicator(models.Model):
|
|
"""Quality metrics and performance indicators for monitoring and improvement"""
|
|
|
|
CATEGORY_CHOICES = [
|
|
('clinical', 'Clinical'),
|
|
('safety', 'Safety'),
|
|
('operational', 'Operational'),
|
|
('financial', 'Financial'),
|
|
('patient_satisfaction', 'Patient Satisfaction'),
|
|
('staff_satisfaction', 'Staff Satisfaction'),
|
|
('compliance', 'Compliance'),
|
|
('efficiency', 'Efficiency'),
|
|
('quality', 'Quality'),
|
|
('outcome', 'Outcome'),
|
|
]
|
|
|
|
TYPE_CHOICES = [
|
|
('structure', 'Structure'),
|
|
('process', 'Process'),
|
|
('outcome', 'Outcome'),
|
|
('balancing', 'Balancing'),
|
|
]
|
|
|
|
FREQUENCY_CHOICES = [
|
|
('daily', 'Daily'),
|
|
('weekly', 'Weekly'),
|
|
('monthly', 'Monthly'),
|
|
('quarterly', 'Quarterly'),
|
|
('annually', 'Annually'),
|
|
]
|
|
|
|
tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='quality_indicators')
|
|
name = models.CharField(max_length=200)
|
|
description = models.TextField()
|
|
category = models.CharField(max_length=50, choices=CATEGORY_CHOICES)
|
|
type = models.CharField(max_length=20, choices=TYPE_CHOICES)
|
|
measurement_unit = models.CharField(max_length=50)
|
|
target_value = models.DecimalField(max_digits=10, decimal_places=2)
|
|
current_value = models.DecimalField(max_digits=10, decimal_places=2)
|
|
threshold_warning = models.DecimalField(max_digits=10, decimal_places=2)
|
|
threshold_critical = models.DecimalField(max_digits=10, decimal_places=2)
|
|
calculation_method = models.TextField()
|
|
data_source = models.CharField(max_length=200)
|
|
frequency = models.CharField(max_length=20, choices=FREQUENCY_CHOICES)
|
|
responsible_department = models.ForeignKey('hr.Department', on_delete=models.SET_NULL, null=True, blank=True)
|
|
responsible_user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='responsible_quality_indicators')
|
|
is_active = models.BooleanField(default=True)
|
|
regulatory_requirement = models.BooleanField(default=False)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'quality_indicator'
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'category', 'type']),
|
|
models.Index(fields=['is_active', 'regulatory_requirement']),
|
|
models.Index(fields=['responsible_department', 'responsible_user']),
|
|
]
|
|
ordering = ['name']
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.category})"
|
|
|
|
@property
|
|
def latest_measurement(self):
|
|
"""Get the most recent measurement for this indicator"""
|
|
return self.measurements.order_by('-measurement_date').first()
|
|
|
|
@property
|
|
def current_status(self):
|
|
"""Get current status based on latest measurement"""
|
|
latest = self.latest_measurement
|
|
if not latest:
|
|
return 'no_data'
|
|
return latest.status
|
|
|
|
|
|
class QualityMeasurement(models.Model):
|
|
"""Individual measurements and values for quality indicators"""
|
|
|
|
STATUS_CHOICES = [
|
|
('within_target', 'Within Target'),
|
|
('warning', 'Warning'),
|
|
('critical', 'Critical'),
|
|
('improving', 'Improving'),
|
|
('declining', 'Declining'),
|
|
]
|
|
|
|
tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='quality_measurements')
|
|
indicator = models.ForeignKey(QualityIndicator, on_delete=models.CASCADE, related_name='measurements')
|
|
measurement_date = models.DateField()
|
|
value = models.DecimalField(max_digits=10, decimal_places=2)
|
|
numerator = models.IntegerField(null=True, blank=True)
|
|
denominator = models.IntegerField(null=True, blank=True)
|
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
|
|
measurement_method = models.TextField(blank=True)
|
|
sample_size = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
|
data_source = models.CharField(max_length=200, null=True, blank=True)
|
|
notes = models.TextField(blank=True)
|
|
data_source_reference = models.CharField(max_length=200, blank=True)
|
|
verified_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='verified_measurements')
|
|
verified_at = models.DateTimeField(null=True, blank=True)
|
|
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='created_measurements')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'quality_measurement'
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'indicator', 'measurement_date']),
|
|
models.Index(fields=['status', 'verified_by']),
|
|
models.Index(fields=['created_by', 'created_at']),
|
|
]
|
|
ordering = ['-measurement_date']
|
|
unique_together = ['indicator', 'measurement_date']
|
|
|
|
def __str__(self):
|
|
return f"{self.indicator.name} - {self.measurement_date}: {self.value}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""Auto-determine status based on thresholds"""
|
|
if not self.status:
|
|
if self.value >= self.indicator.threshold_critical:
|
|
self.status = 'critical'
|
|
elif self.value >= self.indicator.threshold_warning:
|
|
self.status = 'warning'
|
|
else:
|
|
self.status = 'within_target'
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class IncidentReport(models.Model):
|
|
"""Patient safety incidents, adverse events, and near-miss reporting"""
|
|
|
|
INCIDENT_TYPE_CHOICES = [
|
|
('medication_error', 'Medication Error'),
|
|
('fall', 'Fall'),
|
|
('infection', 'Infection'),
|
|
('equipment_failure', 'Equipment Failure'),
|
|
('documentation_error', 'Documentation Error'),
|
|
('communication_failure', 'Communication Failure'),
|
|
('surgical_complication', 'Surgical Complication'),
|
|
('diagnostic_error', 'Diagnostic Error'),
|
|
('treatment_delay', 'Treatment Delay'),
|
|
('other', 'Other'),
|
|
]
|
|
|
|
SEVERITY_CHOICES = [
|
|
('no_harm', 'No Harm'),
|
|
('minor_harm', 'Minor Harm'),
|
|
('moderate_harm', 'Moderate Harm'),
|
|
('severe_harm', 'Severe Harm'),
|
|
('death', 'Death'),
|
|
]
|
|
|
|
CATEGORY_CHOICES = [
|
|
('patient_safety', 'Patient Safety'),
|
|
('medication', 'Medication'),
|
|
('infection_control', 'Infection Control'),
|
|
('equipment', 'Equipment'),
|
|
('documentation', 'Documentation'),
|
|
('communication', 'Communication'),
|
|
('surgical', 'Surgical'),
|
|
('diagnostic', 'Diagnostic'),
|
|
('treatment', 'Treatment'),
|
|
('environmental', 'Environmental'),
|
|
]
|
|
|
|
STATUS_CHOICES = [
|
|
('reported', 'Reported'),
|
|
('under_investigation', 'Under Investigation'),
|
|
('investigated', 'Investigated'),
|
|
('closed', 'Closed'),
|
|
('cancelled', 'Cancelled'),
|
|
]
|
|
|
|
PRIORITY_CHOICES = [
|
|
('low', 'Low'),
|
|
('medium', 'Medium'),
|
|
('high', 'High'),
|
|
('urgent', 'Urgent'),
|
|
]
|
|
|
|
tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='incident_reports')
|
|
incident_number = models.CharField(max_length=50, unique=True)
|
|
title = models.CharField(max_length=200)
|
|
description = models.TextField()
|
|
incident_type = models.CharField(max_length=30, choices=INCIDENT_TYPE_CHOICES)
|
|
severity = models.CharField(max_length=20, choices=SEVERITY_CHOICES)
|
|
category = models.CharField(max_length=30, choices=CATEGORY_CHOICES)
|
|
location = models.CharField(max_length=200)
|
|
incident_date = models.DateField()
|
|
incident_time = models.TimeField()
|
|
discovered_date = models.DateTimeField()
|
|
patient = models.ForeignKey('patients.PatientProfile', on_delete=models.SET_NULL, null=True, blank=True, related_name='incident_reports')
|
|
witness_information = models.TextField(null=True, blank=True)
|
|
reported_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='reported_incidents')
|
|
status = models.CharField(max_length=30, choices=STATUS_CHOICES, default='reported')
|
|
priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default='medium')
|
|
root_cause = models.TextField(blank=True)
|
|
contributing_factors = models.TextField(blank=True)
|
|
corrective_actions = models.TextField(blank=True)
|
|
preventive_actions = models.TextField(blank=True)
|
|
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_incidents')
|
|
due_date = models.DateField(null=True, blank=True)
|
|
closed_date = models.DateTimeField(null=True, blank=True)
|
|
is_confidential = models.BooleanField(default=False)
|
|
regulatory_notification = models.BooleanField(default=False)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'incident_report'
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'incident_type', 'severity']),
|
|
models.Index(fields=['status', 'priority', 'assigned_to']),
|
|
models.Index(fields=['incident_date', 'patient']),
|
|
]
|
|
ordering = ['-incident_date']
|
|
|
|
def __str__(self):
|
|
return f"{self.incident_number}: {self.title}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.incident_number:
|
|
# Generate incident number
|
|
from django.utils import timezone
|
|
year = timezone.now().year
|
|
count = IncidentReport.objects.filter(
|
|
tenant=self.tenant,
|
|
created_at__year=year
|
|
).count() + 1
|
|
self.incident_number = f"INC-{year}-{count:04d}"
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class RiskAssessment(models.Model):
|
|
"""Risk assessments and risk management activities"""
|
|
|
|
class RiskCategory(models.TextChoices):
|
|
CLINICAL = 'CLINICAL', 'Clinical'
|
|
OPERATIONAL = 'OPERATIONAL', 'Operational'
|
|
FINANCIAL = 'FINANCIAL', 'Financial'
|
|
REGULATORY = 'REGULATORY', 'Regulatory'
|
|
REPUTATIONAL = 'REPUTATIONAL', 'Reputational'
|
|
STRATEGIC = 'STRATEGIC', 'Strategic'
|
|
TECHNOLOGY = 'TECHNOLOGY', 'Technology'
|
|
ENVIRONMENTAL = 'ENVIRONMENTAL', 'Environmental'
|
|
SECURITY = 'SECURITY', 'Security'
|
|
LEGAL = 'LEGAL', 'Legal'
|
|
|
|
class RiskType(models.TextChoices):
|
|
PATIENT_SAFETY = 'PATIENT_SAFETY', 'Patient Safety'
|
|
QUALITY = 'QUALITY', 'Quality'
|
|
COMPLIANCE = 'COMPLIANCE', 'Compliance'
|
|
FINANCIAL = 'FINANCIAL', 'Financial'
|
|
OPERATIONAL = 'OPERATIONAL', 'Operational'
|
|
STRATEGIC = 'STRATEGIC', 'Strategic'
|
|
TECHNOLOGY = 'TECHNOLOGY', 'Technology'
|
|
ENVIRONMENTAL = 'ENVIRONMENTAL', 'Environmental'
|
|
SECURITY = 'SECURITY', 'Security'
|
|
LEGAL = 'LEGAL', 'Legal'
|
|
|
|
class Likelihood(models.IntegerChoices):
|
|
VERY_LOW = 1, 'Very Low'
|
|
LOW = 2, 'Low'
|
|
MEDIUM = 3, 'Medium'
|
|
HIGH = 4, 'High'
|
|
VERY_HIGH = 5, 'Very High'
|
|
|
|
class Impact(models.IntegerChoices):
|
|
VERY_LOW = 1, 'Very Low'
|
|
LOW = 2, 'Low'
|
|
MEDIUM = 3, 'Medium'
|
|
HIGH = 4, 'High'
|
|
VERY_HIGH = 5, 'Very High'
|
|
|
|
class RiskLevel(models.TextChoices):
|
|
LOW = 'LOW', 'Low'
|
|
MEDIUM = 'MEDIUM', 'Medium'
|
|
HIGH = 'HIGH', 'High'
|
|
CRITICAL = 'CRITICAL', 'Critical'
|
|
|
|
class ControlEffectiveness(models.TextChoices):
|
|
POOR = 'POOR', 'Poor'
|
|
FAIR = 'FAIR', 'Fair'
|
|
GOOD = 'GOOD', 'Good'
|
|
EXCELLENT = 'EXCELLENT', 'Excellent'
|
|
|
|
class RiskStatus(models.TextChoices):
|
|
DRAFT = 'DRAFT', 'Draft'
|
|
ACTIVE = 'ACTIVE', 'Active'
|
|
UNDER_REVIEW = 'UNDER_REVIEW', 'Under Review'
|
|
CLOSED = 'CLOSED', 'Closed'
|
|
CANCELLED = 'CANCELLED', 'Cancelled'
|
|
|
|
tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='risk_assessments')
|
|
title = models.CharField(max_length=200)
|
|
description = models.TextField()
|
|
risk_category = models.CharField(max_length=20, choices=RiskCategory.choices)
|
|
risk_type = models.CharField(max_length=20, choices=RiskType.choices)
|
|
likelihood = models.IntegerField(choices=Likelihood.choices)
|
|
impact = models.IntegerField(choices=Impact.choices)
|
|
risk_score = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(25)])
|
|
risk_level = models.CharField(max_length=20, choices=RiskLevel.choices)
|
|
current_controls = models.TextField()
|
|
control_effectiveness = models.CharField(max_length=20, choices=ControlEffectiveness.choices)
|
|
mitigation_plan = models.TextField()
|
|
target_completion_date = models.DateTimeField()
|
|
responsible_person = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='responsible_risks')
|
|
review_date = models.DateField()
|
|
review_notes = models.TextField(null=True, blank=True)
|
|
status = models.CharField(max_length=20, choices=RiskStatus.choices, default=RiskStatus.DRAFT)
|
|
incident_report = models.ForeignKey(IncidentReport, on_delete=models.SET_NULL, null=True, blank=True, related_name='risk_assessments')
|
|
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='created_risk_assessments')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'risk_assessment'
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'risk_category', 'risk_level']),
|
|
models.Index(fields=['status', 'responsible_person']),
|
|
models.Index(fields=['review_date', 'created_by']),
|
|
]
|
|
ordering = ['-risk_score', '-created_at']
|
|
|
|
def __str__(self):
|
|
return f"{self.title} ({self.risk_level})"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""Auto-calculate risk scores and levels"""
|
|
# Risk score calculation (1-25 scale)
|
|
likelihood_values = {'very_low': 1, 'low': 2, 'medium': 3, 'high': 4, 'very_high': 5}
|
|
impact_values = {'very_low': 1, 'low': 2, 'medium': 3, 'high': 4, 'very_high': 5}
|
|
|
|
self.risk_score = likelihood_values[self.likelihood] * impact_values[self.impact]
|
|
self.residual_risk_score = likelihood_values[self.residual_likelihood] * impact_values[self.residual_impact]
|
|
|
|
# Risk level determination
|
|
if self.risk_score >= 20:
|
|
self.risk_level = 'critical'
|
|
elif self.risk_score >= 12:
|
|
self.risk_level = 'high'
|
|
elif self.risk_score >= 6:
|
|
self.risk_level = 'medium'
|
|
else:
|
|
self.risk_level = 'low'
|
|
|
|
if self.residual_risk_score >= 20:
|
|
self.residual_risk_level = 'critical'
|
|
elif self.residual_risk_score >= 12:
|
|
self.residual_risk_level = 'high'
|
|
elif self.residual_risk_score >= 6:
|
|
self.residual_risk_level = 'medium'
|
|
else:
|
|
self.residual_risk_level = 'low'
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class AuditPlan(models.Model):
|
|
"""Quality audits and compliance monitoring plans"""
|
|
|
|
class AuditType(models.TextChoices):
|
|
INTERNAL = 'INTERNAL', 'Internal'
|
|
EXTERNAL = 'EXTERNAL', 'External'
|
|
REGULATORY = 'REGULATORY', 'Regulatory'
|
|
ACCREDITATION = 'ACCREDITATION', 'Accreditation'
|
|
QUALITY = 'QUALITY', 'Quality'
|
|
COMPLIANCE = 'COMPLIANCE', 'Compliance'
|
|
SAFETY = 'SAFETY', 'Safety'
|
|
OPERATIONAL = 'OPERATIONAL', 'Operational'
|
|
FINANCIAL = 'FINANCIAL', 'Financial'
|
|
CLINICAL = 'CLINICAL', 'Clinical'
|
|
|
|
class AuditStatus(models.TextChoices):
|
|
PLANNED = 'PLANNED', 'Planned'
|
|
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
|
|
COMPLETED = 'COMPLETED', 'Completed'
|
|
CANCELLED = 'CANCELLED', 'Cancelled'
|
|
POSTPONED = 'POSTPONED', 'Postponed'
|
|
|
|
class AuditPriority(models.TextChoices):
|
|
LOW = 'LOW', 'Low'
|
|
MEDIUM = 'MEDIUM', 'Medium'
|
|
HIGH = 'HIGH', 'High'
|
|
URGENT = 'URGENT', 'Urgent'
|
|
|
|
tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='audit_plans')
|
|
title = models.CharField(max_length=200)
|
|
description = models.TextField()
|
|
audit_type = models.CharField(max_length=20, choices=AuditType.choices)
|
|
scope = models.TextField()
|
|
criteria = models.TextField()
|
|
department = models.ForeignKey('hr.Department', on_delete=models.SET_NULL, null=True, blank=True, related_name='audit_plans')
|
|
auditor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='led_audits')
|
|
audit_team = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='audit_team_memberships', blank=True)
|
|
planned_start_date = models.DateField()
|
|
planned_end_date = models.DateField()
|
|
actual_start_date = models.DateField(null=True, blank=True)
|
|
actual_end_date = models.DateField(null=True, blank=True)
|
|
status = models.CharField(max_length=20, choices=AuditStatus.choices, default=AuditStatus.PLANNED)
|
|
priority = models.CharField(max_length=10, choices=AuditPriority.choices, default=AuditPriority.MEDIUM)
|
|
regulatory_requirement = models.BooleanField(default=False)
|
|
accreditation_body = models.CharField(max_length=200, blank=True)
|
|
objectives = models.TextField(blank=True)
|
|
notes = models.TextField(blank=True)
|
|
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='created_audit_plans')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'audit_plan'
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'audit_type', 'status']),
|
|
models.Index(fields=['department', 'auditor']),
|
|
models.Index(fields=['planned_start_date', 'planned_end_date']),
|
|
]
|
|
ordering = ['planned_start_date']
|
|
|
|
def __str__(self):
|
|
return f"{self.title} ({self.audit_type})"
|
|
|
|
@property
|
|
def findings_count(self):
|
|
"""Get count of findings for this audit"""
|
|
return self.findings.count()
|
|
|
|
@property
|
|
def open_findings_count(self):
|
|
"""Get count of open findings"""
|
|
return self.findings.exclude(status='closed').count()
|
|
|
|
|
|
class AuditFinding(models.Model):
|
|
"""Audit findings, non-conformities, and observations"""
|
|
|
|
class FindingType(models.TextChoices):
|
|
NON_CONFORMITY = 'NON_CONFORMITY', 'Non-Conformity'
|
|
OBSERVATION = 'OBSERVATION', 'Observation'
|
|
OPPORTUNITY_FOR_IMPROVEMENT = 'OPPORTUNITY_FOR_IMPROVEMENT', 'Opportunity for Improvement'
|
|
POSITIVE_FINDING = 'POSITIVE_FINDING', 'Positive Finding'
|
|
RECOMMENDATION = 'RECOMMENDATION', 'Recommendation'
|
|
|
|
class FindingSeverity(models.TextChoices):
|
|
MINOR = 'MINOR', 'Minor'
|
|
MAJOR = 'MAJOR', 'Major'
|
|
CRITICAL = 'CRITICAL', 'Critical'
|
|
|
|
class FindingCategory(models.TextChoices):
|
|
DOCUMENTATION = 'DOCUMENTATION', 'Documentation'
|
|
PROCESS = 'PROCESS', 'Process'
|
|
TRAINING = 'TRAINING', 'Training'
|
|
EQUIPMENT = 'EQUIPMENT', 'Equipment'
|
|
ENVIRONMENT = 'ENVIRONMENT', 'Environment'
|
|
MANAGEMENT = 'MANAGEMENT', 'Management'
|
|
COMMUNICATION = 'COMMUNICATION', 'Communication'
|
|
SAFETY = 'SAFETY', 'Safety'
|
|
QUALITY = 'QUALITY', 'Quality'
|
|
COMPLIANCE = 'COMPLIANCE', 'Compliance'
|
|
|
|
class FindingStatus(models.TextChoices):
|
|
OPEN = 'OPEN', 'Open'
|
|
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
|
|
COMPLETED = 'COMPLETED', 'Completed'
|
|
VERIFIED = 'VERIFIED', 'Verified'
|
|
CLOSED = 'CLOSED', 'Closed'
|
|
|
|
tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='audit_findings')
|
|
audit_plan = models.ForeignKey(AuditPlan, on_delete=models.CASCADE, related_name='findings')
|
|
finding_number = models.CharField(max_length=50)
|
|
title = models.CharField(max_length=200)
|
|
description = models.TextField()
|
|
finding_type = models.CharField(max_length=30, choices=FindingType.choices)
|
|
finding_date = models.DateField()
|
|
severity = models.CharField(max_length=20, choices=FindingSeverity.choices)
|
|
category = models.CharField(max_length=20, choices=FindingCategory.choices)
|
|
criteria_reference = models.CharField(max_length=200)
|
|
evidence = models.TextField()
|
|
root_cause = models.TextField(blank=True)
|
|
corrective_action_required = models.BooleanField(default=True)
|
|
corrective_actions = models.TextField(blank=True)
|
|
responsible_person = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='responsible_findings')
|
|
target_completion_date = models.DateField(null=True, blank=True)
|
|
actual_completion_date = models.DateField(null=True, blank=True)
|
|
status = models.CharField(max_length=20, choices=FindingStatus.choices, default=FindingStatus.OPEN)
|
|
verification_method = models.CharField(max_length=200, blank=True)
|
|
verified_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='verified_findings')
|
|
verified_date = models.DateField(null=True, blank=True)
|
|
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='created_findings')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'audit_finding'
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'audit_plan', 'finding_type']),
|
|
models.Index(fields=['severity', 'status', 'responsible_person']),
|
|
models.Index(fields=['target_completion_date', 'verified_by']),
|
|
]
|
|
ordering = ['-severity', 'target_completion_date']
|
|
unique_together = ['audit_plan', 'finding_number']
|
|
|
|
def __str__(self):
|
|
return f"{self.finding_number}: {self.title}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.finding_number:
|
|
# Generate finding number
|
|
count = AuditFinding.objects.filter(audit_plan=self.audit_plan).count() + 1
|
|
self.finding_number = f"{self.audit_plan.id}-F{count:03d}"
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class ImprovementProject(models.Model):
|
|
"""Quality improvement projects and initiatives"""
|
|
|
|
class ProjectType(models.TextChoices):
|
|
QUALITY_IMPROVEMENT = 'QUALITY_IMPROVEMENT', 'Quality Improvement'
|
|
PROCESS_IMPROVEMENT = 'PROCESS_IMPROVEMENT', 'Process Improvement'
|
|
SAFETY_INITIATIVE = 'SAFETY_INITIATIVE', 'Safety Initiative'
|
|
COMPLIANCE_PROJECT = 'COMPLIANCE_PROJECT', 'Compliance Project'
|
|
COST_REDUCTION = 'COST_REDUCTION', 'Cost Reduction'
|
|
EFFICIENCY_IMPROVEMENT = 'EFFICIENCY_IMPROVEMENT', 'Efficiency Improvement'
|
|
PATIENT_SATISFACTION = 'PATIENT_SATISFACTION', 'Patient Satisfaction'
|
|
STAFF_SATISFACTION = 'STAFF_SATISFACTION', 'Staff Satisfaction'
|
|
TECHNOLOGY_IMPLEMENTATION = 'TECHNOLOGY_IMPLEMENTATION', 'Technology Implementation'
|
|
TRAINING_PROGRAM = 'TRAINING_PROGRAM', 'Training Program'
|
|
|
|
class Methodology(models.TextChoices):
|
|
PDSA = 'PDSA', 'PDSA (Plan-Do-Study-Act)'
|
|
LEAN = 'LEAN', 'Lean'
|
|
SIX_SIGMA = 'SIX_SIGMA', 'Six Sigma'
|
|
KAIZEN = 'KAIZEN', 'Kaizen'
|
|
ROOT_CAUSE_ANALYSIS = 'ROOT_CAUSE_ANALYSIS', 'Root Cause Analysis'
|
|
FAILURE_MODE_ANALYSIS = 'FAILURE_MODE_ANALYSIS', 'Failure Mode Analysis'
|
|
OTHER = 'OTHER', 'Other'
|
|
|
|
class ProjectStatus(models.TextChoices):
|
|
PLANNED = 'PLANNED', 'Planned'
|
|
ACTIVE = 'ACTIVE', 'Active'
|
|
ON_HOLD = 'ON_HOLD', 'On Hold'
|
|
COMPLETED = 'COMPLETED', 'Completed'
|
|
CANCELLED = 'CANCELLED', 'Cancelled'
|
|
|
|
class ProjectPhase(models.TextChoices):
|
|
DEFINE = 'DEFINE', 'Define'
|
|
MEASURE = 'MEASURE', 'Measure'
|
|
ANALYZE = 'ANALYZE', 'Analyze'
|
|
IMPROVE = 'IMPROVE', 'Improve'
|
|
CONTROL = 'CONTROL', 'Control'
|
|
PLAN = 'PLAN', 'Plan'
|
|
DO = 'DO', 'Do'
|
|
STUDY = 'STUDY', 'Study'
|
|
ACT = 'ACT', 'Act'
|
|
|
|
tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='improvement_projects')
|
|
project_number = models.CharField(max_length=50, unique=True)
|
|
title = models.CharField(max_length=200)
|
|
description = models.TextField()
|
|
project_type = models.CharField(max_length=30, choices=ProjectType.choices)
|
|
methodology = models.CharField(max_length=30, choices=Methodology.choices)
|
|
problem_statement = models.TextField()
|
|
goal_statement = models.TextField()
|
|
success_metrics = models.TextField()
|
|
baseline_data = models.TextField(blank=True)
|
|
target_metrics = models.TextField()
|
|
scope = models.TextField()
|
|
project_manager = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='managed_projects')
|
|
project_team = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='project_team_memberships', blank=True)
|
|
sponsor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='sponsored_projects')
|
|
# stakeholders = models.ManyToManyField(User, related_name='stakeholder_memberships', blank=True)
|
|
department = models.ForeignKey('hr.Department', on_delete=models.SET_NULL, null=True, blank=True, related_name='improvement_projects')
|
|
planned_start_date = models.DateField()
|
|
planned_end_date = models.DateField()
|
|
actual_start_date = models.DateField(null=True, blank=True)
|
|
actual_end_date = models.DateField(null=True, blank=True)
|
|
status = models.CharField(max_length=20, choices=ProjectStatus.choices, default=ProjectStatus.PLANNED)
|
|
phase = models.CharField(max_length=20, choices=ProjectPhase.choices, default=ProjectPhase.DEFINE)
|
|
estimated_cost = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
resources_required = models.TextField(blank=True)
|
|
actual_cost = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
roi_expected = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
|
roi_actual = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
|
lessons_learned = models.TextField(blank=True)
|
|
notes = models.TextField(blank=True)
|
|
sustainability_plan = models.TextField(blank=True)
|
|
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='created_improvement_projects')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'improvement_project'
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'project_type', 'status']),
|
|
models.Index(fields=['project_manager', 'sponsor', 'department']),
|
|
models.Index(fields=['planned_start_date', 'planned_end_date']),
|
|
]
|
|
ordering = ['planned_start_date']
|
|
|
|
def __str__(self):
|
|
return f"{self.project_number}: {self.title}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.project_number:
|
|
# Generate project number
|
|
from django.utils import timezone
|
|
year = timezone.now().year
|
|
count = ImprovementProject.objects.filter(
|
|
tenant=self.tenant,
|
|
created_at__year=year
|
|
).count() + 1
|
|
self.project_number = f"QI-{year}-{count:04d}"
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def duration_planned(self):
|
|
"""Get planned project duration in days"""
|
|
if self.planned_start_date and self.planned_end_date:
|
|
return (self.planned_end_date - self.planned_start_date).days
|
|
return None
|
|
|
|
@property
|
|
def duration_actual(self):
|
|
"""Get actual project duration in days"""
|
|
if self.actual_start_date and self.actual_end_date:
|
|
return (self.actual_end_date - self.actual_start_date).days
|
|
return None
|
|
|