""" Patients app models for hospital management system. Provides patient demographics, profiles, and consent management functionality. """ import uuid from django.db import models from django.core.validators import RegexValidator from django.utils import timezone from django.conf import settings class PatientProfile(models.Model): """ Patient profile with comprehensive demographics and healthcare information. """ class Gender(models.TextChoices): MALE = 'MALE', 'Male' FEMALE = 'FEMALE', 'Female' OTHER = 'OTHER', 'Other' UNKNOWN = 'UNKNOWN', 'Unknown' PREFER_NOT_TO_SAY = 'PREFER_NOT_TO_SAY', 'Prefer not to say' class MaritalStatus(models.TextChoices): SINGLE = 'SINGLE', 'Single' MARRIED = 'MARRIED', 'Married' DIVORCED = 'DIVORCED', 'Divorced' WIDOWED = 'WIDOWED', 'Widowed' SEPARATED = 'SEPARATED', 'Separated' DOMESTIC_PARTNER = 'DOMESTIC_PARTNER', 'Domestic Partner' OTHER = 'OTHER', 'Other' UNKNOWN = 'UNKNOWN', 'Unknown' class CommunicationPreference(models.TextChoices): PHONE = 'PHONE', 'Phone' EMAIL = 'EMAIL', 'Email' SMS = 'SMS', 'SMS' MAIL = 'MAIL', 'Mail' PORTAL = 'PORTAL', 'Patient Portal' class AdvanceDirectiveType(models.TextChoices): LIVING_WILL = 'LIVING_WILL', 'Living Will' HEALTHCARE_PROXY = 'HEALTHCARE_PROXY', 'Healthcare Proxy' DNR = 'DNR', 'Do Not Resuscitate' POLST = 'POLST', 'POLST' OTHER = 'OTHER', 'Other' # Basic Identifiers patient_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique patient identifier' ) # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='patients', help_text='Organization tenant' ) # Medical Record Number mrn = models.CharField( max_length=50, unique=True, help_text='Medical Record Number' ) # Personal Information first_name = models.CharField( max_length=150, help_text='First name' ) last_name = models.CharField( max_length=150, help_text='Last name' ) middle_name = models.CharField( max_length=150, blank=True, null=True, help_text='Middle name' ) preferred_name = models.CharField( max_length=150, blank=True, null=True, help_text='Preferred name' ) suffix = models.CharField( max_length=20, blank=True, null=True, help_text='Name suffix (Jr., Sr., III, etc.)' ) # Demographics date_of_birth = models.DateField( help_text='Date of birth' ) gender = models.CharField( max_length=20, choices=Gender.choices, help_text='Gender' ) # Contact Information email = models.EmailField( blank=True, null=True, help_text='Email address' ) phone_number = models.CharField( max_length=20, blank=True, null=True, validators=[RegexValidator( regex=r'^\+?1?\d{9,15}$', message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.' )], help_text='Primary phone number' ) mobile_number = models.CharField( max_length=20, blank=True, null=True, validators=[RegexValidator( regex=r'^\+?1?\d{9,15}$', message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.' )], help_text='Mobile phone number' ) # Address Information address_line_1 = models.CharField( max_length=255, blank=True, null=True, help_text='Address line 1' ) address_line_2 = models.CharField( max_length=255, blank=True, null=True, help_text='Address line 2' ) city = models.CharField( max_length=100, blank=True, null=True, help_text='City' ) state = models.CharField( max_length=50, blank=True, null=True, help_text='State/Province' ) zip_code = models.CharField( max_length=20, blank=True, null=True, help_text='ZIP/Postal code' ) country = models.CharField( max_length=100, default='Saudi Arabia', help_text='Country' ) id_number = models.CharField( max_length=10, unique=True, blank=True, null=True, validators=[RegexValidator( regex=r'^\d{10}$', message='Saudi National ID must be exactly 10 digits' )], help_text='Saudi National ID (10 digits)' ) # Marital Status and Family marital_status = models.CharField( max_length=20, choices=MaritalStatus.choices, blank=True, null=True, help_text='Marital status' ) # Language and Communication primary_language = models.CharField( max_length=50, default='Arabic', help_text='Primary language' ) interpreter_needed = models.BooleanField( default=False, help_text='Interpreter services needed' ) communication_preference = models.CharField( max_length=20, choices=CommunicationPreference.choices, default=CommunicationPreference.PHONE, help_text='Preferred communication method' ) # Employment and Financial employer = models.CharField( max_length=200, blank=True, null=True, help_text='Employer' ) occupation = models.CharField( max_length=100, blank=True, null=True, help_text='Occupation' ) # Healthcare Information primary_care_physician = models.CharField( max_length=200, blank=True, null=True, help_text='Primary care physician' ) referring_physician = models.CharField( max_length=200, blank=True, null=True, help_text='Referring physician' ) # Allergies and Medical Alerts allergies = models.TextField( blank=True, null=True, help_text='Known allergies' ) medical_alerts = models.TextField( blank=True, null=True, help_text='Medical alerts and warnings' ) # Advance Directives has_advance_directive = models.BooleanField( default=False, help_text='Has advance directive on file' ) advance_directive_type = models.CharField( max_length=50, choices=AdvanceDirectiveType.choices, blank=True, null=True, help_text='Type of advance directive' ) # Status and Flags is_active = models.BooleanField( default=True, help_text='Patient is active' ) is_deceased = models.BooleanField( default=False, help_text='Patient is deceased' ) date_of_death = models.DateField( blank=True, null=True, help_text='Date of death' ) # VIP and Special Handling is_vip = models.BooleanField( default=False, help_text='VIP patient requiring special handling' ) confidential_patient = models.BooleanField( default=False, help_text='Confidential patient with restricted access' ) # Registration Information registration_date = models.DateTimeField( auto_now_add=True, help_text='Initial registration date' ) registered_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='registered_patients', help_text='User who registered the patient' ) photo = models.ImageField( upload_to='patient_photos/', blank=True, null=True, help_text='Patient photo' ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) last_visit_date = models.DateTimeField( blank=True, null=True, help_text='Last visit date' ) class Meta: db_table = 'patients_patient_profile' verbose_name = 'Patient Profile' verbose_name_plural = 'Patient Profiles' ordering = ['last_name', 'first_name'] indexes = [ models.Index(fields=['tenant', 'mrn']), models.Index(fields=['last_name', 'first_name']), models.Index(fields=['date_of_birth']), models.Index(fields=['id_number']), models.Index(fields=['mobile_number']), models.Index(fields=['email']), ] unique_together = ['tenant', 'mrn'] def __str__(self): return f"{self.get_full_name()} (MRN: {self.mrn})" def get_full_name(self): """ Return the full name for the patient. """ if self.preferred_name: name = f"{self.preferred_name} {self.last_name}" else: name = f"{self.first_name} {self.last_name}" if self.suffix: name += f" {self.suffix}" return name def get_display_name(self): """ Return the display name for the patient. """ return self.get_full_name() @property def age(self): """ Calculate patient age. """ if self.date_of_birth: today = timezone.now().date() return today.year - self.date_of_birth.year - ( (today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day) ) return None @property def full_address(self): """ Return formatted full address. """ address_parts = [] if self.address_line_1: address_parts.append(self.address_line_1) if self.address_line_2: address_parts.append(self.address_line_2) if self.city: city_state_zip = self.city if self.state: city_state_zip += f", {self.state}" if self.zip_code: city_state_zip += f" {self.zip_code}" address_parts.append(city_state_zip) if self.country and self.country != 'United States': address_parts.append(self.country) return '\n'.join(address_parts) if address_parts else None class EmergencyContact(models.Model): """ Emergency contact information for patients. """ class Relationship(models.TextChoices): SPOUSE = 'SPOUSE', 'Spouse' PARENT = 'PARENT', 'Parent' CHILD = 'CHILD', 'Child' SIBLING = 'SIBLING', 'Sibling' GRANDPARENT = 'GRANDPARENT', 'Grandparent' GRANDCHILD = 'GRANDCHILD', 'Grandchild' AUNT_UNCLE = 'AUNT_UNCLE', 'Aunt/Uncle' COUSIN = 'COUSIN', 'Cousin' FRIEND = 'FRIEND', 'Friend' NEIGHBOR = 'NEIGHBOR', 'Neighbor' CAREGIVER = 'CAREGIVER', 'Caregiver' GUARDIAN = 'GUARDIAN', 'Guardian' OTHER = 'OTHER', 'Other' # Patient relationship patient = models.ForeignKey( PatientProfile, on_delete=models.CASCADE, related_name='emergency_contacts' ) # Contact Information first_name = models.CharField( max_length=150, help_text='First name' ) last_name = models.CharField( max_length=150, help_text='Last name' ) relationship = models.CharField( max_length=50, choices=Relationship.choices, help_text='Relationship to patient' ) # Contact Details phone_number = models.CharField( max_length=20, validators=[RegexValidator( regex=r'^\+?1?\d{9,15}$', message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.' )], help_text='Primary phone number' ) mobile_number = models.CharField( max_length=20, blank=True, null=True, validators=[RegexValidator( regex=r'^\+?1?\d{9,15}$', message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.' )], help_text='Mobile phone number' ) email = models.EmailField( blank=True, null=True, help_text='Email address' ) # Address address_line_1 = models.CharField( max_length=255, blank=True, null=True, help_text='Address line 1' ) address_line_2 = models.CharField( max_length=255, blank=True, null=True, help_text='Address line 2' ) city = models.CharField( max_length=100, blank=True, null=True, help_text='City' ) state = models.CharField( max_length=50, blank=True, null=True, help_text='State/Province' ) zip_code = models.CharField( max_length=20, blank=True, null=True, help_text='ZIP/Postal code' ) # Priority and Authorization priority = models.PositiveIntegerField( default=1, help_text='Contact priority (1 = highest)' ) is_authorized_for_medical_decisions = models.BooleanField( default=False, help_text='Authorized to make medical decisions' ) is_authorized_for_financial_decisions = models.BooleanField( default=False, help_text='Authorized to make financial decisions' ) is_authorized_for_information = models.BooleanField( default=True, help_text='Authorized to receive medical information' ) is_primary = models.BooleanField( default=False, help_text='Primary emergency contact' ) authorization_number = models.CharField( max_length=100, blank=True, null=True, help_text='Authorization number' ) # Status is_active = models.BooleanField( default=True, help_text='Contact is active' ) # Notes notes = models.TextField( blank=True, null=True, help_text='Additional notes about this contact' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'patients_emergency_contact' verbose_name = 'Emergency Contact' verbose_name_plural = 'Emergency Contacts' ordering = ['priority', 'last_name', 'first_name'] indexes = [ models.Index(fields=['patient', 'priority']), models.Index(fields=['phone_number']), ] def __str__(self): return f"{self.first_name} {self.last_name} ({self.relationship}) - {self.patient.get_full_name()}" def get_full_name(self): """ Return the full name for the contact. """ return f"{self.first_name} {self.last_name}" class InsuranceInfo(models.Model): """ Insurance information for patients. """ class InsuranceType(models.TextChoices): PRIMARY = 'PRIMARY', 'Primary' SECONDARY = 'SECONDARY', 'Secondary' TERTIARY = 'TERTIARY', 'Tertiary' class PlanType(models.TextChoices): HMO = 'HMO', 'Health Maintenance Organization' PPO = 'PPO', 'Preferred Provider Organization' EPO = 'EPO', 'Exclusive Provider Organization' POS = 'POS', 'Point of Service' HDHP = 'HDHP', 'High Deductible Health Plan' MEDICARE = 'MEDICARE', 'Medicare' MEDICAID = 'MEDICAID', 'Medicaid' TRICARE = 'TRICARE', 'TRICARE' WORKERS_COMP = 'WORKERS_COMP', 'Workers Compensation' AUTO = 'AUTO', 'Auto Insurance' OTHER = 'OTHER', 'Other' class SubscriberRelationship(models.TextChoices): SELF = 'SELF', 'Self' SPOUSE = 'SPOUSE', 'Spouse' CHILD = 'CHILD', 'Child' PARENT = 'PARENT', 'Parent' OTHER = 'OTHER', 'Other' class InsuranceStatus(models.TextChoices): PENDING = 'PENDING', 'Pending' APPROVED = 'APPROVED', 'Approved' DENIED = 'DENIED', 'Denied' # Patient relationship patient = models.ForeignKey( PatientProfile, on_delete=models.CASCADE, related_name='insurance_info' ) # Insurance Details insurance_type = models.CharField( max_length=20, choices=InsuranceType.choices, default=InsuranceType.PRIMARY, help_text='Insurance type' ) # Insurance Company insurance_company = models.CharField( max_length=200, help_text='Insurance company name' ) plan_name = models.CharField( max_length=200, blank=True, null=True, help_text='Insurance plan name' ) plan_type = models.CharField( max_length=50, choices=PlanType.choices, blank=True, null=True, help_text='Plan type' ) status = models.CharField( max_length=20, choices=InsuranceStatus.choices, default=InsuranceStatus.PENDING, help_text='Insurance status' ) # Policy Information policy_number = models.CharField( max_length=100, help_text='Policy/Member ID number' ) group_number = models.CharField( max_length=100, blank=True, null=True, help_text='Group number' ) # Subscriber Information subscriber_name = models.CharField( max_length=200, help_text='Subscriber name' ) subscriber_relationship = models.CharField( max_length=20, choices=SubscriberRelationship.choices, default=SubscriberRelationship.SELF, help_text='Relationship to subscriber' ) subscriber_dob = models.DateField( blank=True, null=True, help_text='Subscriber date of birth' ) subscriber_id_number = models.CharField( max_length=10, unique=True, blank=True, null=True, validators=[RegexValidator( regex=r'^\d{10}$', message='Saudi National ID must be exactly 10 digits' )], help_text='Saudi National ID (10 digits)' ) # Coverage Information effective_date = models.DateField( help_text='Coverage effective date' ) termination_date = models.DateField( blank=True, null=True, help_text='Coverage termination date' ) # Financial Information copay_amount = models.DecimalField( max_digits=10, decimal_places=2, blank=True, null=True, help_text='Copay amount' ) deductible_amount = models.DecimalField( max_digits=10, decimal_places=2, blank=True, null=True, help_text='Deductible amount' ) out_of_pocket_max = models.DecimalField( max_digits=10, decimal_places=2, blank=True, null=True, help_text='Out of pocket maximum' ) # Verification is_verified = models.BooleanField( default=False, help_text='Insurance has been verified' ) verification_date = models.DateTimeField( blank=True, null=True, help_text='Date insurance was verified' ) verified_by = models.ForeignKey( 'hr.Employee', on_delete=models.SET_NULL, null=True, blank=True, related_name='verified_insurance', help_text='User who verified insurance' ) # Authorization requires_authorization = models.BooleanField( default=False, help_text='Requires prior authorization' ) authorization_number = models.CharField( max_length=100, blank=True, null=True, help_text='Authorization number' ) authorization_expiry = models.DateField( blank=True, null=True, help_text='Authorization expiry date' ) # Status is_active = models.BooleanField( default=True, help_text='Insurance is active' ) is_primary = models.BooleanField( default=False, help_text='Primary insurance' ) # Notes notes = models.TextField( blank=True, null=True, help_text='Additional notes about this insurance' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'patients_insurance_info' verbose_name = 'Insurance Information' verbose_name_plural = 'Insurance Information' ordering = ['insurance_type', 'insurance_company'] indexes = [ models.Index(fields=['patient', 'insurance_type']), models.Index(fields=['policy_number']), models.Index(fields=['is_verified']), ] def __str__(self): return f"{self.insurance_company} ({self.insurance_type}) - {self.patient.get_full_name()}" @property def is_coverage_active(self): """ Check if coverage is currently active. """ today = timezone.now().date() if self.termination_date and today > self.termination_date: return False return today >= self.effective_date and self.is_active class InsuranceClaim(models.Model): """ Insurance claims for patient services and treatments. Designed for Saudi healthcare system with local insurance providers. """ # Claim Status Choices class ClaimStatus(models.TextChoices): DRAFT = 'DRAFT', 'Draft' SUBMITTED = 'SUBMITTED', 'Submitted' UNDER_REVIEW = 'UNDER_REVIEW', 'Under Review' APPROVED = 'APPROVED', 'Approved' PARTIALLY_APPROVED = 'PARTIALLY_APPROVED', 'Partially Approved' DENIED = 'DENIED', 'Denied' PAID = 'PAID', 'Paid' CANCELLED = 'CANCELLED', 'Cancelled' APPEALED = 'APPEALED', 'Appealed' RESUBMITTED = 'RESUBMITTED', 'Resubmitted' class ClaimType(models.TextChoices): MEDICAL = 'MEDICAL', 'Medical' DENTAL = 'DENTAL', 'Dental' VISION = 'VISION', 'Vision' PHARMACY = 'PHARMACY', 'Pharmacy' EMERGENCY = 'EMERGENCY', 'Emergency' INPATIENT = 'INPATIENT', 'Inpatient' OUTPATIENT = 'OUTPATIENT', 'Outpatient' PREVENTIVE = 'PREVENTIVE', 'Preventive Care' MATERNITY = 'MATERNITY', 'Maternity' MENTAL_HEALTH = 'MENTAL_HEALTH', 'Mental Health' REHABILITATION = 'REHABILITATION', 'Rehabilitation' DIAGNOSTIC = 'DIAGNOSTIC', 'Diagnostic' SURGICAL = 'SURGICAL', 'Surgical' CHRONIC_CARE = 'CHRONIC_CARE', 'Chronic Care' class ClaimPriority(models.TextChoices): LOW = 'LOW', 'Low' NORMAL = 'NORMAL', 'Normal' HIGH = 'HIGH', 'High' URGENT = 'URGENT', 'Urgent' EMERGENCY = 'EMERGENCY', 'Emergency' # Basic Information claim_number = models.CharField( max_length=50, unique=True, help_text='Unique claim number' ) # Relationships patient = models.ForeignKey( PatientProfile, on_delete=models.CASCADE, related_name='insurance_claims', help_text='Patient associated with this claim' ) insurance_info = models.ForeignKey( InsuranceInfo, on_delete=models.CASCADE, related_name='claims', help_text='Insurance policy used for this claim' ) # Claim Details claim_type = models.CharField( max_length=20, choices=ClaimType.choices, default=ClaimType.MEDICAL, help_text='Type of claim' ) status = models.CharField( max_length=20, choices=ClaimStatus.choices, default=ClaimStatus.DRAFT, help_text='Current claim status' ) priority = models.CharField( max_length=10, choices=ClaimPriority.choices, default=ClaimPriority.NORMAL, help_text='Claim priority' ) # Service Information service_date = models.DateField( help_text='Date when service was provided' ) service_provider = models.CharField( max_length=200, help_text='Healthcare provider who provided the service' ) service_provider_license = models.CharField( max_length=50, blank=True, null=True, help_text='Provider license number (Saudi Medical License)' ) facility_name = models.CharField( max_length=200, blank=True, null=True, help_text='Healthcare facility name' ) facility_license = models.CharField( max_length=50, blank=True, null=True, help_text='Facility license number (MOH License)' ) # Medical Codes (Saudi/International Standards) primary_diagnosis_code = models.CharField( max_length=20, help_text='Primary diagnosis code (ICD-10)' ) primary_diagnosis_description = models.TextField( help_text='Primary diagnosis description' ) secondary_diagnosis_codes = models.JSONField( default=list, blank=True, help_text='Secondary diagnosis codes and descriptions' ) procedure_codes = models.JSONField( default=list, blank=True, help_text='Procedure codes (CPT/HCPCS) and descriptions' ) # Financial Information (Saudi Riyal) billed_amount = models.DecimalField( max_digits=12, decimal_places=2, help_text='Total amount billed (SAR)' ) approved_amount = models.DecimalField( max_digits=12, decimal_places=2, default=0, help_text='Amount approved by insurance (SAR)' ) paid_amount = models.DecimalField( max_digits=12, decimal_places=2, default=0, help_text='Amount actually paid (SAR)' ) patient_responsibility = models.DecimalField( max_digits=12, decimal_places=2, default=0, help_text='Patient copay/deductible amount (SAR)' ) discount_amount = models.DecimalField( max_digits=12, decimal_places=2, default=0, help_text='Discount applied (SAR)' ) # Claim Processing submitted_date = models.DateTimeField( blank=True, null=True, help_text='Date claim was submitted to insurance' ) processed_date = models.DateTimeField( blank=True, null=True, help_text='Date claim was processed' ) payment_date = models.DateTimeField( blank=True, null=True, help_text='Date payment was received' ) # Saudi-specific fields saudi_id_number = models.CharField( max_length=10, blank=True, null=True, help_text='Saudi National ID or Iqama number' ) insurance_card_number = models.CharField( max_length=50, blank=True, null=True, help_text='Insurance card number' ) authorization_number = models.CharField( max_length=50, blank=True, null=True, help_text='Prior authorization number if required' ) # Denial/Appeal Information denial_reason = models.TextField( blank=True, null=True, help_text='Reason for denial if applicable' ) denial_code = models.CharField( max_length=20, blank=True, null=True, help_text='Insurance denial code' ) appeal_date = models.DateTimeField( blank=True, null=True, help_text='Date appeal was filed' ) appeal_reason = models.TextField( blank=True, null=True, help_text='Reason for appeal' ) # Additional Information notes = models.TextField( blank=True, null=True, help_text='Additional notes about the claim' ) attachments = models.JSONField( default=list, blank=True, help_text='List of attached documents' ) # Tracking created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_claims' ) class Meta: db_table = 'patients_insurance_claim' verbose_name = 'Insurance Claim' verbose_name_plural = 'Insurance Claims' ordering = ['-created_at'] indexes = [ models.Index(fields=['claim_number']), models.Index(fields=['patient', 'service_date']), models.Index(fields=['status', 'priority']), models.Index(fields=['submitted_date']), models.Index(fields=['insurance_info']), ] def __str__(self): return f"Claim {self.claim_number} - {self.patient.get_full_name()}" @property def is_approved(self): """Check if claim is approved.""" return self.status in ['APPROVED', 'PARTIALLY_APPROVED', 'PAID'] @property def is_denied(self): """Check if claim is denied.""" return self.status == 'DENIED' @property def is_paid(self): """Check if claim is paid.""" return self.status == 'PAID' @property def days_since_submission(self): """Calculate days since submission.""" if self.submitted_date: return (timezone.now() - self.submitted_date).days return None @property def processing_time_days(self): """Calculate processing time in days.""" if self.submitted_date and self.processed_date: return (self.processed_date - self.submitted_date).days return None @property def approval_percentage(self): """Calculate approval percentage.""" if self.billed_amount > 0: return (self.approved_amount / self.billed_amount) * 100 return 0 def save(self, *args, **kwargs): # Generate claim number if not provided if not self.claim_number: import random from datetime import datetime year = datetime.now().year random_num = random.randint(100000, 999999) self.claim_number = f"CLM{year}{random_num}" # Auto-set dates based on status changes if self.status == 'SUBMITTED' and not self.submitted_date: self.submitted_date = timezone.now() elif self.status in ['APPROVED', 'PARTIALLY_APPROVED', 'DENIED'] and not self.processed_date: self.processed_date = timezone.now() elif self.status == 'PAID' and not self.payment_date: self.payment_date = timezone.now() super().save(*args, **kwargs) class ClaimDocument(models.Model): """ Documents attached to insurance claims. """ class DocumentType(models.TextChoices): MEDICAL_REPORT = 'MEDICAL_REPORT', 'Medical Report' LAB_RESULT = 'LAB_RESULT', 'Laboratory Result' RADIOLOGY_REPORT = 'RADIOLOGY_REPORT', 'Radiology Report' PRESCRIPTION = 'PRESCRIPTION', 'Prescription' INVOICE = 'INVOICE', 'Invoice' RECEIPT = 'RECEIPT', 'Receipt' AUTHORIZATION = 'AUTHORIZATION', 'Prior Authorization' REFERRAL = 'REFERRAL', 'Referral Letter' DISCHARGE_SUMMARY = 'DISCHARGE_SUMMARY', 'Discharge Summary' OPERATIVE_REPORT = 'OPERATIVE_REPORT', 'Operative Report' PATHOLOGY_REPORT = 'PATHOLOGY_REPORT', 'Pathology Report' INSURANCE_CARD = 'INSURANCE_CARD', 'Insurance Card Copy' ID_COPY = 'ID_COPY', 'ID Copy' OTHER = 'OTHER', 'Other' claim = models.ForeignKey( InsuranceClaim, on_delete=models.CASCADE, related_name='documents' ) document_type = models.CharField( max_length=20, choices=DocumentType.choices, help_text='Type of document' ) title = models.CharField( max_length=200, help_text='Document title' ) description = models.TextField( blank=True, null=True, help_text='Document description' ) file_path = models.CharField( max_length=500, help_text='Path to the document file' ) file_size = models.PositiveIntegerField( help_text='File size in bytes' ) mime_type = models.CharField( max_length=100, help_text='MIME type of the file' ) uploaded_at = models.DateTimeField(auto_now_add=True) uploaded_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True ) class Meta: db_table = 'patients_claim_document' verbose_name = 'Claim Document' verbose_name_plural = 'Claim Documents' ordering = ['-uploaded_at'] def __str__(self): return f"{self.title} - {self.claim.claim_number}" class ClaimStatusHistory(models.Model): """ Track status changes for insurance claims. """ claim = models.ForeignKey( InsuranceClaim, on_delete=models.CASCADE, related_name='status_history' ) from_status = models.CharField( max_length=20, choices=InsuranceClaim.ClaimStatus.choices, blank=True, null=True, help_text='Previous status' ) to_status = models.CharField( max_length=20, choices=InsuranceClaim.ClaimStatus.choices, help_text='New status' ) reason = models.TextField( blank=True, null=True, help_text='Reason for status change' ) notes = models.TextField( blank=True, null=True, help_text='Additional notes' ) changed_at = models.DateTimeField(auto_now_add=True) changed_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True ) class Meta: db_table = 'patients_claim_status_history' verbose_name = 'Claim Status History' verbose_name_plural = 'Claim Status Histories' ordering = ['-changed_at'] def __str__(self): return f"{self.claim.claim_number}: {self.from_status} → {self.to_status}" class ConsentTemplate(models.Model): """ Templates for consent forms. """ class ConsentCategory(models.TextChoices): TREATMENT = 'TREATMENT', 'Treatment Consent' PROCEDURE = 'PROCEDURE', 'Procedure Consent' SURGERY = 'SURGERY', 'Surgical Consent' ANESTHESIA = 'ANESTHESIA', 'Anesthesia Consent' RESEARCH = 'RESEARCH', 'Research Consent' PRIVACY = 'PRIVACY', 'Privacy Consent' FINANCIAL = 'FINANCIAL', 'Financial Consent' ADMISSION = 'ADMISSION', 'Admission Consent' DISCHARGE = 'DISCHARGE', 'Discharge Consent' OTHER = 'OTHER', 'Other' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='consent_templates' ) # Template Information name = models.CharField( max_length=200, help_text='Template name' ) description = models.TextField( blank=True, null=True, help_text='Template description' ) category = models.CharField( max_length=50, choices=ConsentCategory.choices, help_text='Consent category' ) # Content content = models.TextField( help_text='Consent form content' ) # Requirements requires_signature = models.BooleanField( default=True, help_text='Requires patient signature' ) requires_witness = models.BooleanField( default=False, help_text='Requires witness signature' ) requires_guardian = models.BooleanField( default=False, help_text='Requires guardian signature for minors' ) # Validity is_active = models.BooleanField( default=True, help_text='Template is active' ) version = models.CharField( max_length=20, default='1.0', help_text='Template version' ) effective_date = models.DateField( default=timezone.now, help_text='Template effective date' ) expiry_date = models.DateField( blank=True, null=True, help_text='Template expiry date' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_consent_templates', help_text='User who created the template' ) class Meta: db_table = 'patients_consent_template' verbose_name = 'Consent Template' verbose_name_plural = 'Consent Templates' ordering = ['category', 'name'] indexes = [ models.Index(fields=['tenant', 'category']), models.Index(fields=['is_active']), ] def __str__(self): return f"{self.name} (v{self.version})" class ConsentForm(models.Model): """ Patient consent forms. """ class ConsentStatus(models.TextChoices): PENDING = 'PENDING', 'Pending' SIGNED = 'SIGNED', 'Signed' DECLINED = 'DECLINED', 'Declined' EXPIRED = 'EXPIRED', 'Expired' REVOKED = 'REVOKED', 'Revoked' # Patient relationship patient = models.ForeignKey( PatientProfile, on_delete=models.CASCADE, related_name='consent_forms' ) # Template relationship template = models.ForeignKey( ConsentTemplate, on_delete=models.CASCADE, related_name='consent_forms' ) # Consent Information consent_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique consent identifier' ) # Status status = models.CharField( max_length=20, choices=ConsentStatus.choices, default=ConsentStatus.PENDING, help_text='Consent status' ) # Signatures patient_signature = models.TextField( blank=True, null=True, help_text='Patient digital signature' ) patient_signed_at = models.DateTimeField( blank=True, null=True, help_text='Patient signature timestamp' ) patient_ip_address = models.GenericIPAddressField( blank=True, null=True, help_text='Patient IP address when signed' ) guardian_signature = models.TextField( blank=True, null=True, help_text='Guardian digital signature' ) guardian_signed_at = models.DateTimeField( blank=True, null=True, help_text='Guardian signature timestamp' ) guardian_name = models.CharField( max_length=200, blank=True, null=True, help_text='Guardian name' ) guardian_relationship = models.CharField( max_length=50, blank=True, null=True, help_text='Guardian relationship to patient' ) witness_signature = models.TextField( blank=True, null=True, help_text='Witness digital signature' ) witness_signed_at = models.DateTimeField( blank=True, null=True, help_text='Witness signature timestamp' ) witness_name = models.CharField( max_length=200, blank=True, null=True, help_text='Witness name' ) witness_title = models.CharField( max_length=100, blank=True, null=True, help_text='Witness title' ) # Provider Information provider_name = models.CharField( max_length=200, blank=True, null=True, help_text='Provider name' ) provider_signature = models.TextField( blank=True, null=True, help_text='Provider digital signature' ) provider_signed_at = models.DateTimeField( blank=True, null=True, help_text='Provider signature timestamp' ) # Validity effective_date = models.DateTimeField( default=timezone.now, help_text='Consent effective date' ) expiry_date = models.DateTimeField( blank=True, null=True, help_text='Consent expiry date' ) # Revocation revoked_at = models.DateTimeField( blank=True, null=True, help_text='Consent revocation timestamp' ) revoked_by = models.ForeignKey( 'hr.Employee', on_delete=models.SET_NULL, null=True, blank=True, related_name='revoked_consents', help_text='User who revoked the consent' ) revocation_reason = models.TextField( blank=True, null=True, help_text='Reason for revocation' ) # Notes notes = models.TextField( blank=True, null=True, help_text='Additional notes about this consent' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_consent_forms', help_text='User who created the consent form' ) class Meta: db_table = 'patients_consent_form' verbose_name = 'Consent Form' verbose_name_plural = 'Consent Forms' ordering = ['-created_at'] indexes = [ models.Index(fields=['patient', 'status']), models.Index(fields=['template']), models.Index(fields=['consent_id']), ] def __str__(self): return f"{self.template.name} - {self.patient.get_full_name()} ({self.status})" @property def is_valid(self): """ Check if consent is currently valid. """ now = timezone.now() if self.status != 'SIGNED': return False if self.expiry_date and now > self.expiry_date: return False if self.revoked_at: return False return now >= self.effective_date @property def is_fully_signed(self): """ Check if all required signatures are present. """ if self.template.requires_signature and not self.patient_signature: return False if self.template.requires_witness and not self.witness_signature: return False if self.template.requires_guardian and self.patient.age and self.patient.age < 18: if not self.guardian_signature: return False return True class PatientNote(models.Model): """ General notes and comments about patients. """ class CaseNoteCategory(models.TextChoices): GENERAL = 'GENERAL', 'General' ADMINISTRATIVE = 'ADMINISTRATIVE', 'Administrative' CLINICAL = 'CLINICAL', 'Clinical' BILLING = 'BILLING', 'Billing' INSURANCE = 'INSURANCE', 'Insurance' SOCIAL = 'SOCIAL', 'Social' DISCHARGE = 'DISCHARGE', 'Discharge Planning' FOLLOW_UP = 'FOLLOW_UP', 'Follow-up' ALERT = 'ALERT', 'Alert' OTHER = 'OTHER', 'Other' class NotePriority(models.TextChoices): LOW = 'LOW', 'Low' NORMAL = 'NORMAL', 'Normal' HIGH = 'HIGH', 'High' URGENT = 'URGENT', 'Urgent' # Patient relationship patient = models.ForeignKey( PatientProfile, on_delete=models.CASCADE, related_name='patient_notes' ) # Note Information note_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique note identifier' ) # Content title = models.CharField( max_length=200, help_text='Note title' ) content = models.TextField( help_text='Note content' ) # Category category = models.CharField( max_length=50, choices=CaseNoteCategory.choices, default=CaseNoteCategory.GENERAL, help_text='Note category' ) # Priority priority = models.CharField( max_length=20, choices=NotePriority.choices, default=NotePriority.NORMAL, help_text='Note priority' ) # Visibility is_confidential = models.BooleanField( default=False, help_text='Note is confidential' ) is_alert = models.BooleanField( default=False, help_text='Note is an alert' ) # Status is_active = models.BooleanField( default=True, help_text='Note is active' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_patient_notes', help_text='User who created the note' ) class Meta: db_table = 'patients_patient_note' verbose_name = 'Patient Note' verbose_name_plural = 'Patient Notes' ordering = ['-created_at'] indexes = [ models.Index(fields=['patient', 'category']), models.Index(fields=['priority']), models.Index(fields=['is_alert']), ] def __str__(self): return f"{self.title} - {self.patient.get_full_name()}"