1682 lines
44 KiB
Python
1682 lines
44 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.
|
|
"""
|
|
GENDER_CHOICES = [
|
|
('MALE', 'Male'),
|
|
('FEMALE', 'Female'),
|
|
('OTHER', 'Other'),
|
|
('UNKNOWN', 'Unknown'),
|
|
('PREFER_NOT_TO_SAY', 'Prefer not to say'),
|
|
]
|
|
MARITAL_STATUS_CHOICES = [
|
|
('SINGLE', 'Single'),
|
|
('MARRIED', 'Married'),
|
|
('DIVORCED', 'Divorced'),
|
|
('WIDOWED', 'Widowed'),
|
|
('SEPARATED', 'Separated'),
|
|
('DOMESTIC_PARTNER', 'Domestic Partner'),
|
|
('OTHER', 'Other'),
|
|
('UNKNOWN', 'Unknown'),
|
|
]
|
|
COMMUNICATION_PREFERENCE_CHOICES = [
|
|
('PHONE', 'Phone'),
|
|
('EMAIL', 'Email'),
|
|
('SMS', 'SMS'),
|
|
('MAIL', 'Mail'),
|
|
('PORTAL', 'Patient Portal'),
|
|
]
|
|
ADVANCE_DIRECTIVE_TYPE_CHOICES = [
|
|
('LIVING_WILL', 'Living Will'),
|
|
('HEALTHCARE_PROXY', 'Healthcare Proxy'),
|
|
('DNR', 'Do Not Resuscitate'),
|
|
('POLST', 'POLST'),
|
|
('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=MARITAL_STATUS_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=COMMUNICATION_PREFERENCE_CHOICES,
|
|
default='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=ADVANCE_DIRECTIVE_TYPE_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.
|
|
"""
|
|
RELATIONSHIP_CHOICES = [
|
|
('SPOUSE', 'Spouse'),
|
|
('PARENT', 'Parent'),
|
|
('CHILD', 'Child'),
|
|
('SIBLING', 'Sibling'),
|
|
('GRANDPARENT', 'Grandparent'),
|
|
('GRANDCHILD', 'Grandchild'),
|
|
('AUNT_UNCLE', 'Aunt/Uncle'),
|
|
('COUSIN', 'Cousin'),
|
|
('FRIEND', 'Friend'),
|
|
('NEIGHBOR', 'Neighbor'),
|
|
('CAREGIVER', 'Caregiver'),
|
|
('GUARDIAN', 'Guardian'),
|
|
('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.
|
|
"""
|
|
INSURANCE_TYPE_CHOICES = [
|
|
('PRIMARY', 'Primary'),
|
|
('SECONDARY', 'Secondary'),
|
|
('TERTIARY', 'Tertiary'),
|
|
]
|
|
PLAN_TYPE_CHOICES = [
|
|
('HMO', 'Health Maintenance Organization'),
|
|
('PPO', 'Preferred Provider Organization'),
|
|
('EPO', 'Exclusive Provider Organization'),
|
|
('POS', 'Point of Service'),
|
|
('HDHP', 'High Deductible Health Plan'),
|
|
('MEDICARE', 'Medicare'),
|
|
('MEDICAID', 'Medicaid'),
|
|
('TRICARE', 'TRICARE'),
|
|
('WORKERS_COMP', 'Workers Compensation'),
|
|
('AUTO', 'Auto Insurance'),
|
|
('OTHER', 'Other'),
|
|
]
|
|
SUBSCRIBER_RELATIONSHIP_CHOICES = [
|
|
('SELF', 'Self'),
|
|
('SPOUSE', 'Spouse'),
|
|
('CHILD', 'Child'),
|
|
('PARENT', 'Parent'),
|
|
('OTHER', 'Other'),
|
|
]
|
|
STATUS_CHOICES = [
|
|
('PENDING', 'Pending'),
|
|
('APPROVED', 'Approved'),
|
|
('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=INSURANCE_TYPE_CHOICES,
|
|
default='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=PLAN_TYPE_CHOICES,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Plan type'
|
|
)
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=STATUS_CHOICES,
|
|
default='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=SUBSCRIBER_RELATIONSHIP_CHOICES,
|
|
default='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
|
|
STATUS_CHOICES = [
|
|
('DRAFT', 'Draft'),
|
|
('SUBMITTED', 'Submitted'),
|
|
('UNDER_REVIEW', 'Under Review'),
|
|
('APPROVED', 'Approved'),
|
|
('PARTIALLY_APPROVED', 'Partially Approved'),
|
|
('DENIED', 'Denied'),
|
|
('PAID', 'Paid'),
|
|
('CANCELLED', 'Cancelled'),
|
|
('APPEALED', 'Appealed'),
|
|
('RESUBMITTED', 'Resubmitted'),
|
|
]
|
|
|
|
# Claim Type Choices
|
|
CLAIM_TYPE_CHOICES = [
|
|
('MEDICAL', 'Medical'),
|
|
('DENTAL', 'Dental'),
|
|
('VISION', 'Vision'),
|
|
('PHARMACY', 'Pharmacy'),
|
|
('EMERGENCY', 'Emergency'),
|
|
('INPATIENT', 'Inpatient'),
|
|
('OUTPATIENT', 'Outpatient'),
|
|
('PREVENTIVE', 'Preventive Care'),
|
|
('MATERNITY', 'Maternity'),
|
|
('MENTAL_HEALTH', 'Mental Health'),
|
|
('REHABILITATION', 'Rehabilitation'),
|
|
('DIAGNOSTIC', 'Diagnostic'),
|
|
('SURGICAL', 'Surgical'),
|
|
('CHRONIC_CARE', 'Chronic Care'),
|
|
]
|
|
|
|
# Priority Choices
|
|
PRIORITY_CHOICES = [
|
|
('LOW', 'Low'),
|
|
('NORMAL', 'Normal'),
|
|
('HIGH', 'High'),
|
|
('URGENT', 'Urgent'),
|
|
('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=CLAIM_TYPE_CHOICES,
|
|
default='MEDICAL',
|
|
help_text='Type of claim'
|
|
)
|
|
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=STATUS_CHOICES,
|
|
default='DRAFT',
|
|
help_text='Current claim status'
|
|
)
|
|
|
|
priority = models.CharField(
|
|
max_length=10,
|
|
choices=PRIORITY_CHOICES,
|
|
default='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.
|
|
"""
|
|
|
|
DOCUMENT_TYPE_CHOICES = [
|
|
('MEDICAL_REPORT', 'Medical Report'),
|
|
('LAB_RESULT', 'Laboratory Result'),
|
|
('RADIOLOGY_REPORT', 'Radiology Report'),
|
|
('PRESCRIPTION', 'Prescription'),
|
|
('INVOICE', 'Invoice'),
|
|
('RECEIPT', 'Receipt'),
|
|
('AUTHORIZATION', 'Prior Authorization'),
|
|
('REFERRAL', 'Referral Letter'),
|
|
('DISCHARGE_SUMMARY', 'Discharge Summary'),
|
|
('OPERATIVE_REPORT', 'Operative Report'),
|
|
('PATHOLOGY_REPORT', 'Pathology Report'),
|
|
('INSURANCE_CARD', 'Insurance Card Copy'),
|
|
('ID_COPY', 'ID Copy'),
|
|
('OTHER', 'Other'),
|
|
]
|
|
|
|
claim = models.ForeignKey(
|
|
InsuranceClaim,
|
|
on_delete=models.CASCADE,
|
|
related_name='documents'
|
|
)
|
|
|
|
document_type = models.CharField(
|
|
max_length=20,
|
|
choices=DOCUMENT_TYPE_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.STATUS_CHOICES,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Previous status'
|
|
)
|
|
|
|
to_status = models.CharField(
|
|
max_length=20,
|
|
choices=InsuranceClaim.STATUS_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.
|
|
"""
|
|
CATEGORY_CHOICES = [
|
|
('TREATMENT', 'Treatment Consent'),
|
|
('PROCEDURE', 'Procedure Consent'),
|
|
('SURGERY', 'Surgical Consent'),
|
|
('ANESTHESIA', 'Anesthesia Consent'),
|
|
('RESEARCH', 'Research Consent'),
|
|
('PRIVACY', 'Privacy Consent'),
|
|
('FINANCIAL', 'Financial Consent'),
|
|
('ADMISSION', 'Admission Consent'),
|
|
('DISCHARGE', 'Discharge Consent'),
|
|
('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=CATEGORY_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.
|
|
"""
|
|
STATUS_CHOICES = [
|
|
('PENDING', 'Pending'),
|
|
('SIGNED', 'Signed'),
|
|
('DECLINED', 'Declined'),
|
|
('EXPIRED', 'Expired'),
|
|
('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=STATUS_CHOICES,
|
|
default='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.
|
|
"""
|
|
CATEGORY_CHOICES = [
|
|
('GENERAL', 'General'),
|
|
('ADMINISTRATIVE', 'Administrative'),
|
|
('CLINICAL', 'Clinical'),
|
|
('BILLING', 'Billing'),
|
|
('INSURANCE', 'Insurance'),
|
|
('SOCIAL', 'Social'),
|
|
('DISCHARGE', 'Discharge Planning'),
|
|
('FOLLOW_UP', 'Follow-up'),
|
|
('ALERT', 'Alert'),
|
|
('OTHER', 'Other'),
|
|
]
|
|
PRIORITY_CHOICES = [
|
|
('LOW', 'Low'),
|
|
('NORMAL', 'Normal'),
|
|
('HIGH', 'High'),
|
|
('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=CATEGORY_CHOICES,
|
|
default='GENERAL',
|
|
help_text='Note category'
|
|
)
|
|
|
|
# Priority
|
|
priority = models.CharField(
|
|
max_length=20,
|
|
choices=PRIORITY_CHOICES,
|
|
default='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()}"
|
|
|