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