All checks were successful
Build and Push Docker Image / build (push) Successful in 4m14s
Reference numbers (unified scheme PREFIX-YYYYMM-HOSP-NNNN, e.g. CMP-202606-HHN-0001): - new ReferenceSequence model + generate_reference() helper (apps/core) - Complaint/Inquiry/Observation/Appreciation/Suggestion emit unified refs via save() - prefix-based auto-routing in public track API (CMP/INQ/OBS trackable; APR/SGT internal-only) - removed legacy CMP-/INQ- generators in ui_views, integrations, px_sources - migrations: core.0003_referencesequence, appreciation.0006, feedback.0008, observations.0012 - unit tests (format, sanitization, monthly reset, 40-thread concurrency) QA audit: - isolated E2E hospital sandbox mirroring HH-N + 10 role users (create_e2e_isolated_env) - feedback-modules-audit.spec.ts + audit helper (headed, run-to-completion) - reports/feedback-modules-qa-report.md Also bundles accumulated in-progress work across complaints, observations, organizations, templates, and other modules.
847 lines
30 KiB
Python
847 lines
30 KiB
Python
"""
|
|
Organizations models - Hospital, Department, Physician, Employee, Patient
|
|
"""
|
|
|
|
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from apps.core.encryption import EncryptedCharField, compute_national_id_hash, mask_national_id
|
|
from apps.core.models import TimeStampedModel, UUIDModel, StatusChoices
|
|
|
|
|
|
class Organization(UUIDModel, TimeStampedModel):
|
|
"""Top-level healthcare organization/company"""
|
|
|
|
name = models.CharField(max_length=200)
|
|
name_ar = models.CharField(max_length=200, blank=True, verbose_name="Name (Arabic)")
|
|
code = models.CharField(max_length=50, unique=True)
|
|
|
|
# Contact information
|
|
phone = models.CharField(max_length=20, blank=True)
|
|
email = models.EmailField(blank=True)
|
|
address = models.TextField(blank=True)
|
|
city = models.CharField(max_length=100, blank=True)
|
|
|
|
# Language preference for communications
|
|
preferred_language = models.CharField(
|
|
max_length=10,
|
|
choices=[("en", _("English")), ("ar", _("Arabic"))],
|
|
default="en",
|
|
help_text="Preferred language for surveys and notifications",
|
|
)
|
|
|
|
# Status
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True)
|
|
|
|
class Meta:
|
|
ordering = ["name"]
|
|
verbose_name = "Organization"
|
|
verbose_name_plural = "Organizations"
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def get_localized_name(self):
|
|
from django.utils.translation import get_language
|
|
|
|
if get_language() == "ar" and self.name_ar:
|
|
return self.name_ar
|
|
return self.name
|
|
|
|
|
|
class Hospital(UUIDModel, TimeStampedModel):
|
|
"""Hospital/Facility model"""
|
|
|
|
organization = models.ForeignKey(
|
|
Organization,
|
|
on_delete=models.CASCADE,
|
|
null=True,
|
|
blank=True,
|
|
related_name="hospitals",
|
|
help_text="Parent organization (null for backward compatibility)",
|
|
)
|
|
name = models.CharField(max_length=200)
|
|
name_ar = models.CharField(max_length=200, blank=True, verbose_name="Name (Arabic)")
|
|
display_name = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
help_text="Display name (English). Falls back to 'name' if empty.",
|
|
)
|
|
display_name_ar = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
verbose_name="Display Name (Arabic)",
|
|
help_text="Display name (Arabic). Falls back to 'name_ar' if empty.",
|
|
)
|
|
code = models.CharField(max_length=50, unique=True)
|
|
|
|
# Contact information
|
|
address = models.TextField(blank=True)
|
|
city = models.CharField(max_length=100, blank=True)
|
|
phone = models.CharField(max_length=20, blank=True)
|
|
email = models.EmailField(blank=True)
|
|
|
|
# Status
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True)
|
|
|
|
# Executive leadership
|
|
ceo = models.ForeignKey(
|
|
"accounts.User",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="hospitals_as_ceo",
|
|
verbose_name="CEO",
|
|
help_text="Chief Executive Officer",
|
|
)
|
|
medical_director = models.ForeignKey(
|
|
"accounts.User",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="hospitals_as_medical_director",
|
|
verbose_name="Medical Director",
|
|
help_text="Medical Director",
|
|
)
|
|
coo = models.ForeignKey(
|
|
"accounts.User",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="hospitals_as_coo",
|
|
verbose_name="COO",
|
|
help_text="Chief Operating Officer",
|
|
)
|
|
cfo = models.ForeignKey(
|
|
"accounts.User",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="hospitals_as_cfo",
|
|
verbose_name="CFO",
|
|
help_text="Chief Financial Officer",
|
|
)
|
|
|
|
# Metadata
|
|
license_number = models.CharField(max_length=100, blank=True)
|
|
capacity = models.IntegerField(null=True, blank=True, help_text="Bed capacity")
|
|
metadata = models.JSONField(default=dict, blank=True, help_text="Hospital configuration settings")
|
|
|
|
class Meta:
|
|
ordering = ["name"]
|
|
verbose_name_plural = "Hospitals"
|
|
|
|
def __str__(self):
|
|
return self.get_display_name()
|
|
|
|
def get_localized_name(self):
|
|
from django.utils.translation import get_language
|
|
|
|
if get_language() == "ar":
|
|
return self.get_display_name_ar()
|
|
return self.get_display_name()
|
|
|
|
def get_display_name(self):
|
|
return self.display_name or self.name
|
|
|
|
def get_display_name_ar(self):
|
|
return self.display_name_ar or self.name_ar or self.get_display_name()
|
|
|
|
|
|
class DepartmentCategory(models.TextChoices):
|
|
MEDICAL = "medical", _("Medical")
|
|
NON_MEDICAL = "non_medical", _("Non-Medical")
|
|
NURSING = "nursing", _("Nursing")
|
|
SUPPORT_SERVICES = "support_services", _("Support Services")
|
|
ADMINISTRATIVE = "administrative", _("Administrative")
|
|
|
|
|
|
class LocationType(models.TextChoices):
|
|
OP = "OP", _("Outpatient")
|
|
IP = "IP", _("Inpatient")
|
|
ER = "ER", _("Emergency")
|
|
GENERAL = "GENERAL", _("General")
|
|
|
|
|
|
class Area(UUIDModel, TimeStampedModel):
|
|
hospital = models.ForeignKey(Hospital, on_delete=models.CASCADE, related_name="areas")
|
|
name_en = models.CharField(max_length=200)
|
|
name_ar = models.CharField(max_length=200, blank=True)
|
|
code = models.CharField(max_length=100, blank=True)
|
|
location_type = models.CharField(
|
|
max_length=20, choices=LocationType.choices, blank=True, help_text="Location type (OP/IP/ER/GO)"
|
|
)
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default="active")
|
|
|
|
class Meta:
|
|
unique_together = [["hospital", "code"]]
|
|
|
|
def __str__(self):
|
|
return f"{self.name_en} ({self.hospital.code})"
|
|
|
|
|
|
class Department(UUIDModel, TimeStampedModel):
|
|
"""Department within a hospital, matching the 4th Version Excel."""
|
|
|
|
hospital = models.ForeignKey(Hospital, on_delete=models.CASCADE, related_name="departments")
|
|
|
|
main_section = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
help_text="Main section name (often same as department name)",
|
|
)
|
|
|
|
name = models.CharField(max_length=200)
|
|
name_en = models.CharField(max_length=200, blank=True)
|
|
name_ar = models.CharField(max_length=200, blank=True, verbose_name="Name (Arabic)")
|
|
hr_name = models.CharField(max_length=200, blank=True, verbose_name="HR System Name", help_text="Department name from HR system (employees file). If empty, name_en matches HR exactly.")
|
|
code = models.CharField(max_length=100, db_index=True)
|
|
|
|
category = models.CharField(
|
|
max_length=30,
|
|
choices=DepartmentCategory.choices,
|
|
blank=True,
|
|
default="",
|
|
db_index=True,
|
|
)
|
|
|
|
parent = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, blank=True, related_name="sub_departments")
|
|
|
|
location_type = models.CharField(
|
|
max_length=20,
|
|
choices=LocationType.choices,
|
|
blank=True,
|
|
help_text="Location type (OP, IP, ER, GENERAL)",
|
|
)
|
|
sub_location = models.CharField(max_length=200, blank=True, help_text="Detailed location (e.g., OPD5/GATE1)")
|
|
floor = models.CharField(max_length=50, blank=True, help_text="Floor (e.g., GF, 1st Floor, Basement)")
|
|
area = models.ForeignKey(
|
|
Area, on_delete=models.SET_NULL, null=True, blank=True, related_name="departments"
|
|
)
|
|
|
|
manager = models.ForeignKey(
|
|
"accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="managed_departments"
|
|
)
|
|
|
|
manager_3rd = models.ForeignKey(
|
|
"Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="dept_manager_3rd"
|
|
)
|
|
manager_2nd = models.ForeignKey(
|
|
"Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="dept_manager_2nd"
|
|
)
|
|
deputy_manager = models.ForeignKey(
|
|
"Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="dept_deputy_manager"
|
|
)
|
|
supervisor = models.ForeignKey(
|
|
"Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="dept_supervisor"
|
|
)
|
|
deputy_supervisor = models.ForeignKey(
|
|
"Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="dept_deputy_supervisor"
|
|
)
|
|
|
|
champion = models.ForeignKey(
|
|
"Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="champion_departments"
|
|
)
|
|
champion_email = models.EmailField(max_length=200, blank=True, help_text="Fallback email for champion")
|
|
|
|
old_name_en = models.CharField(max_length=200, blank=True, help_text="Legacy English name from Excel col W")
|
|
old_name_ar = models.CharField(max_length=200, blank=True, help_text="Legacy Arabic name from Excel col X")
|
|
|
|
phone = models.CharField(max_length=20, blank=True)
|
|
email = models.EmailField(blank=True)
|
|
location = models.CharField(max_length=200, blank=True, help_text="Building/Floor/Room")
|
|
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True)
|
|
|
|
class Meta:
|
|
ordering = ["hospital", "category", "name_en"]
|
|
unique_together = [["hospital", "code"]]
|
|
|
|
def __str__(self):
|
|
return self.name_en or self.name
|
|
|
|
ROLE_FIELDS = [
|
|
("champion", "Champion"),
|
|
("manager_2nd", "2nd Manager"),
|
|
("manager_3rd", "3rd Manager"),
|
|
("deputy_manager", "Deputy Manager"),
|
|
("supervisor", "Supervisor"),
|
|
("deputy_supervisor", "Deputy Supervisor"),
|
|
]
|
|
|
|
def get_role_holders(self):
|
|
holders = []
|
|
for field_name, role_label in self.ROLE_FIELDS:
|
|
staff = getattr(self, field_name, None)
|
|
if staff:
|
|
holders.append({
|
|
"staff": staff,
|
|
"staff_id": str(staff.id),
|
|
"name": staff.get_full_name(),
|
|
"email": staff.email or (staff.user.email if staff.user else None),
|
|
"role_field": field_name,
|
|
"role_label": role_label,
|
|
})
|
|
return holders
|
|
|
|
def is_valid_contact_person(self, staff_id):
|
|
for holder in self.get_role_holders():
|
|
if str(holder["staff_id"]) == str(staff_id):
|
|
return holder
|
|
return None
|
|
|
|
def get_localized_name(self):
|
|
from django.utils.translation import get_language
|
|
|
|
if get_language() == "ar" and self.name_ar:
|
|
return self.name_ar
|
|
return self.name_en or self.name
|
|
|
|
|
|
class Staff(UUIDModel, TimeStampedModel):
|
|
class StaffType(models.TextChoices):
|
|
PHYSICIAN = "physician", _("Physician")
|
|
NURSE = "nurse", _("Nurse")
|
|
ADMIN = "admin", _("Administrative")
|
|
OTHER = "other", _("Other")
|
|
|
|
# Link to User (Keep it optional for external/temp staff)
|
|
user = models.OneToOneField(
|
|
"accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="staff_profile"
|
|
)
|
|
|
|
# Unified Identity (AI will search these 4 fields)
|
|
first_name = models.CharField(max_length=100)
|
|
last_name = models.CharField(max_length=100)
|
|
first_name_ar = models.CharField(max_length=100, blank=True)
|
|
last_name_ar = models.CharField(max_length=100, blank=True)
|
|
|
|
# Role Logic
|
|
staff_type = models.CharField(max_length=20, choices=StaffType.choices)
|
|
department_type = models.CharField(
|
|
max_length=30,
|
|
choices=DepartmentCategory.choices,
|
|
blank=True,
|
|
default="",
|
|
db_index=True,
|
|
)
|
|
job_title = models.CharField(max_length=200) # "Cardiologist", "Senior Nurse", etc.
|
|
|
|
# Professional Data (Nullable for non-physicians)
|
|
license_number = models.CharField(max_length=100, unique=True, null=True, blank=True)
|
|
specialization = models.CharField(max_length=200, blank=True)
|
|
email = models.EmailField(blank=True)
|
|
phone = models.CharField(max_length=20, blank=True, verbose_name="Phone Number")
|
|
employee_id = models.CharField(max_length=50, unique=True)
|
|
|
|
# Original name from CSV (preserves exact format)
|
|
name = models.CharField(max_length=300, blank=True, verbose_name="Full Name (Original)")
|
|
name_ar = models.CharField(max_length=300, blank=True, verbose_name="Full Name (Arabic)")
|
|
|
|
# Organization
|
|
hospital = models.ForeignKey(Hospital, on_delete=models.CASCADE, related_name="staff")
|
|
department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True, blank=True, related_name="staff")
|
|
|
|
# Additional fields from CSV import
|
|
civil_id = models.CharField(max_length=50, blank=True, db_index=True, verbose_name="Civil Identity Number")
|
|
country = models.CharField(max_length=100, blank=True, verbose_name="Country")
|
|
country_ar = models.CharField(max_length=100, blank=True, verbose_name="Country (Arabic)")
|
|
location = models.CharField(max_length=200, blank=True, verbose_name="Location")
|
|
location_ar = models.CharField(max_length=200, blank=True, verbose_name="Location (Arabic)")
|
|
gender = models.CharField(
|
|
max_length=10, choices=[("male", _("Male")), ("female", _("Female")), ("other", _("Other"))], blank=True
|
|
)
|
|
department_name = models.CharField(max_length=200, blank=True, verbose_name="Department (Original)")
|
|
department_name_ar = models.CharField(max_length=200, blank=True, verbose_name="Department (Arabic)")
|
|
|
|
# Section and Subsection (CharFields for storing original CSV values)
|
|
section = models.CharField(max_length=200, blank=True, verbose_name="Section")
|
|
section_ar = models.CharField(max_length=200, blank=True, verbose_name="Section (Arabic)")
|
|
subsection = models.CharField(max_length=200, blank=True, verbose_name="Subsection")
|
|
subsection_ar = models.CharField(max_length=200, blank=True, verbose_name="Subsection (Arabic)")
|
|
|
|
# ForeignKeys to Section and Subsection models
|
|
section_fk = models.ForeignKey(
|
|
"StaffSection",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="staff_members",
|
|
verbose_name="Section (FK)",
|
|
)
|
|
subsection_fk = models.ForeignKey(
|
|
"StaffSubsection",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="staff_members",
|
|
verbose_name="Subsection (FK)",
|
|
)
|
|
|
|
job_title_ar = models.CharField(max_length=200, blank=True, verbose_name="Job Title (Arabic)")
|
|
|
|
# Self-referential manager field for hierarchy
|
|
report_to = models.ForeignKey(
|
|
"self",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="direct_reports",
|
|
verbose_name="Reports To",
|
|
)
|
|
|
|
# Head of department/section/subsection indicator
|
|
is_head = models.BooleanField(default=False, verbose_name="Is Head")
|
|
|
|
# Physician indicator - set to True when staff comes from physician rating import
|
|
physician = models.BooleanField(
|
|
default=False,
|
|
verbose_name="Is Physician",
|
|
help_text="Set to True when staff record comes from physician rating import",
|
|
)
|
|
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE)
|
|
|
|
class Meta:
|
|
indexes = [
|
|
models.Index(fields=["hospital", "department", "status"]),
|
|
models.Index(fields=["hospital", "status"]),
|
|
models.Index(fields=["department", "status"]),
|
|
models.Index(fields=["status"]),
|
|
]
|
|
|
|
def __str__(self):
|
|
from django.utils.translation import get_language
|
|
|
|
if get_language() == "ar" and self.name_ar:
|
|
return self.name_ar
|
|
if self.name:
|
|
return self.name
|
|
return self.get_localized_name()
|
|
|
|
def get_localized_name(self):
|
|
from django.utils.translation import get_language
|
|
|
|
if get_language() == "ar" and self.first_name_ar and self.last_name_ar:
|
|
return f"{self.first_name_ar} {self.last_name_ar}"
|
|
return f"{self.first_name} {self.last_name}"
|
|
|
|
def get_localized_job_title(self):
|
|
from django.utils.translation import get_language
|
|
|
|
if get_language() == "ar" and self.job_title_ar:
|
|
return self.job_title_ar
|
|
return self.job_title
|
|
|
|
def get_full_name(self):
|
|
"""Get full name including Arabic if available"""
|
|
if self.first_name_ar and self.last_name_ar:
|
|
return f"{self.first_name} {self.last_name} ({self.first_name_ar} {self.last_name_ar})"
|
|
return f"{self.first_name} {self.last_name}"
|
|
|
|
def get_org_info(self):
|
|
"""Get organization and department information"""
|
|
parts = [self.hospital.name]
|
|
if self.department:
|
|
parts.append(self.department.name)
|
|
if self.department_name:
|
|
parts.append(self.department_name)
|
|
return " - ".join(parts)
|
|
|
|
|
|
# TODO Add Section
|
|
# class Physician(UUIDModel, TimeStampedModel):
|
|
# """Physician/Doctor model"""
|
|
# # Link to user account (optional - some physicians may not have system access)
|
|
# user = models.OneToOneField(
|
|
# 'accounts.User',
|
|
# on_delete=models.SET_NULL,
|
|
# null=True,
|
|
# blank=True,
|
|
# related_name='physician_profile'
|
|
# )
|
|
|
|
# # Basic information
|
|
# first_name = models.CharField(max_length=100)
|
|
# last_name = models.CharField(max_length=100)
|
|
# first_name_ar = models.CharField(max_length=100, blank=True)
|
|
# last_name_ar = models.CharField(max_length=100, blank=True)
|
|
|
|
# # Professional information
|
|
# license_number = models.CharField(max_length=100, unique=True, db_index=True)
|
|
# specialization = models.CharField(max_length=200)
|
|
|
|
# # Organization
|
|
# hospital = models.ForeignKey(Hospital, on_delete=models.CASCADE, related_name='physicians')
|
|
# department = models.ForeignKey(
|
|
# Department,
|
|
# on_delete=models.SET_NULL,
|
|
# null=True,
|
|
# blank=True,
|
|
# related_name='physicians'
|
|
# )
|
|
|
|
# # Contact
|
|
# phone = models.CharField(max_length=20, blank=True)
|
|
# email = models.EmailField(blank=True)
|
|
|
|
# # Status
|
|
# status = models.CharField(
|
|
# max_length=20,
|
|
# choices=StatusChoices.choices,
|
|
# default=StatusChoices.ACTIVE,
|
|
# db_index=True
|
|
# )
|
|
|
|
# class Meta:
|
|
# ordering = ['last_name', 'first_name']
|
|
|
|
# def __str__(self):
|
|
# return f"Dr. {self.first_name} {self.last_name}"
|
|
|
|
# def get_full_name(self):
|
|
# return f"{self.first_name} {self.last_name}"
|
|
|
|
|
|
# class Employee(UUIDModel, TimeStampedModel):
|
|
# """Employee model (non-physician staff)"""
|
|
# user = models.OneToOneField(
|
|
# 'accounts.User',
|
|
# on_delete=models.CASCADE,
|
|
# related_name='employee_profile'
|
|
# )
|
|
|
|
# # Organization
|
|
# hospital = models.ForeignKey(Hospital, on_delete=models.CASCADE, related_name='employees')
|
|
# department = models.ForeignKey(
|
|
# Department,
|
|
# on_delete=models.SET_NULL,
|
|
# null=True,
|
|
# blank=True,
|
|
# related_name='employees'
|
|
# )
|
|
|
|
# # Job information
|
|
# employee_id = models.CharField(max_length=50, unique=True, db_index=True)
|
|
# job_title = models.CharField(max_length=200)
|
|
# hire_date = models.DateField(null=True, blank=True)
|
|
|
|
# # Status
|
|
# status = models.CharField(
|
|
# max_length=20,
|
|
# choices=StatusChoices.choices,
|
|
# default=StatusChoices.ACTIVE,
|
|
# db_index=True
|
|
# )
|
|
|
|
# class Meta:
|
|
# ordering = ['user__last_name', 'user__first_name']
|
|
|
|
# def __str__(self):
|
|
# return f"{self.user.get_full_name()} - {self.job_title}"
|
|
|
|
|
|
class Patient(UUIDModel, TimeStampedModel):
|
|
"""Patient model"""
|
|
|
|
# Basic information
|
|
mrn = models.CharField(max_length=50, unique=True, verbose_name="Medical Record Number")
|
|
national_id = EncryptedCharField(max_length=256, blank=True, default="")
|
|
national_id_hash = models.CharField(max_length=64, blank=True, db_index=True, default="")
|
|
|
|
first_name = models.CharField(max_length=100)
|
|
last_name = models.CharField(max_length=100)
|
|
first_name_ar = models.CharField(max_length=100, blank=True)
|
|
last_name_ar = models.CharField(max_length=100, blank=True)
|
|
|
|
# Demographics
|
|
date_of_birth = models.DateField(null=True, blank=True)
|
|
gender = models.CharField(
|
|
max_length=10, choices=[("male", _("Male")), ("female", _("Female")), ("other", _("Other"))], blank=True
|
|
)
|
|
nationality = models.CharField(max_length=100, blank=True, db_index=True)
|
|
|
|
# Contact
|
|
phone = models.CharField(max_length=20, blank=True)
|
|
email = models.EmailField(blank=True)
|
|
address = models.TextField(blank=True)
|
|
city = models.CharField(max_length=100, blank=True)
|
|
|
|
# Primary hospital
|
|
primary_hospital = models.ForeignKey(
|
|
Hospital, on_delete=models.SET_NULL, null=True, blank=True, related_name="patients"
|
|
)
|
|
|
|
# Status
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True)
|
|
|
|
class Meta:
|
|
ordering = ["last_name", "first_name"]
|
|
|
|
def __str__(self):
|
|
return f"{self.first_name} {self.last_name} (MRN: {self.mrn})"
|
|
|
|
def get_full_name(self):
|
|
return f"{self.first_name} {self.last_name}"
|
|
|
|
def get_masked_national_id(self):
|
|
return mask_national_id(self.national_id)
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.national_id:
|
|
self.national_id_hash = compute_national_id_hash(self.national_id)
|
|
else:
|
|
self.national_id = self.national_id or ""
|
|
self.national_id_hash = ""
|
|
super().save(*args, **kwargs)
|
|
|
|
@staticmethod
|
|
def generate_mrn():
|
|
"""
|
|
Generate a unique Medical Record Number (MRN).
|
|
|
|
Returns:
|
|
str: A unique MRN in the format: PTN-YYYYMMDD-XXXXXX
|
|
where XXXXXX is a random 6-digit number
|
|
"""
|
|
import random
|
|
from datetime import datetime
|
|
|
|
# Generate MRN with date prefix for better traceability
|
|
date_prefix = datetime.now().strftime("%Y%m%d")
|
|
random_suffix = random.randint(100000, 999999)
|
|
mrn = f"PTN-{date_prefix}-{random_suffix}"
|
|
|
|
# Ensure uniqueness (in case of collision)
|
|
while Patient.objects.filter(mrn=mrn).exists():
|
|
random_suffix = random.randint(100000, 999999)
|
|
mrn = f"PTN-{date_prefix}-{random_suffix}"
|
|
|
|
return mrn
|
|
|
|
|
|
class StaffSection(UUIDModel, TimeStampedModel):
|
|
"""Section within a department (for staff organization)"""
|
|
|
|
department = models.ForeignKey(Department, on_delete=models.CASCADE, related_name="staff_sections")
|
|
|
|
name = models.CharField(max_length=200)
|
|
name_ar = models.CharField(max_length=200, blank=True, verbose_name="Name (Arabic)")
|
|
code = models.CharField(max_length=50, blank=True)
|
|
|
|
# Manager
|
|
head = models.ForeignKey("Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="headed_sections")
|
|
|
|
# Status
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True)
|
|
|
|
class Meta:
|
|
ordering = ["department", "name"]
|
|
unique_together = [["department", "name"]]
|
|
|
|
def __str__(self):
|
|
return f"{self.department.name} - {self.name}"
|
|
|
|
|
|
class StaffSubsection(UUIDModel, TimeStampedModel):
|
|
"""Subsection within a section (for staff organization)"""
|
|
|
|
section = models.ForeignKey(StaffSection, on_delete=models.CASCADE, related_name="subsections")
|
|
|
|
name = models.CharField(max_length=200)
|
|
name_ar = models.CharField(max_length=200, blank=True, verbose_name="Name (Arabic)")
|
|
code = models.CharField(max_length=50, blank=True)
|
|
|
|
# Manager
|
|
head = models.ForeignKey(
|
|
"Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="headed_subsections"
|
|
)
|
|
|
|
# Status
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True)
|
|
|
|
class Meta:
|
|
ordering = ["section", "name"]
|
|
unique_together = [["section", "name"]]
|
|
|
|
def __str__(self):
|
|
return f"{self.section.department.name} - {self.section.name} - {self.name}"
|
|
|
|
|
|
class LegacyLocation(models.Model):
|
|
id = models.IntegerField(primary_key=True)
|
|
name_ar = models.CharField(max_length=100)
|
|
name_en = models.CharField(max_length=100)
|
|
|
|
ACTIVE_IDS = [48, 49, 82, 110]
|
|
|
|
class Meta:
|
|
db_table = "organizations_location"
|
|
|
|
@classmethod
|
|
def active_locations(cls):
|
|
return cls.objects.filter(id__in=cls.ACTIVE_IDS).order_by("name_en")
|
|
|
|
def __str__(self):
|
|
return self.name_en if self.name_en else self.name_ar
|
|
|
|
|
|
class LegacyMainSection(models.Model):
|
|
id = models.IntegerField(primary_key=True)
|
|
name_ar = models.CharField(max_length=100)
|
|
name_en = models.CharField(max_length=100)
|
|
|
|
class Meta:
|
|
db_table = "organizations_mainsection"
|
|
|
|
def __str__(self):
|
|
return self.name_en if self.name_en else self.name_ar
|
|
|
|
|
|
class LegacySubSection(models.Model):
|
|
internal_id = models.IntegerField(primary_key=True)
|
|
name_ar = models.CharField(max_length=255)
|
|
name_en = models.CharField(max_length=255)
|
|
location = models.ForeignKey(LegacyLocation, on_delete=models.CASCADE, related_name="subsections")
|
|
main_section = models.ForeignKey(LegacyMainSection, on_delete=models.CASCADE, related_name="subsections")
|
|
|
|
class Meta:
|
|
db_table = "organizations_subsection"
|
|
|
|
def __str__(self):
|
|
name = self.name_en if self.name_en else self.name_ar
|
|
location_name = self.location.name_en if self.location.name_en else self.location.name_ar
|
|
return f"{name} - {location_name}"
|
|
|
|
|
|
class Section(UUIDModel, TimeStampedModel):
|
|
"""Section within a Department (Excel Col E, e.g., Central Outpatient Pharmacy)."""
|
|
|
|
department = models.ForeignKey(Department, on_delete=models.CASCADE, related_name="sections")
|
|
|
|
name_en = models.CharField(max_length=200)
|
|
name_ar = models.CharField(max_length=200, blank=True)
|
|
code = models.CharField(max_length=100, blank=True)
|
|
|
|
location_type = models.CharField(max_length=20, choices=LocationType.choices, blank=True)
|
|
sub_location = models.CharField(max_length=200, blank=True)
|
|
floor = models.CharField(max_length=50, blank=True)
|
|
|
|
champion = models.ForeignKey(
|
|
"Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="champion_sections"
|
|
)
|
|
|
|
supervisor = models.ForeignKey(
|
|
"Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="supervised_sections"
|
|
)
|
|
deputy_supervisor = models.ForeignKey(
|
|
"Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="deputy_supervised_sections"
|
|
)
|
|
|
|
display_name_en = models.CharField(max_length=255, blank=True, help_text="Patient-facing display name (English)")
|
|
display_name_ar = models.CharField(max_length=255, blank=True, help_text="Patient-facing display name (Arabic)")
|
|
|
|
old_name_en = models.CharField(max_length=200, blank=True)
|
|
old_name_ar = models.CharField(max_length=200, blank=True)
|
|
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True)
|
|
|
|
class Meta:
|
|
db_table = "organizations_section"
|
|
unique_together = [("department", "code")]
|
|
ordering = ["department", "name_en"]
|
|
verbose_name = "Section"
|
|
verbose_name_plural = "Sections"
|
|
|
|
def __str__(self):
|
|
return f"{self.department.name_en} / {self.name_en}"
|
|
|
|
def get_localized_display_name(self):
|
|
from django.utils.translation import get_language
|
|
if get_language() == "ar" and self.display_name_ar:
|
|
return self.display_name_ar
|
|
if self.display_name_en:
|
|
return self.display_name_en
|
|
if get_language() == "ar" and self.name_ar:
|
|
return self.name_ar
|
|
return self.name_en
|
|
|
|
def get_localized_name(self):
|
|
from django.utils.translation import get_language
|
|
if get_language() == "ar" and self.name_ar:
|
|
return self.name_ar
|
|
return self.name_en
|
|
|
|
|
|
class SubSection(UUIDModel, TimeStampedModel):
|
|
"""SubSection within a Section (Department -> Section -> SubSection)."""
|
|
|
|
section = models.ForeignKey(Section, on_delete=models.CASCADE, related_name="subsections")
|
|
|
|
name_en = models.CharField(max_length=200)
|
|
name_ar = models.CharField(max_length=200, blank=True)
|
|
code = models.CharField(max_length=100, blank=True)
|
|
|
|
champion = models.ForeignKey(
|
|
"Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="champion_subsections"
|
|
)
|
|
|
|
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True)
|
|
|
|
class Meta:
|
|
db_table = "organizations_subsection_new"
|
|
unique_together = [("section", "code")]
|
|
ordering = ["section", "name_en"]
|
|
verbose_name = "SubSection"
|
|
verbose_name_plural = "SubSections"
|
|
|
|
def __str__(self):
|
|
return f"{self.section.department.name_en} / {self.section.name_en} / {self.name_en}"
|
|
|
|
def get_localized_name(self):
|
|
from django.utils.translation import get_language
|
|
if get_language() == "ar" and self.name_ar:
|
|
return self.name_ar
|
|
return self.name_en
|
|
|
|
|
|
# Backward compatibility alias
|
|
OrgSubSection = Section
|
|
|
|
# Deprecation alias for OrgSubSubSection (removed)
|
|
OrgSubSubSection = None
|
|
|
|
|
|
class LegacyHierarchyMapping(models.Model):
|
|
"""Maps old complaint location hierarchy (Arabic) to current Department/Section."""
|
|
|
|
old_location_ar = models.CharField(max_length=200, db_index=True)
|
|
old_main_section_ar = models.CharField(max_length=200, db_index=True)
|
|
old_subsection_ar = models.CharField(max_length=200, db_index=True)
|
|
|
|
old_location_en = models.CharField(max_length=200, blank=True)
|
|
old_main_section_en = models.CharField(max_length=200, blank=True)
|
|
old_subsection_en = models.CharField(max_length=200, blank=True)
|
|
|
|
main_section = models.ForeignKey(
|
|
Department, on_delete=models.SET_NULL, null=True, blank=True, related_name="legacy_mappings"
|
|
)
|
|
subsection = models.ForeignKey(
|
|
Section, on_delete=models.SET_NULL, null=True, blank=True, related_name="legacy_mappings"
|
|
)
|
|
|
|
class Meta:
|
|
unique_together = [("old_location_ar", "old_main_section_ar", "old_subsection_ar")]
|
|
indexes = [
|
|
models.Index(fields=["old_location_ar", "old_main_section_ar", "old_subsection_ar"]),
|
|
]
|
|
|
|
def __str__(self):
|
|
target = self.subsection or self.main_section
|
|
return f"{self.old_location_ar} / {self.old_main_section_ar} / {self.old_subsection_ar} → {target}"
|
|
|
|
|
|
# Backward-compatible aliases (deprecated - use Legacy* names)
|
|
Location = LegacyLocation
|
|
MainSection = LegacyMainSection
|