1678 lines
45 KiB
Python
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()}"
|
|
|