Marwan Alwali ab2c4a36c5 update
2025-10-02 10:13:03 +03:00

1678 lines
45 KiB
Python

"""
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()}"