before changing surgery

This commit is contained in:
Marwan Alwali 2025-10-03 20:11:25 +03:00
parent ab2c4a36c5
commit 6b3916abaf
169 changed files with 22120 additions and 32857 deletions

View File

@ -0,0 +1,588 @@
# Insurance Approval Integration Documentation
## Overview
The Insurance Approval Request module has been successfully integrated with all relevant clinical order modules in the hospital management system. This document outlines the integration points and usage patterns.
---
## Integrated Modules
### 1. Laboratory Module ✅
**Model:** `LabOrder`
**File:** `laboratory/models.py`
#### Integration Details:
- Added `GenericRelation` to link lab orders with approval requests
- Added helper methods for approval status checking
- Supports multi-tenant approval tracking
#### Helper Methods Added:
```python
# Check if order has valid approval
lab_order.has_valid_approval() # Returns True/False
# Get active approval
lab_order.get_active_approval() # Returns InsuranceApprovalRequest or None
# Check if approval is required
lab_order.requires_approval() # Returns True/False
# Get approval status for display
lab_order.approval_status # Returns status string
```
#### Usage Example:
```python
from laboratory.models import LabOrder
from insurance_approvals.models import InsuranceApprovalRequest
# Create a lab order
lab_order = LabOrder.objects.get(order_number='LAB-1-000001')
# Check if approval is needed
if lab_order.requires_approval():
# Create approval request
approval = InsuranceApprovalRequest.objects.create(
tenant=lab_order.tenant,
patient=lab_order.patient,
insurance_info=lab_order.patient.insurance_info.first(),
request_type='LABORATORY',
content_object=lab_order, # Links to lab order
service_description=f"Lab tests: {', '.join([t.test_name for t in lab_order.tests.all()])}",
procedure_codes=', '.join([t.cpt_code for t in lab_order.tests.all() if t.cpt_code]),
diagnosis_codes=lab_order.diagnosis_code or '',
clinical_justification=lab_order.clinical_indication,
requesting_provider=lab_order.ordering_provider,
service_start_date=lab_order.collection_datetime.date() if lab_order.collection_datetime else timezone.now().date(),
priority='ROUTINE',
status='PENDING_SUBMISSION',
)
# Check approval status
if lab_order.has_valid_approval():
print("Order has valid approval - proceed with processing")
else:
print(f"Approval status: {lab_order.approval_status}")
# Get all approval requests for this order
approvals = lab_order.approval_requests.all()
```
---
### 2. Radiology Module ✅
**Model:** `ImagingOrder`
**File:** `radiology/models.py`
#### Integration Details:
- Added `GenericRelation` to link imaging orders with approval requests
- Added identical helper methods as laboratory module
- Supports all imaging modalities (CT, MRI, X-Ray, etc.)
#### Helper Methods Added:
```python
# Check if order has valid approval
imaging_order.has_valid_approval() # Returns True/False
# Get active approval
imaging_order.get_active_approval() # Returns InsuranceApprovalRequest or None
# Check if approval is required
imaging_order.requires_approval() # Returns True/False
# Get approval status for display
imaging_order.approval_status # Returns status string
```
#### Usage Example:
```python
from radiology.models import ImagingOrder
from insurance_approvals.models import InsuranceApprovalRequest
# Create an imaging order
imaging_order = ImagingOrder.objects.get(order_number='IMG-1-000001')
# Check if approval is needed
if imaging_order.requires_approval():
# Create approval request
approval = InsuranceApprovalRequest.objects.create(
tenant=imaging_order.tenant,
patient=imaging_order.patient,
insurance_info=imaging_order.patient.insurance_info.first(),
request_type='RADIOLOGY',
content_object=imaging_order, # Links to imaging order
service_description=imaging_order.study_description,
procedure_codes=imaging_order.modality, # Could be enhanced with CPT codes
diagnosis_codes=imaging_order.diagnosis_code or '',
clinical_justification=imaging_order.clinical_indication,
requesting_provider=imaging_order.ordering_provider,
service_start_date=imaging_order.requested_datetime.date() if imaging_order.requested_datetime else timezone.now().date(),
priority='URGENT' if imaging_order.is_stat else 'ROUTINE',
status='PENDING_SUBMISSION',
)
# Access approvals
active_approval = imaging_order.get_active_approval()
if active_approval:
print(f"Authorization Number: {active_approval.authorization_number}")
print(f"Expires: {active_approval.expiration_date}")
```
---
## Common Integration Patterns
### Pattern 1: Pre-Order Approval Check
```python
def create_order_with_approval_check(patient, order_type, **order_data):
"""
Create an order and check if insurance approval is required.
"""
# Create the order
if order_type == 'LAB':
order = LabOrder.objects.create(patient=patient, **order_data)
elif order_type == 'IMAGING':
order = ImagingOrder.objects.create(patient=patient, **order_data)
# Check if approval is required
if order.requires_approval():
# Redirect to approval request creation
return {
'order': order,
'requires_approval': True,
'redirect_url': f'/insurance-approvals/create/?order_type={order_type}&order_id={order.id}'
}
return {
'order': order,
'requires_approval': False
}
```
### Pattern 2: Approval Status Display
```python
def get_order_status_with_approval(order):
"""
Get comprehensive order status including approval information.
"""
status = {
'order_status': order.status,
'has_insurance': order.patient.insurance_info.exists(),
'requires_approval': order.requires_approval(),
'approval_status': order.approval_status,
'can_proceed': False
}
if not status['has_insurance']:
status['can_proceed'] = True # No insurance, no approval needed
elif order.has_valid_approval():
status['can_proceed'] = True
status['approval'] = order.get_active_approval()
return status
```
### Pattern 3: Bulk Approval Status Check
```python
def get_orders_needing_approval(tenant, order_type='LAB'):
"""
Get all orders that need insurance approval.
"""
if order_type == 'LAB':
Model = LabOrder
elif order_type == 'IMAGING':
Model = ImagingOrder
orders = Model.objects.filter(
tenant=tenant,
status__in=['PENDING', 'SCHEDULED']
)
orders_needing_approval = []
for order in orders:
if order.requires_approval():
orders_needing_approval.append({
'order': order,
'patient': order.patient,
'insurance': order.patient.insurance_info.first(),
'approval_status': order.approval_status
})
return orders_needing_approval
```
---
## Approval Request Types
The system supports the following request types:
1. **LABORATORY** - Lab tests and panels
2. **RADIOLOGY** - Imaging studies (CT, MRI, X-Ray, etc.)
3. **PHARMACY** - Medications (to be integrated)
4. **PROCEDURE** - Medical procedures (to be integrated)
5. **SURGERY** - Surgical procedures (to be integrated)
6. **THERAPY** - Physical/Occupational therapy
7. **DME** - Durable Medical Equipment
8. **HOME_HEALTH** - Home health services
9. **HOSPICE** - Hospice care
10. **TRANSPORTATION** - Medical transportation
11. **OTHER** - Other services
---
## Approval Workflow States
```
DRAFT → PENDING_SUBMISSION → SUBMITTED → UNDER_REVIEW
MORE_INFO_REQUIRED (can loop back to SUBMITTED)
APPROVED / PARTIALLY_APPROVED / DENIED
APPEAL_SUBMITTED → APPEAL_APPROVED / APPEAL_DENIED
EXPIRED (if expiration_date passes)
```
---
## Database Relationships
### GenericForeignKey Pattern
```python
# In InsuranceApprovalRequest model
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
# In LabOrder/ImagingOrder models
approval_requests = GenericRelation(
'insurance_approvals.InsuranceApprovalRequest',
content_type_field='content_type',
object_id_field='object_id',
related_query_name='lab_order' # or 'imaging_order'
)
```
### Querying Across Relationships
```python
# Get all lab orders with approvals
from django.contrib.contenttypes.models import ContentType
from laboratory.models import LabOrder
from insurance_approvals.models import InsuranceApprovalRequest
lab_order_ct = ContentType.objects.get_for_model(LabOrder)
approvals_for_lab_orders = InsuranceApprovalRequest.objects.filter(
content_type=lab_order_ct
)
# Get specific lab order's approvals
lab_order = LabOrder.objects.get(pk=1)
approvals = lab_order.approval_requests.all()
# Reverse query - get order from approval
approval = InsuranceApprovalRequest.objects.get(pk=1)
order = approval.content_object # Returns LabOrder or ImagingOrder instance
```
---
## Template Integration Examples
### Display Approval Status in Order List
```django
{% for order in orders %}
<tr>
<td>{{ order.order_number }}</td>
<td>{{ order.patient.get_full_name }}</td>
<td>
{% if order.approval_status == 'APPROVED' %}
<span class="badge bg-success">Approved</span>
{% elif order.approval_status == 'APPROVAL_REQUIRED' %}
<span class="badge bg-warning">Approval Required</span>
<a href="{% url 'insurance_approvals:create' %}?order_id={{ order.id }}"
class="btn btn-sm btn-primary">Request Approval</a>
{% elif order.approval_status == 'NO_INSURANCE' %}
<span class="badge bg-secondary">No Insurance</span>
{% else %}
<span class="badge bg-info">{{ order.get_approval_status_display }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
```
### Display Approval Details in Order Detail
```django
{% if order.has_valid_approval %}
<div class="alert alert-success">
<h6><i class="fa fa-check-circle"></i> Insurance Approval Active</h6>
{% with approval=order.get_active_approval %}
<p><strong>Authorization Number:</strong> {{ approval.authorization_number }}</p>
<p><strong>Expires:</strong> {{ approval.expiration_date|date:"M d, Y" }}</p>
<p><strong>Approved Quantity:</strong> {{ approval.approved_quantity }}</p>
{% endwith %}
</div>
{% elif order.requires_approval %}
<div class="alert alert-warning">
<h6><i class="fa fa-exclamation-triangle"></i> Insurance Approval Required</h6>
<p>This order requires insurance approval before processing.</p>
<a href="{% url 'insurance_approvals:create' %}?order_id={{ order.id }}"
class="btn btn-primary">Request Approval</a>
</div>
{% endif %}
```
---
## API Integration (DRF)
### Serializer Example
```python
from rest_framework import serializers
from laboratory.models import LabOrder
from insurance_approvals.models import InsuranceApprovalRequest
class LabOrderSerializer(serializers.ModelSerializer):
approval_status = serializers.CharField(read_only=True)
has_valid_approval = serializers.BooleanField(read_only=True)
requires_approval = serializers.SerializerMethodField()
active_approval = serializers.SerializerMethodField()
class Meta:
model = LabOrder
fields = '__all__'
def get_requires_approval(self, obj):
return obj.requires_approval()
def get_active_approval(self, obj):
approval = obj.get_active_approval()
if approval:
return {
'id': approval.id,
'approval_number': approval.approval_number,
'authorization_number': approval.authorization_number,
'status': approval.status,
'expiration_date': approval.expiration_date
}
return None
```
---
## Future Integration Points
### Pharmacy Module (Pending)
- **Model:** `PharmacyOrder` or `Prescription`
- **Request Type:** `PHARMACY`
- Same integration pattern as Lab and Radiology
### Operating Theatre Module (Pending)
- **Model:** `SurgerySchedule` or `OperativeCase`
- **Request Type:** `SURGERY` or `PROCEDURE`
- Same integration pattern
### Other Potential Integrations:
- Home Health Orders
- DME Orders
- Physical Therapy Orders
- Hospice Care Orders
---
## Migration Notes
**IMPORTANT:** Migrations are NOT included in this integration. The user will handle migrations separately.
### To apply changes:
1. Create migrations: `python manage.py makemigrations laboratory radiology`
2. Review migrations carefully
3. Apply migrations: `python manage.py migrate`
---
## Testing Recommendations
### Unit Tests
```python
from django.test import TestCase
from laboratory.models import LabOrder
from insurance_approvals.models import InsuranceApprovalRequest
class LabOrderApprovalIntegrationTest(TestCase):
def test_order_requires_approval_with_insurance(self):
# Create patient with insurance
patient = self.create_patient_with_insurance()
order = LabOrder.objects.create(patient=patient, ...)
self.assertTrue(order.requires_approval())
def test_order_has_valid_approval(self):
order = self.create_lab_order()
approval = InsuranceApprovalRequest.objects.create(
content_object=order,
status='APPROVED',
expiration_date=timezone.now().date() + timedelta(days=30),
...
)
self.assertTrue(order.has_valid_approval())
self.assertEqual(order.get_active_approval(), approval)
```
---
### 3. Pharmacy Module ✅
**Model:** `Prescription`
**File:** `pharmacy/models.py`
#### Integration Details:
- Added `GenericRelation` to link prescriptions with approval requests
- Added helper methods for approval status checking
- Enhanced with prior authorization support
- Checks medication formulary status for approval requirements
#### Helper Methods Added:
```python
# Check if prescription has valid approval
prescription.has_valid_approval() # Returns True/False
# Get active approval
prescription.get_active_approval() # Returns InsuranceApprovalRequest or None
# Check if approval is required
prescription.requires_approval() # Returns True/False
# Get approval status for display
prescription.approval_status # Returns status string
```
#### Special Features:
- **Prior Authorization Integration**: Checks both approval requests and prior authorization fields
- **Formulary Status**: Automatically requires approval for RESTRICTED or PRIOR_AUTH medications
- **Enhanced Status**: Returns specific statuses like 'PRIOR_AUTH_REQUIRED', 'PRIOR_AUTH_EXPIRED', etc.
#### Usage Example:
```python
from pharmacy.models import Prescription
from insurance_approvals.models import InsuranceApprovalRequest
# Create a prescription
prescription = Prescription.objects.get(prescription_number='RX-1-000001')
# Check if approval is needed (considers formulary status)
if prescription.requires_approval():
# Create approval request
approval = InsuranceApprovalRequest.objects.create(
tenant=prescription.tenant,
patient=prescription.patient,
insurance_info=prescription.patient.insurance_info.first(),
request_type='PHARMACY',
content_object=prescription, # Links to prescription
service_description=f"{prescription.medication.display_name} - {prescription.quantity_prescribed} {prescription.quantity_unit}",
procedure_codes=prescription.medication.ndc_number or '',
diagnosis_codes=prescription.diagnosis_code or '',
clinical_justification=prescription.indication or 'As prescribed',
requesting_provider=prescription.prescriber,
service_start_date=prescription.date_written,
priority='URGENT' if prescription.medication.is_controlled_substance else 'ROUTINE',
status='PENDING_SUBMISSION',
requested_quantity=prescription.quantity_prescribed,
)
# Check prior authorization
if prescription.prior_authorization_required:
if not prescription.prior_authorization_number:
print("Prior authorization required but not obtained")
```
---
### 4. Operating Theatre Module ✅
**Model:** `SurgicalCase`
**File:** `operating_theatre/models.py`
#### Integration Details:
- Added `GenericRelation` to link surgical cases with approval requests
- Added helper methods for approval status checking
- Enhanced with emergency case handling
- Supports elective and emergency approval workflows
#### Helper Methods Added:
```python
# Check if surgical case has valid approval
surgical_case.has_valid_approval() # Returns True/False
# Get active approval
surgical_case.get_active_approval() # Returns InsuranceApprovalRequest or None
# Check if approval is required
surgical_case.requires_approval() # Returns True/False
# Get approval status for display
surgical_case.approval_status # Returns status string
```
#### Special Features:
- **Emergency Case Handling**: Different approval requirements for emergency vs elective cases
- **Enhanced Status**: Returns 'EMERGENCY_APPROVAL_REQUIRED' for emergency cases
- **Procedure Code Support**: Integrates with procedure_codes JSON field
#### Usage Example:
```python
from operating_theatre.models import SurgicalCase
from insurance_approvals.models import InsuranceApprovalRequest
# Create a surgical case
surgical_case = SurgicalCase.objects.get(case_number='SURG-20250103-0001')
# Check if approval is needed
if surgical_case.requires_approval():
# Determine priority based on case type
if surgical_case.is_emergency:
priority = 'STAT'
is_expedited = True
else:
priority = 'ROUTINE'
is_expedited = False
# Create approval request
approval = InsuranceApprovalRequest.objects.create(
tenant=surgical_case.tenant,
patient=surgical_case.patient,
insurance_info=surgical_case.patient.insurance_info.first(),
request_type='SURGERY',
content_object=surgical_case, # Links to surgical case
service_description=surgical_case.primary_procedure,
procedure_codes=', '.join(surgical_case.procedure_codes) if surgical_case.procedure_codes else '',
diagnosis_codes=', '.join(surgical_case.diagnosis_codes) if surgical_case.diagnosis_codes else surgical_case.diagnosis,
clinical_justification=surgical_case.clinical_notes or f"Surgical intervention required for {surgical_case.diagnosis}",
requesting_provider=surgical_case.primary_surgeon,
service_start_date=surgical_case.scheduled_start.date(),
priority=priority,
is_expedited=is_expedited,
status='PENDING_SUBMISSION',
)
```
---
## Summary
**Laboratory Module** - Fully integrated
**Radiology Module** - Fully integrated
**Pharmacy Module** - Fully integrated (with prior auth support)
**Operating Theatre Module** - Fully integrated (with emergency handling)
All integrated modules now support:
- Automatic approval requirement detection
- Approval status tracking
- Multi-tenant approval management
- Complete audit trail
- Flexible workflow states
- Module-specific enhancements (prior auth, emergency cases, etc.)
The integration is backward compatible and does not affect existing functionality.

View File

@ -0,0 +1,504 @@
# Insurance Approval Integration - Migration Guide
## Overview
This guide provides step-by-step instructions for applying the insurance approval integration to your hospital management system.
---
## Prerequisites
Before starting the migration:
1. ✅ Backup your database
2. ✅ Ensure all existing migrations are applied
3. ✅ Review the integration documentation (`INSURANCE_APPROVAL_INTEGRATION.md`)
4. ✅ Test in a development environment first
---
## Step 1: Verify Current State
### Check Existing Migrations
```bash
# Check migration status
python manage.py showmigrations
# Look for these apps:
# - insurance_approvals
# - laboratory
# - radiology
# - pharmacy
# - operating_theatre
```
### Verify Database Backup
```bash
# For SQLite (development)
cp db.sqlite3 db.sqlite3.backup
# For PostgreSQL (production)
pg_dump -U username -d database_name > backup_$(date +%Y%m%d_%H%M%S).sql
# For MySQL (production)
mysqldump -u username -p database_name > backup_$(date +%Y%m%d_%H%M%S).sql
```
---
## Step 2: Create Migrations
### Generate Migrations for All Modified Apps
```bash
# Create migrations for all integrated modules
python manage.py makemigrations laboratory radiology pharmacy operating_theatre
# Expected output:
# Migrations for 'laboratory':
# laboratory/migrations/0XXX_add_approval_integration.py
# - Add field approval_requests to laborder
# Migrations for 'radiology':
# radiology/migrations/0XXX_add_approval_integration.py
# - Add field approval_requests to imagingorder
# Migrations for 'pharmacy':
# pharmacy/migrations/0XXX_add_approval_integration.py
# - Add field approval_requests to prescription
# Migrations for 'operating_theatre':
# operating_theatre/migrations/0XXX_add_approval_integration.py
# - Add field approval_requests to surgicalcase
```
---
## Step 3: Review Generated Migrations
### Check Migration Files
Review each generated migration file to ensure it only adds the GenericRelation field:
```python
# Example: laboratory/migrations/0XXX_add_approval_integration.py
from django.db import migrations
import django.contrib.contenttypes.fields
class Migration(migrations.Migration):
dependencies = [
('laboratory', '0XXX_previous_migration'),
('contenttypes', '0002_remove_content_type_name'),
('insurance_approvals', '0001_initial'),
]
operations = [
# Note: GenericRelation doesn't create database fields
# This migration may be empty or just update model metadata
]
```
**Important:** GenericRelation fields don't create actual database columns. The migrations might be empty or only update model metadata.
---
## Step 4: Apply Migrations
### Run Migrations
```bash
# Apply all pending migrations
python manage.py migrate
# Expected output:
# Running migrations:
# Applying laboratory.0XXX_add_approval_integration... OK
# Applying radiology.0XXX_add_approval_integration... OK
# Applying pharmacy.0XXX_add_approval_integration... OK
# Applying operating_theatre.0XXX_add_approval_integration... OK
```
### Verify Migration Success
```bash
# Check migration status again
python manage.py showmigrations
# All migrations should show [X] (applied)
```
---
## Step 5: Test Integration
### Test in Django Shell
```python
python manage.py shell
# Test Laboratory Integration
from laboratory.models import LabOrder
from insurance_approvals.models import InsuranceApprovalRequest
# Get a lab order
order = LabOrder.objects.first()
# Test helper methods
print(f"Requires approval: {order.requires_approval()}")
print(f"Has valid approval: {order.has_valid_approval()}")
print(f"Approval status: {order.approval_status}")
# Test creating an approval
if order.patient.insurance_info.exists():
approval = InsuranceApprovalRequest.objects.create(
tenant=order.tenant,
patient=order.patient,
insurance_info=order.patient.insurance_info.first(),
request_type='LABORATORY',
content_object=order,
service_description='Test lab order',
requesting_provider=order.ordering_provider,
service_start_date=order.created_at.date(),
status='PENDING_SUBMISSION',
)
print(f"Created approval: {approval.approval_number}")
# Verify relationship
print(f"Order approvals: {order.approval_requests.count()}")
print(f"Approval content object: {approval.content_object}")
```
### Test Each Module
Repeat the above test for:
- ✅ Radiology (ImagingOrder)
- ✅ Pharmacy (Prescription)
- ✅ Operating Theatre (SurgicalCase)
---
## Step 6: Update Application Code
### Add Approval Checks to Views
#### Example: Laboratory Order Creation
```python
# laboratory/views.py
from django.contrib import messages
from django.shortcuts import redirect
from insurance_approvals.models import InsuranceApprovalRequest
class LabOrderCreateView(LoginRequiredMixin, CreateView):
model = LabOrder
form_class = LabOrderForm
def form_valid(self, form):
response = super().form_valid(form)
order = self.object
# Check if approval is required
if order.requires_approval():
messages.warning(
self.request,
f"Insurance approval required for order {order.order_number}. "
f"Please submit an approval request."
)
# Optionally redirect to approval creation
# return redirect('insurance_approvals:create', order_id=order.id)
return response
```
### Add Approval Status to Templates
#### Example: Order List Template
```django
<!-- laboratory/templates/laboratory/order_list.html -->
{% for order in orders %}
<tr>
<td>{{ order.order_number }}</td>
<td>{{ order.patient.get_full_name }}</td>
<td>
{% if order.approval_status == 'APPROVED' %}
<span class="badge bg-success">
<i class="fa fa-check"></i> Approved
</span>
{% elif order.approval_status == 'APPROVAL_REQUIRED' %}
<span class="badge bg-warning">
<i class="fa fa-exclamation-triangle"></i> Approval Required
</span>
<a href="{% url 'insurance_approvals:create' %}?order_type=LAB&order_id={{ order.id }}"
class="btn btn-sm btn-primary ms-2">
Request Approval
</a>
{% elif order.approval_status == 'NO_INSURANCE' %}
<span class="badge bg-secondary">No Insurance</span>
{% else %}
<span class="badge bg-info">{{ order.approval_status }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
```
---
## Step 7: Generate Sample Data (Optional)
### Run Data Generation Script
```bash
# Generate sample approval data
python insurance_approvals_data.py
# Expected output:
# 🏥 Creating Insurance Approval Data
# 📋 Found X tenants
# 👥 Found X patients
#
# 1⃣ Creating Approval Templates...
# Successfully created X approval templates.
#
# 2⃣ Creating Approval Requests...
# Successfully created X approval requests.
#
# 3⃣ Creating Status History...
# Successfully created X status history records.
#
# 4⃣ Creating Communication Logs...
# Successfully created X communication logs.
#
# 🎉 Insurance Approval Data Creation Complete!
```
---
## Step 8: Configure URLs (If Not Already Done)
### Add Insurance Approvals URLs
```python
# hospital_management/urls.py
from django.urls import path, include
urlpatterns = [
# ... existing patterns ...
path('insurance-approvals/', include('insurance_approvals.urls')),
]
```
---
## Step 9: Test End-to-End Workflow
### Complete Workflow Test
1. **Create an Order**
```python
# Create a lab order for a patient with insurance
order = LabOrder.objects.create(
tenant=tenant,
patient=patient_with_insurance,
ordering_provider=provider,
# ... other fields
)
```
2. **Check Approval Requirement**
```python
if order.requires_approval():
print("✓ Approval requirement detected")
```
3. **Create Approval Request**
```python
approval = InsuranceApprovalRequest.objects.create(
tenant=order.tenant,
patient=order.patient,
insurance_info=order.patient.insurance_info.first(),
request_type='LABORATORY',
content_object=order,
# ... other fields
)
```
4. **Update Approval Status**
```python
approval.status = 'APPROVED'
approval.authorization_number = 'AUTH123456'
approval.expiration_date = timezone.now().date() + timedelta(days=90)
approval.save()
```
5. **Verify Approval**
```python
assert order.has_valid_approval()
assert order.approval_status == 'APPROVED'
print("✓ Approval workflow complete")
```
---
## Troubleshooting
### Common Issues
#### Issue 1: Migration Conflicts
**Problem:** Migration conflicts with existing migrations
**Solution:**
```bash
# Reset migrations (development only!)
python manage.py migrate laboratory zero
python manage.py migrate radiology zero
python manage.py migrate pharmacy zero
python manage.py migrate operating_theatre zero
# Delete migration files
rm laboratory/migrations/0XXX_*.py
rm radiology/migrations/0XXX_*.py
rm pharmacy/migrations/0XXX_*.py
rm operating_theatre/migrations/0XXX_*.py
# Recreate migrations
python manage.py makemigrations
python manage.py migrate
```
#### Issue 2: ContentType Not Found
**Problem:** `ContentType matching query does not exist`
**Solution:**
```python
# Run in Django shell
from django.contrib.contenttypes.models import ContentType
from laboratory.models import LabOrder
# Ensure ContentType exists
ct = ContentType.objects.get_for_model(LabOrder)
print(f"ContentType created: {ct}")
```
#### Issue 3: Circular Import
**Problem:** Circular import when accessing approval_requests
**Solution:**
```python
# In helper methods, import inside the function
def has_valid_approval(self):
from django.utils import timezone # Import inside method
return self.approval_requests.filter(
status__in=['APPROVED', 'PARTIALLY_APPROVED'],
expiration_date__gte=timezone.now().date()
).exists()
```
---
## Rollback Procedure
### If You Need to Rollback
```bash
# 1. Rollback migrations
python manage.py migrate laboratory 0XXX # Previous migration number
python manage.py migrate radiology 0XXX
python manage.py migrate pharmacy 0XXX
python manage.py migrate operating_theatre 0XXX
# 2. Restore database backup
# For SQLite
cp db.sqlite3.backup db.sqlite3
# For PostgreSQL
psql -U username -d database_name < backup_file.sql
# For MySQL
mysql -u username -p database_name < backup_file.sql
# 3. Remove migration files
rm laboratory/migrations/0XXX_add_approval_integration.py
rm radiology/migrations/0XXX_add_approval_integration.py
rm pharmacy/migrations/0XXX_add_approval_integration.py
rm operating_theatre/migrations/0XXX_add_approval_integration.py
```
---
## Post-Migration Checklist
- [ ] All migrations applied successfully
- [ ] Helper methods work correctly
- [ ] Approval requests can be created
- [ ] Approval status displays correctly
- [ ] No errors in application logs
- [ ] Templates updated with approval status
- [ ] Views updated with approval checks
- [ ] Sample data generated (if needed)
- [ ] End-to-end workflow tested
- [ ] Documentation reviewed
- [ ] Team trained on new features
---
## Performance Considerations
### Database Indexes
The integration uses GenericForeignKey which queries ContentType. Ensure these indexes exist:
```sql
-- These should already exist from insurance_approvals migrations
CREATE INDEX idx_approval_content_type ON insurance_approvals_insuranceapprovalrequest(content_type_id);
CREATE INDEX idx_approval_object_id ON insurance_approvals_insuranceapprovalrequest(object_id);
CREATE INDEX idx_approval_status ON insurance_approvals_insuranceapprovalrequest(status);
CREATE INDEX idx_approval_expiration ON insurance_approvals_insuranceapprovalrequest(expiration_date);
```
### Query Optimization
```python
# Use select_related for better performance
orders = LabOrder.objects.select_related(
'patient',
'patient__insurance_info'
).prefetch_related(
'approval_requests'
)
# Check approval status efficiently
for order in orders:
if order.requires_approval():
# Process...
```
---
## Support
For issues or questions:
1. Review `INSURANCE_APPROVAL_INTEGRATION.md`
2. Check this migration guide
3. Review the insurance_approvals README.md
4. Check Django logs for errors
---
## Summary
✅ **Migration Complete When:**
- All migrations applied
- Helper methods working
- Approval workflow tested
- Templates updated
- No errors in logs
🎉 **You're ready to use the insurance approval integration!**

View File

@ -0,0 +1,460 @@
# Insurance Approval Integration - Quick Reference Card
## 🚀 Quick Start
### Check if Order Needs Approval
```python
if order.requires_approval():
# Create approval request
```
### Get Approval Status
```python
status = order.approval_status
# Returns: 'APPROVED', 'APPROVAL_REQUIRED', 'NO_INSURANCE', etc.
```
### Check if Has Valid Approval
```python
if order.has_valid_approval():
# Proceed with order
```
### Get Active Approval
```python
approval = order.get_active_approval()
if approval:
print(f"Auth #: {approval.authorization_number}")
```
---
## 📦 Integrated Models
| Module | Model | Request Type |
|--------|-------|--------------|
| Laboratory | `LabOrder` | `LABORATORY` |
| Radiology | `ImagingOrder` | `RADIOLOGY` |
| Pharmacy | `Prescription` | `PHARMACY` |
| Operating Theatre | `SurgicalCase` | `SURGERY` |
---
## 🔧 Helper Methods (All Models)
### `requires_approval()` → bool
Returns `True` if order requires insurance approval
```python
if lab_order.requires_approval():
# Patient has insurance but no valid approval
```
### `has_valid_approval()` → bool
Returns `True` if order has a valid, non-expired approval
```python
if imaging_order.has_valid_approval():
# Can proceed with imaging
```
### `get_active_approval()` → InsuranceApprovalRequest | None
Returns the active approval or None
```python
approval = prescription.get_active_approval()
if approval:
auth_number = approval.authorization_number
```
### `approval_status` → str (property)
Returns current approval status as string
```python
status = surgical_case.approval_status
# 'APPROVED', 'APPROVAL_REQUIRED', 'NO_INSURANCE', etc.
```
---
## 📝 Creating Approval Requests
### Laboratory Order
```python
from insurance_approvals.models import InsuranceApprovalRequest
approval = InsuranceApprovalRequest.objects.create(
tenant=lab_order.tenant,
patient=lab_order.patient,
insurance_info=lab_order.patient.insurance_info.first(),
request_type='LABORATORY',
content_object=lab_order,
service_description=f"Lab tests: {', '.join([t.test_name for t in lab_order.tests.all()])}",
procedure_codes=', '.join([t.cpt_code for t in lab_order.tests.all() if t.cpt_code]),
diagnosis_codes=lab_order.diagnosis_code or '',
clinical_justification=lab_order.clinical_indication,
requesting_provider=lab_order.ordering_provider,
service_start_date=lab_order.collection_datetime.date(),
priority='ROUTINE',
status='PENDING_SUBMISSION',
)
```
### Radiology Order
```python
approval = InsuranceApprovalRequest.objects.create(
tenant=imaging_order.tenant,
patient=imaging_order.patient,
insurance_info=imaging_order.patient.insurance_info.first(),
request_type='RADIOLOGY',
content_object=imaging_order,
service_description=imaging_order.study_description,
procedure_codes=imaging_order.modality,
diagnosis_codes=imaging_order.diagnosis_code or '',
clinical_justification=imaging_order.clinical_indication,
requesting_provider=imaging_order.ordering_provider,
service_start_date=imaging_order.requested_datetime.date(),
priority='URGENT' if imaging_order.is_stat else 'ROUTINE',
status='PENDING_SUBMISSION',
)
```
### Pharmacy Prescription
```python
approval = InsuranceApprovalRequest.objects.create(
tenant=prescription.tenant,
patient=prescription.patient,
insurance_info=prescription.patient.insurance_info.first(),
request_type='PHARMACY',
content_object=prescription,
service_description=f"{prescription.medication.display_name} - {prescription.quantity_prescribed} {prescription.quantity_unit}",
procedure_codes=prescription.medication.ndc_number or '',
diagnosis_codes=prescription.diagnosis_code or '',
clinical_justification=prescription.indication or 'As prescribed',
requesting_provider=prescription.prescriber,
service_start_date=prescription.date_written,
priority='URGENT' if prescription.medication.is_controlled_substance else 'ROUTINE',
requested_quantity=prescription.quantity_prescribed,
status='PENDING_SUBMISSION',
)
```
### Surgical Case
```python
approval = InsuranceApprovalRequest.objects.create(
tenant=surgical_case.tenant,
patient=surgical_case.patient,
insurance_info=surgical_case.patient.insurance_info.first(),
request_type='SURGERY',
content_object=surgical_case,
service_description=surgical_case.primary_procedure,
procedure_codes=', '.join(surgical_case.procedure_codes),
diagnosis_codes=', '.join(surgical_case.diagnosis_codes),
clinical_justification=surgical_case.clinical_notes,
requesting_provider=surgical_case.primary_surgeon,
service_start_date=surgical_case.scheduled_start.date(),
priority='STAT' if surgical_case.is_emergency else 'ROUTINE',
is_expedited=surgical_case.is_emergency,
status='PENDING_SUBMISSION',
)
```
---
## 🎨 Template Examples
### Display Approval Status Badge
```django
{% if order.approval_status == 'APPROVED' %}
<span class="badge bg-success">
<i class="fa fa-check"></i> Approved
</span>
{% elif order.approval_status == 'APPROVAL_REQUIRED' %}
<span class="badge bg-warning">
<i class="fa fa-exclamation-triangle"></i> Approval Required
</span>
{% elif order.approval_status == 'NO_INSURANCE' %}
<span class="badge bg-secondary">No Insurance</span>
{% else %}
<span class="badge bg-info">{{ order.approval_status }}</span>
{% endif %}
```
### Show Approval Details
```django
{% if order.has_valid_approval %}
{% with approval=order.get_active_approval %}
<div class="alert alert-success">
<strong>Authorization:</strong> {{ approval.authorization_number }}<br>
<strong>Expires:</strong> {{ approval.expiration_date|date:"M d, Y" }}<br>
<strong>Approved Quantity:</strong> {{ approval.approved_quantity }}
</div>
{% endwith %}
{% endif %}
```
### Request Approval Button
```django
{% if order.requires_approval %}
<a href="{% url 'insurance_approvals:create' %}?order_id={{ order.id }}"
class="btn btn-primary">
<i class="fa fa-file-medical"></i> Request Approval
</a>
{% endif %}
```
---
## 📊 Approval Status Values
| Status | Meaning |
|--------|---------|
| `NO_INSURANCE` | Patient has no insurance |
| `APPROVAL_REQUIRED` | Needs approval but none exists |
| `APPROVED` | Has valid, active approval |
| `PENDING_SUBMISSION` | Request created but not submitted |
| `SUBMITTED` | Request submitted to insurance |
| `UNDER_REVIEW` | Being reviewed by insurance |
| `MORE_INFO_REQUIRED` | Insurance needs more information |
| `PARTIALLY_APPROVED` | Partially approved |
| `DENIED` | Approval denied |
| `EXPIRED` | Approval has expired |
| `PRIOR_AUTH_REQUIRED` | Prior authorization required (Pharmacy) |
| `PRIOR_AUTH_APPROVED` | Prior authorization approved (Pharmacy) |
| `PRIOR_AUTH_EXPIRED` | Prior authorization expired (Pharmacy) |
| `EMERGENCY_APPROVAL_REQUIRED` | Emergency approval needed (Surgery) |
---
## 🔍 Querying Approvals
### Get All Approvals for an Order
```python
approvals = order.approval_requests.all()
```
### Get Active Approvals
```python
from django.utils import timezone
active_approvals = order.approval_requests.filter(
status__in=['APPROVED', 'PARTIALLY_APPROVED'],
expiration_date__gte=timezone.now().date()
)
```
### Get Latest Approval
```python
latest = order.approval_requests.order_by('-created_at').first()
```
### Get Approvals by Status
```python
pending = order.approval_requests.filter(status='PENDING_SUBMISSION')
approved = order.approval_requests.filter(status='APPROVED')
denied = order.approval_requests.filter(status='DENIED')
```
---
## 🔄 Reverse Queries
### Get Order from Approval
```python
approval = InsuranceApprovalRequest.objects.get(pk=1)
order = approval.content_object # Returns LabOrder, ImagingOrder, etc.
```
### Get All Lab Orders with Approvals
```python
from django.contrib.contenttypes.models import ContentType
from laboratory.models import LabOrder
lab_order_ct = ContentType.objects.get_for_model(LabOrder)
approvals = InsuranceApprovalRequest.objects.filter(
content_type=lab_order_ct
)
```
### Filter Orders by Approval Status
```python
# Orders needing approval
orders_needing_approval = [
order for order in LabOrder.objects.all()
if order.requires_approval()
]
# Orders with valid approval
orders_with_approval = [
order for order in LabOrder.objects.all()
if order.has_valid_approval()
]
```
---
## ⚡ Performance Tips
### Use select_related and prefetch_related
```python
orders = LabOrder.objects.select_related(
'patient',
'patient__insurance_info'
).prefetch_related(
'approval_requests'
)
```
### Bulk Check Approval Status
```python
from django.db.models import Prefetch
orders = LabOrder.objects.prefetch_related(
Prefetch(
'approval_requests',
queryset=InsuranceApprovalRequest.objects.filter(
status__in=['APPROVED', 'PARTIALLY_APPROVED']
)
)
)
```
---
## 🎯 Common Patterns
### Pattern 1: Create Order with Approval Check
```python
def create_order_with_approval(patient, **order_data):
order = LabOrder.objects.create(patient=patient, **order_data)
if order.requires_approval():
return {
'order': order,
'needs_approval': True,
'redirect': f'/insurance-approvals/create/?order_id={order.id}'
}
return {'order': order, 'needs_approval': False}
```
### Pattern 2: Validate Before Processing
```python
def process_order(order):
if order.requires_approval() and not order.has_valid_approval():
raise ValidationError("Order requires valid insurance approval")
# Process order...
```
### Pattern 3: Auto-Create Approval Request
```python
def auto_request_approval(order):
if not order.requires_approval():
return None
return InsuranceApprovalRequest.objects.create(
tenant=order.tenant,
patient=order.patient,
insurance_info=order.patient.insurance_info.first(),
request_type='LABORATORY',
content_object=order,
# ... other fields
)
```
---
## 🚨 Error Handling
### Check for Insurance
```python
if not order.patient.insurance_info.exists():
# No insurance - no approval needed
pass
```
### Handle Missing Approval
```python
try:
approval = order.get_active_approval()
if not approval:
# Create new approval request
approval = create_approval_request(order)
except Exception as e:
logger.error(f"Error getting approval: {e}")
```
### Validate Expiration
```python
from django.utils import timezone
approval = order.get_active_approval()
if approval and approval.expiration_date < timezone.now().date():
# Approval expired - need new one
pass
```
---
## 📱 API/DRF Integration
### Serializer with Approval Status
```python
from rest_framework import serializers
class LabOrderSerializer(serializers.ModelSerializer):
approval_status = serializers.CharField(read_only=True)
requires_approval = serializers.SerializerMethodField()
active_approval = serializers.SerializerMethodField()
def get_requires_approval(self, obj):
return obj.requires_approval()
def get_active_approval(self, obj):
approval = obj.get_active_approval()
return {
'id': approval.id,
'authorization_number': approval.authorization_number,
'expiration_date': approval.expiration_date
} if approval else None
```
---
## 📚 Related Documentation
- **Full Integration Guide:** `INSURANCE_APPROVAL_INTEGRATION.md`
- **Migration Guide:** `INSURANCE_APPROVAL_MIGRATION_GUIDE.md`
- **Module README:** `insurance_approvals/README.md`
---
## 💡 Tips
1. **Always check `requires_approval()` before processing orders**
2. **Use `has_valid_approval()` to verify approval is still valid**
3. **Prefetch approval_requests for better performance**
4. **Handle emergency cases differently (Surgery module)**
5. **Check formulary status for medications (Pharmacy module)**
6. **Use approval templates for consistency**
7. **Track all status changes with ApprovalStatusHistory**
8. **Log communications with insurance companies**
---
## 🎉 Quick Checklist
- [ ] Order created
- [ ] Check if approval required
- [ ] Create approval request if needed
- [ ] Submit to insurance
- [ ] Track status changes
- [ ] Update when approved
- [ ] Verify before processing
- [ ] Handle expiration
---
**Last Updated:** 2025-01-03
**Version:** 1.0

Binary file not shown.

View File

@ -1,472 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
name="PasswordHistory",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"password_hash",
models.CharField(help_text="Hashed password", max_length=128),
),
("created_at", models.DateTimeField(auto_now_add=True)),
],
options={
"verbose_name": "Password History",
"verbose_name_plural": "Password History",
"db_table": "accounts_password_history",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="SocialAccount",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"provider",
models.CharField(
choices=[
("GOOGLE", "Google"),
("MICROSOFT", "Microsoft"),
("APPLE", "Apple"),
("FACEBOOK", "Facebook"),
("LINKEDIN", "LinkedIn"),
("GITHUB", "GitHub"),
("OKTA", "Okta"),
("SAML", "SAML"),
("LDAP", "LDAP"),
],
max_length=50,
),
),
(
"provider_id",
models.CharField(help_text="Provider user ID", max_length=200),
),
(
"provider_email",
models.EmailField(
blank=True,
help_text="Email from provider",
max_length=254,
null=True,
),
),
(
"display_name",
models.CharField(
blank=True,
help_text="Display name from provider",
max_length=200,
null=True,
),
),
(
"profile_url",
models.URLField(
blank=True, help_text="Profile URL from provider", null=True
),
),
(
"avatar_url",
models.URLField(
blank=True, help_text="Avatar URL from provider", null=True
),
),
(
"access_token",
models.TextField(
blank=True, help_text="Access token from provider", null=True
),
),
(
"refresh_token",
models.TextField(
blank=True, help_text="Refresh token from provider", null=True
),
),
(
"token_expires_at",
models.DateTimeField(
blank=True, help_text="Token expiration date", null=True
),
),
(
"is_active",
models.BooleanField(
default=True, help_text="Social account is active"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"last_login_at",
models.DateTimeField(
blank=True,
help_text="Last login using this social account",
null=True,
),
),
],
options={
"verbose_name": "Social Account",
"verbose_name_plural": "Social Accounts",
"db_table": "accounts_social_account",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="TwoFactorDevice",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"device_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique device identifier",
unique=True,
),
),
("name", models.CharField(help_text="Device name", max_length=100)),
(
"device_type",
models.CharField(
choices=[
("TOTP", "Time-based OTP (Authenticator App)"),
("SMS", "SMS"),
("EMAIL", "Email"),
("HARDWARE", "Hardware Token"),
("BACKUP", "Backup Codes"),
],
max_length=20,
),
),
(
"secret_key",
models.CharField(
blank=True,
help_text="Secret key for TOTP devices",
max_length=200,
null=True,
),
),
(
"phone_number",
models.CharField(
blank=True,
help_text="Phone number for SMS devices",
max_length=20,
null=True,
),
),
(
"email_address",
models.EmailField(
blank=True,
help_text="Email address for email devices",
max_length=254,
null=True,
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Device is active"),
),
(
"is_verified",
models.BooleanField(default=False, help_text="Device is verified"),
),
(
"verified_at",
models.DateTimeField(
blank=True, help_text="Device verification date", null=True
),
),
(
"last_used_at",
models.DateTimeField(
blank=True, help_text="Last time device was used", null=True
),
),
(
"usage_count",
models.PositiveIntegerField(
default=0, help_text="Number of times device was used"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"verbose_name": "Two Factor Device",
"verbose_name_plural": "Two Factor Devices",
"db_table": "accounts_two_factor_device",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="UserSession",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"session_key",
models.CharField(
help_text="Django session key", max_length=40, unique=True
),
),
(
"session_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique session identifier",
unique=True,
),
),
("ip_address", models.GenericIPAddressField(help_text="IP address")),
("user_agent", models.TextField(help_text="User agent string")),
(
"device_type",
models.CharField(
choices=[
("DESKTOP", "Desktop"),
("MOBILE", "Mobile"),
("TABLET", "Tablet"),
("UNKNOWN", "Unknown"),
],
default="UNKNOWN",
max_length=20,
),
),
(
"browser",
models.CharField(
blank=True,
help_text="Browser name and version",
max_length=100,
null=True,
),
),
(
"operating_system",
models.CharField(
blank=True,
help_text="Operating system",
max_length=100,
null=True,
),
),
(
"country",
models.CharField(
blank=True, help_text="Country", max_length=100, null=True
),
),
(
"region",
models.CharField(
blank=True, help_text="Region/State", max_length=100, null=True
),
),
(
"city",
models.CharField(
blank=True, help_text="City", max_length=100, null=True
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Session is active"),
),
(
"login_method",
models.CharField(
choices=[
("PASSWORD", "Password"),
("TWO_FACTOR", "Two Factor"),
("SOCIAL", "Social Login"),
("SSO", "Single Sign-On"),
("API_KEY", "API Key"),
],
default="PASSWORD",
max_length=20,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("last_activity_at", models.DateTimeField(auto_now=True)),
(
"expires_at",
models.DateTimeField(help_text="Session expiration time"),
),
(
"ended_at",
models.DateTimeField(
blank=True, help_text="Session end time", null=True
),
),
],
options={
"verbose_name": "User Session",
"verbose_name_plural": "User Sessions",
"db_table": "accounts_user_session",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
(
"user_id",
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
("email", models.EmailField(max_length=254, unique=True)),
("force_password_change", models.BooleanField(default=False)),
("password_expires_at", models.DateTimeField(blank=True, null=True)),
("failed_login_attempts", models.PositiveIntegerField(default=0)),
("locked_until", models.DateTimeField(blank=True, null=True)),
("two_factor_enabled", models.BooleanField(default=False)),
("max_concurrent_sessions", models.PositiveIntegerField(default=3)),
("session_timeout_minutes", models.PositiveIntegerField(default=30)),
("last_password_change", models.DateTimeField(blank=True, null=True)),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
],
options={
"db_table": "accounts_user",
"ordering": ["last_name", "first_name"],
},
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
]

View File

@ -1,116 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("accounts", "0001_initial"),
("auth", "0012_alter_user_first_name_max_length"),
("core", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="user",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="users",
to="core.tenant",
),
),
migrations.AddField(
model_name="user",
name="user_permissions",
field=models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
migrations.AddField(
model_name="passwordhistory",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="password_history",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="socialaccount",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="social_accounts",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="twofactordevice",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="two_factor_devices",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="usersession",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="user_sessions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["tenant", "email"], name="accounts_us_tenant__162cd2_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["tenant", "username"], name="accounts_us_tenant__d92906_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["tenant", "is_active"], name="accounts_us_tenant__78e6c9_idx"
),
),
migrations.AlterUniqueTogether(
name="socialaccount",
unique_together={("provider", "provider_id")},
),
migrations.AddIndex(
model_name="usersession",
index=models.Index(
fields=["user", "is_active"], name="accounts_us_user_id_f3bc3f_idx"
),
),
migrations.AddIndex(
model_name="usersession",
index=models.Index(
fields=["session_key"], name="accounts_us_session_5ce38e_idx"
),
),
migrations.AddIndex(
model_name="usersession",
index=models.Index(
fields=["ip_address"], name="accounts_us_ip_addr_f7885b_idx"
),
),
]

View File

@ -1,623 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.core.validators
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="DashboardWidget",
fields=[
(
"widget_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"widget_type",
models.CharField(
choices=[
("CHART", "Chart Widget"),
("TABLE", "Table Widget"),
("METRIC", "Metric Widget"),
("GAUGE", "Gauge Widget"),
("MAP", "Map Widget"),
("TEXT", "Text Widget"),
("IMAGE", "Image Widget"),
("IFRAME", "IFrame Widget"),
("CUSTOM", "Custom Widget"),
],
max_length=20,
),
),
(
"chart_type",
models.CharField(
blank=True,
choices=[
("LINE", "Line Chart"),
("BAR", "Bar Chart"),
("PIE", "Pie Chart"),
("DOUGHNUT", "Doughnut Chart"),
("AREA", "Area Chart"),
("SCATTER", "Scatter Plot"),
("HISTOGRAM", "Histogram"),
("HEATMAP", "Heat Map"),
("TREEMAP", "Tree Map"),
("FUNNEL", "Funnel Chart"),
],
max_length=20,
),
),
(
"query_config",
models.JSONField(
default=dict, help_text="Query configuration for data source"
),
),
("position_x", models.PositiveIntegerField(default=0)),
("position_y", models.PositiveIntegerField(default=0)),
(
"width",
models.PositiveIntegerField(
default=4,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(12),
],
),
),
(
"height",
models.PositiveIntegerField(
default=4,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(12),
],
),
),
(
"display_config",
models.JSONField(
default=dict, help_text="Widget display configuration"
),
),
("color_scheme", models.CharField(default="default", max_length=50)),
("auto_refresh", models.BooleanField(default=True)),
(
"refresh_interval",
models.PositiveIntegerField(
default=300, help_text="Refresh interval in seconds"
),
),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "analytics_dashboard_widget",
"ordering": ["position_y", "position_x"],
},
),
migrations.CreateModel(
name="DataSource",
fields=[
(
"source_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"source_type",
models.CharField(
choices=[
("DATABASE", "Database Query"),
("API", "API Endpoint"),
("FILE", "File Upload"),
("STREAM", "Real-time Stream"),
("WEBHOOK", "Webhook"),
("CUSTOM", "Custom Source"),
],
max_length=20,
),
),
(
"connection_type",
models.CharField(
choices=[
("POSTGRESQL", "PostgreSQL"),
("MYSQL", "MySQL"),
("SQLITE", "SQLite"),
("MONGODB", "MongoDB"),
("REDIS", "Redis"),
("REST_API", "REST API"),
("GRAPHQL", "GraphQL"),
("WEBSOCKET", "WebSocket"),
("CSV", "CSV File"),
("JSON", "JSON File"),
("XML", "XML File"),
],
max_length=20,
),
),
(
"connection_config",
models.JSONField(
default=dict, help_text="Connection configuration"
),
),
(
"authentication_config",
models.JSONField(
default=dict, help_text="Authentication configuration"
),
),
(
"query_template",
models.TextField(
blank=True, help_text="SQL query or API endpoint template"
),
),
(
"parameters",
models.JSONField(default=dict, help_text="Query parameters"),
),
(
"data_transformation",
models.JSONField(
default=dict, help_text="Data transformation rules"
),
),
(
"cache_duration",
models.PositiveIntegerField(
default=300, help_text="Cache duration in seconds"
),
),
("is_healthy", models.BooleanField(default=True)),
("last_health_check", models.DateTimeField(blank=True, null=True)),
(
"health_check_interval",
models.PositiveIntegerField(
default=300, help_text="Health check interval in seconds"
),
),
("is_active", models.BooleanField(default=True)),
(
"last_test_status",
models.CharField(
choices=[
("PENDING", "Pending"),
("RUNNING", "Running"),
("SUCCESS", "Success"),
("FAILURE", "Failure"),
],
default="PENDING",
max_length=20,
),
),
("last_test_start_at", models.DateTimeField(blank=True, null=True)),
("last_test_end_at", models.DateTimeField(blank=True, null=True)),
(
"last_test_duration_seconds",
models.PositiveIntegerField(blank=True, null=True),
),
("last_test_error_message", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "analytics_data_source",
},
),
migrations.CreateModel(
name="MetricDefinition",
fields=[
(
"metric_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"metric_type",
models.CharField(
choices=[
("COUNT", "Count"),
("SUM", "Sum"),
("AVERAGE", "Average"),
("PERCENTAGE", "Percentage"),
("RATIO", "Ratio"),
("RATE", "Rate"),
("DURATION", "Duration"),
("CUSTOM", "Custom Calculation"),
],
max_length=20,
),
),
(
"calculation_config",
models.JSONField(
default=dict, help_text="Metric calculation configuration"
),
),
(
"aggregation_period",
models.CharField(
choices=[
("REAL_TIME", "Real-time"),
("HOURLY", "Hourly"),
("DAILY", "Daily"),
("WEEKLY", "Weekly"),
("MONTHLY", "Monthly"),
("QUARTERLY", "Quarterly"),
("YEARLY", "Yearly"),
],
max_length=20,
),
),
(
"aggregation_config",
models.JSONField(
default=dict, help_text="Aggregation configuration"
),
),
(
"target_value",
models.DecimalField(
blank=True, decimal_places=4, max_digits=15, null=True
),
),
(
"warning_threshold",
models.DecimalField(
blank=True, decimal_places=4, max_digits=15, null=True
),
),
(
"critical_threshold",
models.DecimalField(
blank=True, decimal_places=4, max_digits=15, null=True
),
),
("unit_of_measure", models.CharField(blank=True, max_length=50)),
(
"decimal_places",
models.PositiveIntegerField(
default=2,
validators=[django.core.validators.MaxValueValidator(10)],
),
),
("display_format", models.CharField(default="number", max_length=50)),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "analytics_metric_definition",
},
),
migrations.CreateModel(
name="MetricValue",
fields=[
(
"value_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("value", models.DecimalField(decimal_places=4, max_digits=15)),
("period_start", models.DateTimeField()),
("period_end", models.DateTimeField()),
(
"dimensions",
models.JSONField(
default=dict,
help_text="Metric dimensions (e.g., department, provider)",
),
),
(
"metadata",
models.JSONField(default=dict, help_text="Additional metadata"),
),
(
"data_quality_score",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=5,
null=True,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(100),
],
),
),
(
"confidence_level",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=5,
null=True,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(100),
],
),
),
("calculation_timestamp", models.DateTimeField(auto_now_add=True)),
(
"calculation_duration_ms",
models.PositiveIntegerField(blank=True, null=True),
),
],
options={
"db_table": "analytics_metric_value",
"ordering": ["-period_start"],
},
),
migrations.CreateModel(
name="Report",
fields=[
(
"report_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"report_type",
models.CharField(
choices=[
("OPERATIONAL", "Operational Report"),
("FINANCIAL", "Financial Report"),
("CLINICAL", "Clinical Report"),
("QUALITY", "Quality Report"),
("COMPLIANCE", "Compliance Report"),
("PERFORMANCE", "Performance Report"),
("CUSTOM", "Custom Report"),
],
max_length=20,
),
),
(
"query_config",
models.JSONField(
default=dict, help_text="Query configuration for report"
),
),
(
"output_format",
models.CharField(
choices=[
("PDF", "PDF Document"),
("EXCEL", "Excel Spreadsheet"),
("CSV", "CSV File"),
("JSON", "JSON Data"),
("HTML", "HTML Page"),
("EMAIL", "Email Report"),
],
max_length=20,
),
),
(
"template_config",
models.JSONField(
default=dict, help_text="Report template configuration"
),
),
(
"schedule_type",
models.CharField(
choices=[
("MANUAL", "Manual Execution"),
("DAILY", "Daily"),
("WEEKLY", "Weekly"),
("MONTHLY", "Monthly"),
("QUARTERLY", "Quarterly"),
("YEARLY", "Yearly"),
("CUSTOM", "Custom Schedule"),
],
default="MANUAL",
max_length=20,
),
),
(
"schedule_config",
models.JSONField(default=dict, help_text="Schedule configuration"),
),
("next_execution", models.DateTimeField(blank=True, null=True)),
(
"recipients",
models.JSONField(default=list, help_text="Report recipients"),
),
(
"distribution_config",
models.JSONField(
default=dict, help_text="Distribution configuration"
),
),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "analytics_report",
},
),
migrations.CreateModel(
name="ReportExecution",
fields=[
(
"execution_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"execution_type",
models.CharField(
choices=[
("MANUAL", "Manual"),
("SCHEDULED", "Scheduled"),
("API", "API Triggered"),
],
default="MANUAL",
max_length=20,
),
),
("started_at", models.DateTimeField(auto_now_add=True)),
("completed_at", models.DateTimeField(blank=True, null=True)),
(
"duration_seconds",
models.PositiveIntegerField(blank=True, null=True),
),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("RUNNING", "Running"),
("COMPLETED", "Completed"),
("FAILED", "Failed"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
max_length=20,
),
),
("error_message", models.TextField(blank=True)),
("output_file_path", models.CharField(blank=True, max_length=500)),
(
"output_size_bytes",
models.PositiveBigIntegerField(blank=True, null=True),
),
("record_count", models.PositiveIntegerField(blank=True, null=True)),
(
"execution_parameters",
models.JSONField(default=dict, help_text="Execution parameters"),
),
],
options={
"db_table": "analytics_report_execution",
"ordering": ["-started_at"],
},
),
migrations.CreateModel(
name="Dashboard",
fields=[
(
"dashboard_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"dashboard_type",
models.CharField(
choices=[
("EXECUTIVE", "Executive Dashboard"),
("CLINICAL", "Clinical Dashboard"),
("OPERATIONAL", "Operational Dashboard"),
("FINANCIAL", "Financial Dashboard"),
("QUALITY", "Quality Dashboard"),
("PATIENT", "Patient Dashboard"),
("PROVIDER", "Provider Dashboard"),
("DEPARTMENT", "Department Dashboard"),
("CUSTOM", "Custom Dashboard"),
],
max_length=20,
),
),
(
"layout_config",
models.JSONField(
default=dict, help_text="Dashboard layout configuration"
),
),
(
"refresh_interval",
models.PositiveIntegerField(
default=300, help_text="Refresh interval in seconds"
),
),
("is_public", models.BooleanField(default=False)),
(
"allowed_roles",
models.JSONField(
default=list, help_text="List of allowed user roles"
),
),
("is_active", models.BooleanField(default=True)),
("is_default", models.BooleanField(default=False)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"allowed_users",
models.ManyToManyField(
blank=True,
related_name="accessible_dashboards",
to=settings.AUTH_USER_MODEL,
),
),
(
"created_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"db_table": "analytics_dashboard",
},
),
]

View File

@ -1,309 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("analytics", "0001_initial"),
("core", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="dashboard",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="dashboards",
to="core.tenant",
),
),
migrations.AddField(
model_name="dashboardwidget",
name="dashboard",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="widgets",
to="analytics.dashboard",
),
),
migrations.AddField(
model_name="datasource",
name="created_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="datasource",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="data_sources",
to="core.tenant",
),
),
migrations.AddField(
model_name="dashboardwidget",
name="data_source",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="widgets",
to="analytics.datasource",
),
),
migrations.AddField(
model_name="metricdefinition",
name="created_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="metricdefinition",
name="data_source",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="metrics",
to="analytics.datasource",
),
),
migrations.AddField(
model_name="metricdefinition",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="metric_definitions",
to="core.tenant",
),
),
migrations.AddField(
model_name="metricvalue",
name="metric_definition",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="values",
to="analytics.metricdefinition",
),
),
migrations.AddField(
model_name="report",
name="created_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="report",
name="data_source",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="reports",
to="analytics.datasource",
),
),
migrations.AddField(
model_name="report",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="reports",
to="core.tenant",
),
),
migrations.AddField(
model_name="reportexecution",
name="executed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="reportexecution",
name="report",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="executions",
to="analytics.report",
),
),
migrations.AddIndex(
model_name="dashboard",
index=models.Index(
fields=["tenant", "dashboard_type"],
name="analytics_d_tenant__6c4962_idx",
),
),
migrations.AddIndex(
model_name="dashboard",
index=models.Index(
fields=["tenant", "is_active"], name="analytics_d_tenant__c68e4a_idx"
),
),
migrations.AddIndex(
model_name="dashboard",
index=models.Index(
fields=["tenant", "is_default"], name="analytics_d_tenant__f167b1_idx"
),
),
migrations.AlterUniqueTogether(
name="dashboard",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="datasource",
index=models.Index(
fields=["tenant", "source_type"], name="analytics_d_tenant__1f790a_idx"
),
),
migrations.AddIndex(
model_name="datasource",
index=models.Index(
fields=["tenant", "is_active"], name="analytics_d_tenant__a566a2_idx"
),
),
migrations.AddIndex(
model_name="datasource",
index=models.Index(
fields=["tenant", "is_healthy"], name="analytics_d_tenant__442319_idx"
),
),
migrations.AlterUniqueTogether(
name="datasource",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="dashboardwidget",
index=models.Index(
fields=["dashboard", "is_active"], name="analytics_d_dashboa_6a4da0_idx"
),
),
migrations.AddIndex(
model_name="dashboardwidget",
index=models.Index(
fields=["dashboard", "position_x", "position_y"],
name="analytics_d_dashboa_4ce236_idx",
),
),
migrations.AddIndex(
model_name="metricdefinition",
index=models.Index(
fields=["tenant", "metric_type"], name="analytics_m_tenant__74f857_idx"
),
),
migrations.AddIndex(
model_name="metricdefinition",
index=models.Index(
fields=["tenant", "aggregation_period"],
name="analytics_m_tenant__95594d_idx",
),
),
migrations.AddIndex(
model_name="metricdefinition",
index=models.Index(
fields=["tenant", "is_active"], name="analytics_m_tenant__fed8ae_idx"
),
),
migrations.AlterUniqueTogether(
name="metricdefinition",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="metricvalue",
index=models.Index(
fields=["metric_definition", "period_start"],
name="analytics_m_metric__20f4a3_idx",
),
),
migrations.AddIndex(
model_name="metricvalue",
index=models.Index(
fields=["metric_definition", "period_end"],
name="analytics_m_metric__eca5ed_idx",
),
),
migrations.AddIndex(
model_name="metricvalue",
index=models.Index(
fields=["period_start", "period_end"],
name="analytics_m_period__286467_idx",
),
),
migrations.AddIndex(
model_name="metricvalue",
index=models.Index(
fields=["calculation_timestamp"], name="analytics_m_calcula_c2ca26_idx"
),
),
migrations.AlterUniqueTogether(
name="metricvalue",
unique_together={("metric_definition", "period_start", "period_end")},
),
migrations.AddIndex(
model_name="report",
index=models.Index(
fields=["tenant", "report_type"], name="analytics_r_tenant__9818e0_idx"
),
),
migrations.AddIndex(
model_name="report",
index=models.Index(
fields=["tenant", "schedule_type"],
name="analytics_r_tenant__6d4012_idx",
),
),
migrations.AddIndex(
model_name="report",
index=models.Index(
fields=["tenant", "next_execution"],
name="analytics_r_tenant__832dfb_idx",
),
),
migrations.AddIndex(
model_name="report",
index=models.Index(
fields=["tenant", "is_active"], name="analytics_r_tenant__88f6f3_idx"
),
),
migrations.AlterUniqueTogether(
name="report",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="reportexecution",
index=models.Index(
fields=["report", "status"], name="analytics_r_report__db5768_idx"
),
),
migrations.AddIndex(
model_name="reportexecution",
index=models.Index(
fields=["report", "started_at"], name="analytics_r_report__be32b5_idx"
),
),
migrations.AddIndex(
model_name="reportexecution",
index=models.Index(
fields=["status", "started_at"], name="analytics_r_status_294e23_idx"
),
),
]

File diff suppressed because it is too large Load Diff

View File

@ -1,526 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("appointments", "0001_initial"),
("core", "0001_initial"),
("hr", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="appointmentrequest",
name="patient",
field=models.ForeignKey(
help_text="Patient requesting appointment",
on_delete=django.db.models.deletion.CASCADE,
related_name="appointment_requests",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="appointmentrequest",
name="provider",
field=models.ForeignKey(
help_text="Healthcare provider",
on_delete=django.db.models.deletion.CASCADE,
related_name="provider_appointments",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="appointmentrequest",
name="rescheduled_from",
field=models.ForeignKey(
blank=True,
help_text="Original appointment if rescheduled",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="rescheduled_appointments",
to="appointments.appointmentrequest",
),
),
migrations.AddField(
model_name="appointmentrequest",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="appointment_requests",
to="core.tenant",
),
),
migrations.AddField(
model_name="appointmenttemplate",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the template",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_appointment_templates",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="appointmenttemplate",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="appointment_templates",
to="core.tenant",
),
),
migrations.AddField(
model_name="queueentry",
name="appointment",
field=models.ForeignKey(
help_text="Associated appointment",
on_delete=django.db.models.deletion.CASCADE,
related_name="queue_entries",
to="appointments.appointmentrequest",
),
),
migrations.AddField(
model_name="queueentry",
name="assigned_provider",
field=models.ForeignKey(
blank=True,
help_text="Assigned provider",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="assigned_queue_entries",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="queueentry",
name="patient",
field=models.ForeignKey(
help_text="Patient in queue",
on_delete=django.db.models.deletion.CASCADE,
related_name="queue_entries",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="queueentry",
name="updated_by",
field=models.ForeignKey(
blank=True,
help_text="User who last updated entry",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="updated_queue_entries",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="slotavailability",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the slot",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_availability_slots",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="slotavailability",
name="provider",
field=models.ForeignKey(
help_text="Healthcare provider",
on_delete=django.db.models.deletion.CASCADE,
related_name="availability_slots",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="slotavailability",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="availability_slots",
to="core.tenant",
),
),
migrations.AddField(
model_name="telemedicinesession",
name="appointment",
field=models.OneToOneField(
help_text="Associated appointment",
on_delete=django.db.models.deletion.CASCADE,
related_name="telemedicine_session",
to="appointments.appointmentrequest",
),
),
migrations.AddField(
model_name="telemedicinesession",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the session",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_telemedicine_sessions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglist",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the waiting list entry",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_waiting_list_entries",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglist",
name="department",
field=models.ForeignKey(
help_text="Department for appointment",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_list_entries",
to="hr.department",
),
),
migrations.AddField(
model_name="waitinglist",
name="patient",
field=models.ForeignKey(
help_text="Patient on waiting list",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_list_entries",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="waitinglist",
name="provider",
field=models.ForeignKey(
blank=True,
help_text="Preferred healthcare provider",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="provider_waiting_list",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglist",
name="removed_by",
field=models.ForeignKey(
blank=True,
help_text="User who removed entry from waiting list",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="removed_waiting_list_entries",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglist",
name="scheduled_appointment",
field=models.ForeignKey(
blank=True,
help_text="Scheduled appointment from waiting list",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="waiting_list_entry",
to="appointments.appointmentrequest",
),
),
migrations.AddField(
model_name="waitinglist",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_list_entries",
to="core.tenant",
),
),
migrations.AddField(
model_name="waitinglistcontactlog",
name="contacted_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who made contact",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglistcontactlog",
name="waiting_list_entry",
field=models.ForeignKey(
help_text="Associated waiting list entry",
on_delete=django.db.models.deletion.CASCADE,
related_name="contact_logs",
to="appointments.waitinglist",
),
),
migrations.AddField(
model_name="waitingqueue",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the queue",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_waiting_queues",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitingqueue",
name="providers",
field=models.ManyToManyField(
blank=True,
help_text="Providers associated with this queue",
related_name="waiting_queues",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitingqueue",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_queues",
to="core.tenant",
),
),
migrations.AddField(
model_name="queueentry",
name="queue",
field=models.ForeignKey(
help_text="Waiting queue",
on_delete=django.db.models.deletion.CASCADE,
related_name="queue_entries",
to="appointments.waitingqueue",
),
),
migrations.AddIndex(
model_name="appointmentrequest",
index=models.Index(
fields=["tenant", "status"], name="appointment_tenant__979096_idx"
),
),
migrations.AddIndex(
model_name="appointmentrequest",
index=models.Index(
fields=["patient", "status"], name="appointment_patient_803ab4_idx"
),
),
migrations.AddIndex(
model_name="appointmentrequest",
index=models.Index(
fields=["provider", "scheduled_datetime"],
name="appointment_provide_ef6955_idx",
),
),
migrations.AddIndex(
model_name="appointmentrequest",
index=models.Index(
fields=["scheduled_datetime"], name="appointment_schedul_8f6c0e_idx"
),
),
migrations.AddIndex(
model_name="appointmentrequest",
index=models.Index(
fields=["priority", "urgency_score"],
name="appointment_priorit_cdad1a_idx",
),
),
migrations.AddIndex(
model_name="appointmentrequest",
index=models.Index(
fields=["appointment_type", "specialty"],
name="appointment_appoint_49fcc4_idx",
),
),
migrations.AddIndex(
model_name="appointmenttemplate",
index=models.Index(
fields=["tenant", "specialty"], name="appointment_tenant__8f5ab7_idx"
),
),
migrations.AddIndex(
model_name="appointmenttemplate",
index=models.Index(
fields=["appointment_type"], name="appointment_appoint_da9846_idx"
),
),
migrations.AddIndex(
model_name="appointmenttemplate",
index=models.Index(
fields=["is_active"], name="appointment_is_acti_953e67_idx"
),
),
migrations.AddIndex(
model_name="slotavailability",
index=models.Index(
fields=["tenant", "provider", "date"],
name="appointment_tenant__d41564_idx",
),
),
migrations.AddIndex(
model_name="slotavailability",
index=models.Index(
fields=["date", "start_time"], name="appointment_date_e6d843_idx"
),
),
migrations.AddIndex(
model_name="slotavailability",
index=models.Index(
fields=["specialty"], name="appointment_special_158174_idx"
),
),
migrations.AddIndex(
model_name="slotavailability",
index=models.Index(
fields=["is_active", "is_blocked"],
name="appointment_is_acti_4bd0a5_idx",
),
),
migrations.AlterUniqueTogether(
name="slotavailability",
unique_together={("provider", "date", "start_time")},
),
migrations.AddIndex(
model_name="telemedicinesession",
index=models.Index(
fields=["appointment"], name="appointment_appoint_34f472_idx"
),
),
migrations.AddIndex(
model_name="telemedicinesession",
index=models.Index(fields=["status"], name="appointment_status_f49676_idx"),
),
migrations.AddIndex(
model_name="telemedicinesession",
index=models.Index(
fields=["scheduled_start"], name="appointment_schedul_8a4e8e_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["tenant", "status"], name="appointment_tenant__a558da_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["patient", "status"], name="appointment_patient_73f03d_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["department", "specialty", "status"],
name="appointment_departm_78fd70_idx",
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["priority", "urgency_score"],
name="appointment_priorit_30fb90_idx",
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["status", "created_at"], name="appointment_status_cfe551_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["provider", "status"], name="appointment_provide_dd6c2b_idx"
),
),
migrations.AddIndex(
model_name="waitinglistcontactlog",
index=models.Index(
fields=["waiting_list_entry", "contact_date"],
name="appointment_waiting_50d8ac_idx",
),
),
migrations.AddIndex(
model_name="waitinglistcontactlog",
index=models.Index(
fields=["contact_outcome"], name="appointment_contact_ad9c45_idx"
),
),
migrations.AddIndex(
model_name="waitinglistcontactlog",
index=models.Index(
fields=["next_contact_date"], name="appointment_next_co_b29984_idx"
),
),
migrations.AddIndex(
model_name="waitingqueue",
index=models.Index(
fields=["tenant", "queue_type"], name="appointment_tenant__e21f2a_idx"
),
),
migrations.AddIndex(
model_name="waitingqueue",
index=models.Index(
fields=["specialty"], name="appointment_special_b50647_idx"
),
),
migrations.AddIndex(
model_name="waitingqueue",
index=models.Index(
fields=["is_active"], name="appointment_is_acti_e2f2a7_idx"
),
),
migrations.AddIndex(
model_name="queueentry",
index=models.Index(
fields=["queue", "status"], name="appointment_queue_i_d69f8c_idx"
),
),
migrations.AddIndex(
model_name="queueentry",
index=models.Index(
fields=["patient"], name="appointment_patient_beccf1_idx"
),
),
migrations.AddIndex(
model_name="queueentry",
index=models.Index(
fields=["priority_score"], name="appointment_priorit_48b785_idx"
),
),
migrations.AddIndex(
model_name="queueentry",
index=models.Index(
fields=["joined_at"], name="appointment_joined__709843_idx"
),
),
]

File diff suppressed because it is too large Load Diff

View File

@ -1,110 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("billing", "0001_initial"),
("core", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="billingconfiguration",
name="tenant",
field=models.OneToOneField(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="billing_configuration",
to="core.tenant",
),
),
migrations.AddField(
model_name="billlineitem",
name="rendering_provider",
field=models.ForeignKey(
blank=True,
help_text="Rendering provider",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="rendered_line_items",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="billlineitem",
name="supervising_provider",
field=models.ForeignKey(
blank=True,
help_text="Supervising provider",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="supervised_line_items",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="claimstatusupdate",
name="updated_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who made the update",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="claim_status_updates",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="insuranceclaim",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the claim",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_insurance_claims",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="insuranceclaim",
name="insurance_info",
field=models.ForeignKey(
help_text="Insurance information",
on_delete=django.db.models.deletion.CASCADE,
related_name="insurance_claims",
to="patients.insuranceinfo",
),
),
migrations.AddField(
model_name="insuranceclaim",
name="original_claim",
field=models.ForeignKey(
blank=True,
help_text="Original claim if this is a resubmission",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="resubmissions",
to="billing.insuranceclaim",
),
),
migrations.AddField(
model_name="claimstatusupdate",
name="insurance_claim",
field=models.ForeignKey(
help_text="Related insurance claim",
on_delete=django.db.models.deletion.CASCADE,
related_name="status_updates",
to="billing.insuranceclaim",
),
),
]

View File

@ -1,328 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("billing", "0002_initial"),
("core", "0001_initial"),
("emr", "0001_initial"),
("inpatients", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="medicalbill",
name="admission",
field=models.ForeignKey(
blank=True,
help_text="Related admission",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="medical_bills",
to="inpatients.admission",
),
),
migrations.AddField(
model_name="medicalbill",
name="attending_provider",
field=models.ForeignKey(
blank=True,
help_text="Attending provider",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="attending_bills",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="medicalbill",
name="billing_provider",
field=models.ForeignKey(
blank=True,
help_text="Billing provider",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="billing_provider_bills",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="medicalbill",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the bill",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_medical_bills",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="medicalbill",
name="encounter",
field=models.ForeignKey(
blank=True,
help_text="Related encounter",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="medical_bills",
to="emr.encounter",
),
),
migrations.AddField(
model_name="medicalbill",
name="patient",
field=models.ForeignKey(
help_text="Patient",
on_delete=django.db.models.deletion.CASCADE,
related_name="medical_bills",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="medicalbill",
name="primary_insurance",
field=models.ForeignKey(
blank=True,
help_text="Primary insurance",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="primary_bills",
to="patients.insuranceinfo",
),
),
migrations.AddField(
model_name="medicalbill",
name="secondary_insurance",
field=models.ForeignKey(
blank=True,
help_text="Secondary insurance",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="secondary_bills",
to="patients.insuranceinfo",
),
),
migrations.AddField(
model_name="medicalbill",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="medical_bills",
to="core.tenant",
),
),
migrations.AddField(
model_name="insuranceclaim",
name="medical_bill",
field=models.ForeignKey(
help_text="Related medical bill",
on_delete=django.db.models.deletion.CASCADE,
related_name="insurance_claims",
to="billing.medicalbill",
),
),
migrations.AddField(
model_name="billlineitem",
name="medical_bill",
field=models.ForeignKey(
help_text="Medical bill",
on_delete=django.db.models.deletion.CASCADE,
related_name="line_items",
to="billing.medicalbill",
),
),
migrations.AddField(
model_name="payment",
name="insurance_claim",
field=models.ForeignKey(
blank=True,
help_text="Related insurance claim",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="payments",
to="billing.insuranceclaim",
),
),
migrations.AddField(
model_name="payment",
name="medical_bill",
field=models.ForeignKey(
help_text="Related medical bill",
on_delete=django.db.models.deletion.CASCADE,
related_name="payments",
to="billing.medicalbill",
),
),
migrations.AddField(
model_name="payment",
name="processed_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who processed payment",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="processed_payments",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="payment",
name="received_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who received payment",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="received_payments",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddIndex(
model_name="claimstatusupdate",
index=models.Index(
fields=["insurance_claim"], name="billing_cla_insuran_7c9e49_idx"
),
),
migrations.AddIndex(
model_name="claimstatusupdate",
index=models.Index(
fields=["status_date"], name="billing_cla_status__49d81f_idx"
),
),
migrations.AddIndex(
model_name="claimstatusupdate",
index=models.Index(
fields=["new_status"], name="billing_cla_new_sta_9c28d3_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["tenant", "status"], name="billing_med_tenant__fe8d14_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["patient", "bill_date"], name="billing_med_patient_8f1a85_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["bill_number"], name="billing_med_bill_nu_f01dfa_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["status", "due_date"], name="billing_med_status_cde77f_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["collection_status"], name="billing_med_collect_6d0faf_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["medical_bill"], name="billing_ins_medical_1fec52_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["insurance_info"], name="billing_ins_insuran_e54611_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["claim_number"], name="billing_ins_claim_n_becaa3_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["status", "submission_date"],
name="billing_ins_status_921ea7_idx",
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["response_date"], name="billing_ins_respons_fc4f3d_idx"
),
),
migrations.AddIndex(
model_name="billlineitem",
index=models.Index(
fields=["medical_bill", "line_number"],
name="billing_bil_medical_37a377_idx",
),
),
migrations.AddIndex(
model_name="billlineitem",
index=models.Index(
fields=["service_code"], name="billing_bil_service_b88f5b_idx"
),
),
migrations.AddIndex(
model_name="billlineitem",
index=models.Index(
fields=["service_date"], name="billing_bil_service_658c36_idx"
),
),
migrations.AddIndex(
model_name="billlineitem",
index=models.Index(
fields=["rendering_provider"], name="billing_bil_renderi_2740ad_idx"
),
),
migrations.AlterUniqueTogether(
name="billlineitem",
unique_together={("medical_bill", "line_number")},
),
migrations.AddIndex(
model_name="payment",
index=models.Index(
fields=["medical_bill"], name="billing_pay_medical_e4e348_idx"
),
),
migrations.AddIndex(
model_name="payment",
index=models.Index(
fields=["payment_number"], name="billing_pay_payment_0825d6_idx"
),
),
migrations.AddIndex(
model_name="payment",
index=models.Index(
fields=["payment_date"], name="billing_pay_payment_bed741_idx"
),
),
migrations.AddIndex(
model_name="payment",
index=models.Index(
fields=["payment_method"], name="billing_pay_payment_715e62_idx"
),
),
migrations.AddIndex(
model_name="payment",
index=models.Index(fields=["status"], name="billing_pay_status_b7739e_idx"),
),
]

View File

@ -1,54 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-28 13:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("billing", "0003_initial"),
]
operations = [
migrations.AlterField(
model_name="billlineitem",
name="place_of_service",
field=models.IntegerField(
choices=[
(11, "Office"),
(12, "Home"),
(21, "Inpatient Hospital"),
(22, "Outpatient Hospital"),
(23, "Emergency Room"),
(24, "Ambulatory Surgical Center"),
(25, "Birthing Center"),
(26, "Military Treatment Facility"),
(31, "Skilled Nursing Facility"),
(32, "Nursing Facility"),
(33, "Custodial Care Facility"),
(34, "Hospice"),
(41, "Ambulance - Land"),
(42, "Ambulance - Air or Water"),
(49, "Independent Clinic"),
(50, "Federally Qualified Health Center"),
(51, "Inpatient Psychiatric Facility"),
(52, "Psychiatric Facility-Partial Hospitalization"),
(53, "Community Mental Health Center"),
(54, "Intermediate Care Facility/Mentally Retarded"),
(55, "Residential Substance Abuse Treatment Facility"),
(56, "Psychiatric Residential Treatment Center"),
(57, "Non-residential Substance Abuse Treatment Facility"),
(60, "Mass Immunization Center"),
(61, "Comprehensive Inpatient Rehabilitation Facility"),
(62, "Comprehensive Outpatient Rehabilitation Facility"),
(65, "End-Stage Renal Disease Treatment Facility"),
(71, "Public Health Clinic"),
(72, "Rural Health Clinic"),
(81, "Independent Laboratory"),
(99, "Other Place of Service"),
],
default=22,
help_text="Place of service code",
),
),
]

View File

@ -1,673 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="BloodComponent",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
choices=[
("WHOLE_BLOOD", "Whole Blood"),
("PACKED_RBC", "Packed Red Blood Cells"),
("FRESH_FROZEN_PLASMA", "Fresh Frozen Plasma"),
("PLATELETS", "Platelets"),
("CRYOPRECIPITATE", "Cryoprecipitate"),
("GRANULOCYTES", "Granulocytes"),
],
max_length=50,
unique=True,
),
),
("description", models.TextField()),
("shelf_life_days", models.PositiveIntegerField()),
("storage_temperature", models.CharField(max_length=50)),
("volume_ml", models.PositiveIntegerField()),
("is_active", models.BooleanField(default=True)),
],
options={
"ordering": ["name"],
},
),
migrations.CreateModel(
name="BloodTest",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"test_type",
models.CharField(
choices=[
("ABO_RH", "ABO/Rh Typing"),
("ANTIBODY_SCREEN", "Antibody Screening"),
("HIV", "HIV"),
("HBV", "Hepatitis B"),
("HCV", "Hepatitis C"),
("SYPHILIS", "Syphilis"),
("HTLV", "HTLV"),
("CMV", "CMV"),
("MALARIA", "Malaria"),
],
max_length=20,
),
),
(
"result",
models.CharField(
choices=[
("POSITIVE", "Positive"),
("NEGATIVE", "Negative"),
("INDETERMINATE", "Indeterminate"),
("PENDING", "Pending"),
],
default="PENDING",
max_length=15,
),
),
("test_date", models.DateTimeField()),
("equipment_used", models.CharField(blank=True, max_length=100)),
("lot_number", models.CharField(blank=True, max_length=50)),
("notes", models.TextField(blank=True)),
("verified_at", models.DateTimeField(blank=True, null=True)),
],
options={
"ordering": ["-test_date"],
},
),
migrations.CreateModel(
name="BloodUnit",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("unit_number", models.CharField(max_length=20, unique=True)),
("collection_date", models.DateTimeField()),
("expiry_date", models.DateTimeField()),
("volume_ml", models.PositiveIntegerField()),
(
"status",
models.CharField(
choices=[
("COLLECTED", "Collected"),
("TESTING", "Testing"),
("QUARANTINE", "Quarantine"),
("AVAILABLE", "Available"),
("RESERVED", "Reserved"),
("ISSUED", "Issued"),
("TRANSFUSED", "Transfused"),
("EXPIRED", "Expired"),
("DISCARDED", "Discarded"),
],
default="COLLECTED",
max_length=20,
),
),
("location", models.CharField(max_length=100)),
("bag_type", models.CharField(max_length=50)),
("anticoagulant", models.CharField(default="CPDA-1", max_length=50)),
("collection_site", models.CharField(max_length=100)),
("notes", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["-collection_date"],
},
),
migrations.CreateModel(
name="CrossMatch",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"test_type",
models.CharField(
choices=[
("MAJOR", "Major Crossmatch"),
("MINOR", "Minor Crossmatch"),
("IMMEDIATE_SPIN", "Immediate Spin"),
("ANTIGLOBULIN", "Antiglobulin Test"),
],
max_length=20,
),
),
(
"compatibility",
models.CharField(
choices=[
("COMPATIBLE", "Compatible"),
("INCOMPATIBLE", "Incompatible"),
("PENDING", "Pending"),
],
default="PENDING",
max_length=15,
),
),
("test_date", models.DateTimeField()),
("temperature", models.CharField(default="37°C", max_length=20)),
("incubation_time", models.PositiveIntegerField(default=15)),
("notes", models.TextField(blank=True)),
("verified_at", models.DateTimeField(blank=True, null=True)),
],
options={
"ordering": ["-test_date"],
},
),
migrations.CreateModel(
name="Donor",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("donor_id", models.CharField(max_length=20, unique=True)),
("first_name", models.CharField(max_length=100)),
("last_name", models.CharField(max_length=100)),
("date_of_birth", models.DateField()),
(
"gender",
models.CharField(
choices=[
("MALE", "Male"),
("FEMALE", "Female"),
("OTHER", "Other"),
],
max_length=10,
),
),
("national_id", models.CharField(max_length=10, unique=True)),
("phone", models.CharField(max_length=20)),
("email", models.EmailField(blank=True, max_length=254)),
("address", models.TextField()),
("emergency_contact_name", models.CharField(max_length=100)),
("emergency_contact_phone", models.CharField(max_length=20)),
(
"donor_type",
models.CharField(
choices=[
("VOLUNTARY", "Voluntary"),
("REPLACEMENT", "Replacement"),
("AUTOLOGOUS", "Autologous"),
("DIRECTED", "Directed"),
],
default="VOLUNTARY",
max_length=20,
),
),
(
"status",
models.CharField(
choices=[
("ACTIVE", "Active"),
("DEFERRED", "Deferred"),
("PERMANENTLY_DEFERRED", "Permanently Deferred"),
("INACTIVE", "Inactive"),
],
default="ACTIVE",
max_length=20,
),
),
("registration_date", models.DateTimeField(auto_now_add=True)),
("last_donation_date", models.DateTimeField(blank=True, null=True)),
("total_donations", models.PositiveIntegerField(default=0)),
(
"weight",
models.FloatField(
validators=[django.core.validators.MinValueValidator(45.0)]
),
),
(
"height",
models.FloatField(
validators=[django.core.validators.MinValueValidator(140.0)]
),
),
("notes", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["-registration_date"],
},
),
migrations.CreateModel(
name="InventoryLocation",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100, unique=True)),
(
"location_type",
models.CharField(
choices=[
("REFRIGERATOR", "Refrigerator"),
("FREEZER", "Freezer"),
("PLATELET_AGITATOR", "Platelet Agitator"),
("QUARANTINE", "Quarantine"),
("TESTING", "Testing Area"),
],
max_length=20,
),
),
("temperature_range", models.CharField(max_length=50)),
("temperature", models.FloatField(blank=True, null=True)),
("capacity", models.PositiveIntegerField()),
("current_stock", models.PositiveIntegerField(default=0)),
("is_active", models.BooleanField(default=True)),
("notes", models.TextField(blank=True)),
],
options={
"ordering": ["name"],
},
),
migrations.CreateModel(
name="QualityControl",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"test_type",
models.CharField(
choices=[
("TEMPERATURE_MONITORING", "Temperature Monitoring"),
("EQUIPMENT_CALIBRATION", "Equipment Calibration"),
("REAGENT_TESTING", "Reagent Testing"),
("PROFICIENCY_TESTING", "Proficiency Testing"),
("PROCESS_VALIDATION", "Process Validation"),
],
max_length=30,
),
),
("test_date", models.DateTimeField()),
("equipment_tested", models.CharField(blank=True, max_length=100)),
("parameters_tested", models.TextField()),
("expected_results", models.TextField()),
("actual_results", models.TextField()),
(
"status",
models.CharField(
choices=[
("PASS", "Pass"),
("FAIL", "Fail"),
("PENDING", "Pending"),
],
max_length=10,
),
),
("review_date", models.DateTimeField(blank=True, null=True)),
("review_notes", models.TextField(blank=True)),
("corrective_action", models.TextField(blank=True)),
("next_test_date", models.DateTimeField(blank=True, null=True)),
("capa_initiated", models.BooleanField(default=False)),
("capa_number", models.CharField(blank=True, max_length=50)),
(
"capa_priority",
models.CharField(
blank=True,
choices=[
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
],
max_length=10,
),
),
("capa_date", models.DateTimeField(blank=True, null=True)),
("capa_assessment", models.TextField(blank=True)),
(
"capa_status",
models.CharField(
blank=True,
choices=[
("OPEN", "Open"),
("IN_PROGRESS", "In Progress"),
("CLOSED", "Closed"),
],
max_length=20,
),
),
],
options={
"ordering": ["-test_date"],
},
),
migrations.CreateModel(
name="Transfusion",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("start_time", models.DateTimeField()),
("end_time", models.DateTimeField(blank=True, null=True)),
(
"status",
models.CharField(
choices=[
("STARTED", "Started"),
("IN_PROGRESS", "In Progress"),
("COMPLETED", "Completed"),
("STOPPED", "Stopped"),
("ADVERSE_REACTION", "Adverse Reaction"),
],
default="STARTED",
max_length=20,
),
),
(
"volume_transfused",
models.PositiveIntegerField(blank=True, null=True),
),
("transfusion_rate", models.CharField(blank=True, max_length=50)),
("pre_transfusion_vitals", models.JSONField(default=dict)),
("post_transfusion_vitals", models.JSONField(default=dict)),
("vital_signs_history", models.JSONField(default=list)),
("current_blood_pressure", models.CharField(blank=True, max_length=20)),
("current_heart_rate", models.IntegerField(blank=True, null=True)),
("current_temperature", models.FloatField(blank=True, null=True)),
(
"current_respiratory_rate",
models.IntegerField(blank=True, null=True),
),
(
"current_oxygen_saturation",
models.IntegerField(blank=True, null=True),
),
("last_vitals_check", models.DateTimeField(blank=True, null=True)),
("patient_consent", models.BooleanField(default=False)),
("consent_date", models.DateTimeField(blank=True, null=True)),
("notes", models.TextField(blank=True)),
("stop_reason", models.TextField(blank=True)),
("completion_notes", models.TextField(blank=True)),
],
options={
"ordering": ["-start_time"],
},
),
migrations.CreateModel(
name="AdverseReaction",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"reaction_type",
models.CharField(
choices=[
("FEBRILE", "Febrile Non-Hemolytic"),
("ALLERGIC", "Allergic"),
("HEMOLYTIC_ACUTE", "Acute Hemolytic"),
("HEMOLYTIC_DELAYED", "Delayed Hemolytic"),
("ANAPHYLACTIC", "Anaphylactic"),
("SEPTIC", "Septic"),
("CIRCULATORY_OVERLOAD", "Circulatory Overload"),
("LUNG_INJURY", "Transfusion-Related Acute Lung Injury"),
("OTHER", "Other"),
],
max_length=30,
),
),
(
"severity",
models.CharField(
choices=[
("MILD", "Mild"),
("MODERATE", "Moderate"),
("SEVERE", "Severe"),
("LIFE_THREATENING", "Life Threatening"),
],
max_length=20,
),
),
("onset_time", models.DateTimeField()),
("symptoms", models.TextField()),
("treatment_given", models.TextField()),
("outcome", models.TextField()),
("investigation_notes", models.TextField(blank=True)),
("regulatory_reported", models.BooleanField(default=False)),
("report_date", models.DateTimeField(blank=True, null=True)),
(
"investigated_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="investigated_reactions",
to=settings.AUTH_USER_MODEL,
),
),
(
"reported_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="reported_reactions",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-onset_time"],
},
),
migrations.CreateModel(
name="BloodGroup",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"abo_type",
models.CharField(
choices=[("A", "A"), ("B", "B"), ("AB", "AB"), ("O", "O")],
max_length=2,
),
),
(
"rh_factor",
models.CharField(
choices=[("POS", "Positive"), ("NEG", "Negative")], max_length=8
),
),
],
options={
"ordering": ["abo_type", "rh_factor"],
"unique_together": {("abo_type", "rh_factor")},
},
),
migrations.CreateModel(
name="BloodIssue",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("issue_date", models.DateTimeField(auto_now_add=True)),
("expiry_time", models.DateTimeField()),
("returned", models.BooleanField(default=False)),
("return_date", models.DateTimeField(blank=True, null=True)),
("return_reason", models.TextField(blank=True)),
("notes", models.TextField(blank=True)),
(
"issued_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="issued_units",
to=settings.AUTH_USER_MODEL,
),
),
(
"issued_to",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="received_units",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-issue_date"],
},
),
migrations.CreateModel(
name="BloodRequest",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("request_number", models.CharField(max_length=20, unique=True)),
(
"units_requested",
models.PositiveIntegerField(
validators=[django.core.validators.MinValueValidator(1)]
),
),
(
"urgency",
models.CharField(
choices=[
("ROUTINE", "Routine"),
("URGENT", "Urgent"),
("EMERGENCY", "Emergency"),
],
default="ROUTINE",
max_length=10,
),
),
("indication", models.TextField()),
("special_requirements", models.TextField(blank=True)),
("hemoglobin_level", models.FloatField(blank=True, null=True)),
("platelet_count", models.IntegerField(blank=True, null=True)),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("PROCESSING", "Processing"),
("READY", "Ready"),
("ISSUED", "Issued"),
("COMPLETED", "Completed"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
max_length=15,
),
),
("request_date", models.DateTimeField(auto_now_add=True)),
("required_by", models.DateTimeField()),
("processed_at", models.DateTimeField(blank=True, null=True)),
("notes", models.TextField(blank=True)),
("cancellation_reason", models.TextField(blank=True)),
("cancellation_date", models.DateTimeField(blank=True, null=True)),
(
"cancelled_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="cancelled_requests",
to=settings.AUTH_USER_MODEL,
),
),
(
"component_requested",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.bloodcomponent",
),
),
],
options={
"ordering": ["-request_date"],
},
),
]

View File

@ -1,297 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("blood_bank", "0001_initial"),
("hr", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="bloodrequest",
name="patient",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="blood_requests",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="bloodrequest",
name="patient_blood_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="blood_bank.bloodgroup"
),
),
migrations.AddField(
model_name="bloodrequest",
name="processed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="processed_requests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodrequest",
name="requesting_department",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="hr.department"
),
),
migrations.AddField(
model_name="bloodrequest",
name="requesting_physician",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="blood_requests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodissue",
name="blood_request",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="issues",
to="blood_bank.bloodrequest",
),
),
migrations.AddField(
model_name="bloodtest",
name="tested_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
),
),
migrations.AddField(
model_name="bloodtest",
name="verified_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="verified_tests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodunit",
name="blood_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="blood_bank.bloodgroup"
),
),
migrations.AddField(
model_name="bloodunit",
name="collected_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="collected_units",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodunit",
name="component",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.bloodcomponent",
),
),
migrations.AddField(
model_name="bloodtest",
name="blood_unit",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="tests",
to="blood_bank.bloodunit",
),
),
migrations.AddField(
model_name="bloodissue",
name="blood_unit",
field=models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="issue",
to="blood_bank.bloodunit",
),
),
migrations.AddField(
model_name="crossmatch",
name="blood_unit",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="crossmatches",
to="blood_bank.bloodunit",
),
),
migrations.AddField(
model_name="crossmatch",
name="recipient",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="crossmatch",
name="tested_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
),
),
migrations.AddField(
model_name="crossmatch",
name="verified_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="verified_crossmatches",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodissue",
name="crossmatch",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.crossmatch",
),
),
migrations.AddField(
model_name="donor",
name="blood_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="blood_bank.bloodgroup"
),
),
migrations.AddField(
model_name="donor",
name="created_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="created_donors",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodunit",
name="donor",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="blood_units",
to="blood_bank.donor",
),
),
migrations.AddField(
model_name="qualitycontrol",
name="capa_initiated_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="initiated_capas",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="qualitycontrol",
name="performed_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="qc_tests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="qualitycontrol",
name="reviewed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="reviewed_qc_tests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfusion",
name="administered_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="administered_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfusion",
name="blood_issue",
field=models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="transfusion",
to="blood_bank.bloodissue",
),
),
migrations.AddField(
model_name="transfusion",
name="completed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="completed_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfusion",
name="stopped_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="stopped_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfusion",
name="witnessed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="witnessed_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="adversereaction",
name="transfusion",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="adverse_reactions",
to="blood_bank.transfusion",
),
),
migrations.AlterUniqueTogether(
name="bloodtest",
unique_together={("blood_unit", "test_type")},
),
]

View File

@ -1,968 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="CommunicationChannel",
fields=[
(
"channel_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the channel",
primary_key=True,
serialize=False,
),
),
("name", models.CharField(help_text="Channel name", max_length=255)),
(
"description",
models.TextField(
blank=True, help_text="Channel description", null=True
),
),
(
"channel_type",
models.CharField(
choices=[
("EMAIL", "Email"),
("SMS", "SMS"),
("PUSH", "Push Notification"),
("SLACK", "Slack"),
("TEAMS", "Microsoft Teams"),
("WEBHOOK", "Webhook"),
("PHONE", "Phone Call"),
("FAX", "Fax"),
("PAGER", "Pager"),
],
help_text="Type of communication channel",
max_length=20,
),
),
(
"provider_type",
models.CharField(
choices=[
("SMTP", "SMTP Email"),
("SENDGRID", "SendGrid"),
("MAILGUN", "Mailgun"),
("TWILIO", "Twilio SMS"),
("AWS_SNS", "AWS SNS"),
("FIREBASE", "Firebase"),
("SLACK_API", "Slack API"),
("TEAMS_API", "Teams API"),
("WEBHOOK", "Webhook"),
("CUSTOM", "Custom Provider"),
],
help_text="Provider type",
max_length=20,
),
),
(
"configuration",
models.JSONField(
default=dict, help_text="Channel configuration settings"
),
),
(
"authentication_config",
models.JSONField(
default=dict, help_text="Authentication configuration"
),
),
(
"rate_limits",
models.JSONField(
default=dict, help_text="Rate limiting configuration"
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Channel is active"),
),
(
"is_healthy",
models.BooleanField(
default=True, help_text="Channel health status"
),
),
(
"last_health_check",
models.DateTimeField(
blank=True, help_text="Last health check timestamp", null=True
),
),
(
"health_check_interval",
models.PositiveIntegerField(
default=300, help_text="Health check interval in seconds"
),
),
(
"message_count",
models.PositiveIntegerField(
default=0, help_text="Total messages sent through channel"
),
),
(
"success_count",
models.PositiveIntegerField(
default=0, help_text="Successful message deliveries"
),
),
(
"failure_count",
models.PositiveIntegerField(
default=0, help_text="Failed message deliveries"
),
),
(
"last_used_at",
models.DateTimeField(
blank=True, help_text="Last usage timestamp", null=True
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, help_text="Channel creation timestamp"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, help_text="Last update timestamp"
),
),
],
options={
"db_table": "communications_communication_channel",
},
),
migrations.CreateModel(
name="DeliveryLog",
fields=[
(
"log_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the delivery log",
primary_key=True,
serialize=False,
),
),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("PROCESSING", "Processing"),
("SENT", "Sent"),
("DELIVERED", "Delivered"),
("FAILED", "Failed"),
("BOUNCED", "Bounced"),
("REJECTED", "Rejected"),
("TIMEOUT", "Timeout"),
],
default="PENDING",
help_text="Delivery status",
max_length=20,
),
),
(
"attempt_number",
models.PositiveIntegerField(
default=1, help_text="Delivery attempt number"
),
),
(
"started_at",
models.DateTimeField(
auto_now_add=True, help_text="Delivery start timestamp"
),
),
(
"completed_at",
models.DateTimeField(
blank=True, help_text="Delivery completion timestamp", null=True
),
),
(
"external_id",
models.CharField(
blank=True,
help_text="External delivery ID",
max_length=255,
null=True,
),
),
(
"response_code",
models.CharField(
blank=True,
help_text="Response code from provider",
max_length=50,
null=True,
),
),
(
"response_message",
models.TextField(
blank=True,
help_text="Response message from provider",
null=True,
),
),
(
"error_details",
models.JSONField(
default=dict, help_text="Detailed error information"
),
),
(
"processing_time_ms",
models.PositiveIntegerField(
blank=True,
help_text="Processing time in milliseconds",
null=True,
),
),
(
"payload_size_bytes",
models.PositiveIntegerField(
blank=True, help_text="Payload size in bytes", null=True
),
),
(
"metadata",
models.JSONField(
default=dict, help_text="Additional delivery metadata"
),
),
],
options={
"db_table": "communications_delivery_log",
"ordering": ["-started_at"],
},
),
migrations.CreateModel(
name="Message",
fields=[
(
"message_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the message",
primary_key=True,
serialize=False,
),
),
(
"subject",
models.CharField(help_text="Message subject line", max_length=255),
),
("content", models.TextField(help_text="Message content/body")),
(
"message_type",
models.CharField(
choices=[
("INTERNAL", "Internal Message"),
("EMAIL", "Email"),
("SMS", "SMS"),
("PUSH", "Push Notification"),
("SLACK", "Slack Message"),
("TEAMS", "Microsoft Teams"),
("WEBHOOK", "Webhook"),
("SYSTEM", "System Message"),
("ALERT", "Alert Message"),
],
default="INTERNAL",
help_text="Type of message",
max_length=20,
),
),
(
"priority",
models.CharField(
choices=[
("LOW", "Low"),
("NORMAL", "Normal"),
("HIGH", "High"),
("URGENT", "Urgent"),
("CRITICAL", "Critical"),
],
default="NORMAL",
help_text="Message priority level",
max_length=20,
),
),
(
"status",
models.CharField(
choices=[
("DRAFT", "Draft"),
("PENDING", "Pending"),
("SENDING", "Sending"),
("SENT", "Sent"),
("DELIVERED", "Delivered"),
("READ", "Read"),
("FAILED", "Failed"),
("CANCELLED", "Cancelled"),
],
default="DRAFT",
help_text="Message status",
max_length=20,
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, help_text="Message creation timestamp"
),
),
(
"scheduled_at",
models.DateTimeField(
blank=True, help_text="Scheduled send time", null=True
),
),
(
"sent_at",
models.DateTimeField(
blank=True, help_text="Actual send timestamp", null=True
),
),
(
"expires_at",
models.DateTimeField(
blank=True, help_text="Message expiration time", null=True
),
),
(
"is_urgent",
models.BooleanField(default=False, help_text="Urgent message flag"),
),
(
"requires_acknowledgment",
models.BooleanField(
default=False, help_text="Requires recipient acknowledgment"
),
),
(
"is_confidential",
models.BooleanField(
default=False, help_text="Confidential message flag"
),
),
(
"delivery_attempts",
models.PositiveIntegerField(
default=0, help_text="Number of delivery attempts"
),
),
(
"max_delivery_attempts",
models.PositiveIntegerField(
default=3, help_text="Maximum delivery attempts"
),
),
(
"message_thread_id",
models.UUIDField(
blank=True,
help_text="Thread ID for message grouping",
null=True,
),
),
(
"external_message_id",
models.CharField(
blank=True,
help_text="External system message ID",
max_length=255,
null=True,
),
),
(
"has_attachments",
models.BooleanField(
default=False, help_text="Message has attachments"
),
),
(
"content_type",
models.CharField(
default="text/plain",
help_text="Content MIME type",
max_length=50,
),
),
],
options={
"db_table": "communications_message",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="MessageRecipient",
fields=[
(
"recipient_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the recipient",
primary_key=True,
serialize=False,
),
),
(
"recipient_type",
models.CharField(
choices=[
("USER", "User"),
("EMAIL", "Email Address"),
("PHONE", "Phone Number"),
("ROLE", "User Role"),
("DEPARTMENT", "Department"),
("GROUP", "User Group"),
],
help_text="Type of recipient",
max_length=20,
),
),
(
"email_address",
models.EmailField(
blank=True,
help_text="Email address recipient",
max_length=254,
null=True,
),
),
(
"phone_number",
models.CharField(
blank=True,
help_text="Phone number recipient",
max_length=20,
null=True,
),
),
(
"role_name",
models.CharField(
blank=True,
help_text="Role name for role-based recipients",
max_length=100,
null=True,
),
),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("SENT", "Sent"),
("DELIVERED", "Delivered"),
("READ", "Read"),
("ACKNOWLEDGED", "Acknowledged"),
("FAILED", "Failed"),
("BOUNCED", "Bounced"),
("UNSUBSCRIBED", "Unsubscribed"),
],
default="PENDING",
help_text="Delivery status",
max_length=20,
),
),
(
"sent_at",
models.DateTimeField(
blank=True, help_text="Sent timestamp", null=True
),
),
(
"delivered_at",
models.DateTimeField(
blank=True, help_text="Delivered timestamp", null=True
),
),
(
"read_at",
models.DateTimeField(
blank=True, help_text="Read timestamp", null=True
),
),
(
"acknowledged_at",
models.DateTimeField(
blank=True, help_text="Acknowledged timestamp", null=True
),
),
(
"delivery_attempts",
models.PositiveIntegerField(
default=0, help_text="Number of delivery attempts"
),
),
(
"last_attempt_at",
models.DateTimeField(
blank=True,
help_text="Last delivery attempt timestamp",
null=True,
),
),
(
"error_message",
models.TextField(
blank=True, help_text="Last delivery error message", null=True
),
),
(
"external_delivery_id",
models.CharField(
blank=True,
help_text="External delivery tracking ID",
max_length=255,
null=True,
),
),
],
options={
"db_table": "communications_message_recipient",
},
),
migrations.CreateModel(
name="NotificationTemplate",
fields=[
(
"template_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the template",
primary_key=True,
serialize=False,
),
),
("name", models.CharField(help_text="Template name", max_length=255)),
(
"description",
models.TextField(
blank=True, help_text="Template description", null=True
),
),
(
"template_type",
models.CharField(
choices=[
("EMAIL", "Email Template"),
("SMS", "SMS Template"),
("PUSH", "Push Notification Template"),
("SLACK", "Slack Template"),
("TEAMS", "Teams Template"),
("WEBHOOK", "Webhook Template"),
("SYSTEM", "System Notification Template"),
],
help_text="Type of template",
max_length=20,
),
),
(
"category",
models.CharField(
choices=[
("APPOINTMENT", "Appointment Notifications"),
("MEDICATION", "Medication Reminders"),
("LAB_RESULTS", "Lab Results"),
("BILLING", "Billing Notifications"),
("EMERGENCY", "Emergency Alerts"),
("SYSTEM", "System Notifications"),
("MARKETING", "Marketing Communications"),
("CLINICAL", "Clinical Notifications"),
("ADMINISTRATIVE", "Administrative Messages"),
("QUALITY", "Quality Alerts"),
],
help_text="Template category",
max_length=30,
),
),
(
"subject_template",
models.CharField(
blank=True,
help_text="Subject line template",
max_length=255,
null=True,
),
),
(
"content_template",
models.TextField(help_text="Message content template"),
),
(
"variables",
models.JSONField(
default=dict, help_text="Available template variables"
),
),
(
"default_values",
models.JSONField(default=dict, help_text="Default variable values"),
),
(
"formatting_rules",
models.JSONField(
default=dict, help_text="Content formatting rules"
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Template is active"),
),
(
"is_system_template",
models.BooleanField(
default=False, help_text="System-defined template"
),
),
(
"requires_approval",
models.BooleanField(
default=False, help_text="Requires approval before use"
),
),
(
"usage_count",
models.PositiveIntegerField(
default=0, help_text="Number of times template has been used"
),
),
(
"last_used_at",
models.DateTimeField(
blank=True, help_text="Last usage timestamp", null=True
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, help_text="Template creation timestamp"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, help_text="Last update timestamp"
),
),
],
options={
"db_table": "communications_notification_template",
},
),
migrations.CreateModel(
name="AlertInstance",
fields=[
(
"alert_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the alert instance",
primary_key=True,
serialize=False,
),
),
("title", models.CharField(help_text="Alert title", max_length=255)),
("description", models.TextField(help_text="Alert description")),
(
"severity",
models.CharField(
choices=[
("INFO", "Information"),
("WARNING", "Warning"),
("ERROR", "Error"),
("CRITICAL", "Critical"),
("EMERGENCY", "Emergency"),
],
help_text="Alert severity level",
max_length=20,
),
),
(
"trigger_data",
models.JSONField(
default=dict, help_text="Data that triggered the alert"
),
),
(
"context_data",
models.JSONField(default=dict, help_text="Additional context data"),
),
(
"status",
models.CharField(
choices=[
("ACTIVE", "Active"),
("ACKNOWLEDGED", "Acknowledged"),
("RESOLVED", "Resolved"),
("SUPPRESSED", "Suppressed"),
("ESCALATED", "Escalated"),
("EXPIRED", "Expired"),
],
default="ACTIVE",
help_text="Alert status",
max_length=20,
),
),
(
"triggered_at",
models.DateTimeField(
auto_now_add=True, help_text="Alert trigger timestamp"
),
),
(
"acknowledged_at",
models.DateTimeField(
blank=True, help_text="Acknowledgment timestamp", null=True
),
),
(
"resolved_at",
models.DateTimeField(
blank=True, help_text="Resolution timestamp", null=True
),
),
(
"expires_at",
models.DateTimeField(
blank=True, help_text="Alert expiration time", null=True
),
),
(
"resolution_notes",
models.TextField(
blank=True, help_text="Resolution notes", null=True
),
),
(
"escalation_level",
models.PositiveIntegerField(
default=0, help_text="Current escalation level"
),
),
(
"escalated_at",
models.DateTimeField(
blank=True, help_text="Last escalation timestamp", null=True
),
),
(
"notifications_sent",
models.PositiveIntegerField(
default=0, help_text="Number of notifications sent"
),
),
(
"last_notification_at",
models.DateTimeField(
blank=True, help_text="Last notification timestamp", null=True
),
),
(
"acknowledged_by",
models.ForeignKey(
blank=True,
help_text="User who acknowledged the alert",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="acknowledged_alerts",
to=settings.AUTH_USER_MODEL,
),
),
(
"resolved_by",
models.ForeignKey(
blank=True,
help_text="User who resolved the alert",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="resolved_alerts",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"db_table": "communications_alert_instance",
"ordering": ["-triggered_at"],
},
),
migrations.CreateModel(
name="AlertRule",
fields=[
(
"rule_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the alert rule",
primary_key=True,
serialize=False,
),
),
("name", models.CharField(help_text="Alert rule name", max_length=255)),
(
"description",
models.TextField(
blank=True, help_text="Alert rule description", null=True
),
),
(
"trigger_type",
models.CharField(
choices=[
("THRESHOLD", "Threshold Alert"),
("PATTERN", "Pattern Alert"),
("SCHEDULE", "Scheduled Alert"),
("EVENT", "Event-based Alert"),
("ANOMALY", "Anomaly Detection"),
("SYSTEM", "System Alert"),
("CLINICAL", "Clinical Alert"),
("OPERATIONAL", "Operational Alert"),
],
help_text="Type of alert trigger",
max_length=20,
),
),
(
"severity",
models.CharField(
choices=[
("INFO", "Information"),
("WARNING", "Warning"),
("ERROR", "Error"),
("CRITICAL", "Critical"),
("EMERGENCY", "Emergency"),
],
default="WARNING",
help_text="Alert severity level",
max_length=20,
),
),
(
"trigger_conditions",
models.JSONField(
default=dict, help_text="Conditions that trigger the alert"
),
),
(
"evaluation_frequency",
models.PositiveIntegerField(
default=300, help_text="Evaluation frequency in seconds"
),
),
(
"cooldown_period",
models.PositiveIntegerField(
default=3600,
help_text="Cooldown period between alerts in seconds",
),
),
(
"notification_channels",
models.JSONField(
default=list, help_text="Notification channels to use"
),
),
(
"escalation_rules",
models.JSONField(
default=dict, help_text="Escalation configuration"
),
),
(
"recipient_roles",
models.JSONField(default=list, help_text="Recipient roles"),
),
(
"is_active",
models.BooleanField(default=True, help_text="Alert rule is active"),
),
(
"is_system_rule",
models.BooleanField(default=False, help_text="System-defined rule"),
),
(
"trigger_count",
models.PositiveIntegerField(
default=0, help_text="Number of times rule has triggered"
),
),
(
"last_triggered_at",
models.DateTimeField(
blank=True, help_text="Last trigger timestamp", null=True
),
),
(
"last_evaluated_at",
models.DateTimeField(
blank=True, help_text="Last evaluation timestamp", null=True
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, help_text="Rule creation timestamp"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, help_text="Last update timestamp"
),
),
(
"created_by",
models.ForeignKey(
blank=True,
help_text="Rule creator",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_alert_rules",
to=settings.AUTH_USER_MODEL,
),
),
(
"default_recipients",
models.ManyToManyField(
blank=True,
help_text="Default alert recipients",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"db_table": "communications_alert_rule",
},
),
]

View File

@ -1,375 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("communications", "0001_initial"),
("core", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="alertrule",
name="tenant",
field=models.ForeignKey(
help_text="Tenant organization",
on_delete=django.db.models.deletion.CASCADE,
to="core.tenant",
),
),
migrations.AddField(
model_name="alertinstance",
name="alert_rule",
field=models.ForeignKey(
help_text="Associated alert rule",
on_delete=django.db.models.deletion.CASCADE,
related_name="instances",
to="communications.alertrule",
),
),
migrations.AddField(
model_name="communicationchannel",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="Channel creator",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="communicationchannel",
name="tenant",
field=models.ForeignKey(
help_text="Tenant organization",
on_delete=django.db.models.deletion.CASCADE,
to="core.tenant",
),
),
migrations.AddField(
model_name="deliverylog",
name="channel",
field=models.ForeignKey(
help_text="Communication channel used",
on_delete=django.db.models.deletion.CASCADE,
related_name="delivery_logs",
to="communications.communicationchannel",
),
),
migrations.AddField(
model_name="message",
name="reply_to_message",
field=models.ForeignKey(
blank=True,
help_text="Original message if this is a reply",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="communications.message",
),
),
migrations.AddField(
model_name="message",
name="sender",
field=models.ForeignKey(
help_text="Message sender",
on_delete=django.db.models.deletion.CASCADE,
related_name="sent_messages",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="message",
name="tenant",
field=models.ForeignKey(
help_text="Tenant organization",
on_delete=django.db.models.deletion.CASCADE,
to="core.tenant",
),
),
migrations.AddField(
model_name="deliverylog",
name="message",
field=models.ForeignKey(
help_text="Associated message",
on_delete=django.db.models.deletion.CASCADE,
related_name="delivery_logs",
to="communications.message",
),
),
migrations.AddField(
model_name="messagerecipient",
name="message",
field=models.ForeignKey(
help_text="Associated message",
on_delete=django.db.models.deletion.CASCADE,
related_name="recipients",
to="communications.message",
),
),
migrations.AddField(
model_name="messagerecipient",
name="user",
field=models.ForeignKey(
blank=True,
help_text="User recipient",
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="deliverylog",
name="recipient",
field=models.ForeignKey(
help_text="Associated recipient",
on_delete=django.db.models.deletion.CASCADE,
related_name="delivery_logs",
to="communications.messagerecipient",
),
),
migrations.AddField(
model_name="notificationtemplate",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="Template creator",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="notificationtemplate",
name="tenant",
field=models.ForeignKey(
help_text="Tenant organization",
on_delete=django.db.models.deletion.CASCADE,
to="core.tenant",
),
),
migrations.AddField(
model_name="alertrule",
name="notification_template",
field=models.ForeignKey(
blank=True,
help_text="Notification template to use",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="communications.notificationtemplate",
),
),
migrations.AddIndex(
model_name="alertinstance",
index=models.Index(
fields=["alert_rule", "status"], name="communicati_alert_r_69aa1e_idx"
),
),
migrations.AddIndex(
model_name="alertinstance",
index=models.Index(
fields=["severity", "triggered_at"],
name="communicati_severit_65b0c9_idx",
),
),
migrations.AddIndex(
model_name="alertinstance",
index=models.Index(
fields=["status", "triggered_at"], name="communicati_status_402adb_idx"
),
),
migrations.AddIndex(
model_name="alertinstance",
index=models.Index(
fields=["expires_at"], name="communicati_expires_2c10ee_idx"
),
),
migrations.AddIndex(
model_name="communicationchannel",
index=models.Index(
fields=["tenant", "channel_type"], name="communicati_tenant__0326d1_idx"
),
),
migrations.AddIndex(
model_name="communicationchannel",
index=models.Index(
fields=["is_active", "is_healthy"],
name="communicati_is_acti_77d48f_idx",
),
),
migrations.AddIndex(
model_name="communicationchannel",
index=models.Index(
fields=["last_health_check"], name="communicati_last_he_71110c_idx"
),
),
migrations.AddIndex(
model_name="communicationchannel",
index=models.Index(
fields=["provider_type"], name="communicati_provide_583ead_idx"
),
),
migrations.AlterUniqueTogether(
name="communicationchannel",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["tenant", "status"], name="communicati_tenant__d41606_idx"
),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["sender", "created_at"], name="communicati_sender__7da57d_idx"
),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["message_type", "priority"],
name="communicati_message_b3baa0_idx",
),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["scheduled_at"], name="communicati_schedul_59afe2_idx"
),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["message_thread_id"], name="communicati_message_990a68_idx"
),
),
migrations.AddIndex(
model_name="messagerecipient",
index=models.Index(
fields=["message", "status"], name="communicati_message_6c5b5b_idx"
),
),
migrations.AddIndex(
model_name="messagerecipient",
index=models.Index(
fields=["user", "status"], name="communicati_user_id_2e3702_idx"
),
),
migrations.AddIndex(
model_name="messagerecipient",
index=models.Index(
fields=["recipient_type"], name="communicati_recipie_b45f4d_idx"
),
),
migrations.AddIndex(
model_name="messagerecipient",
index=models.Index(
fields=["sent_at"], name="communicati_sent_at_10fda1_idx"
),
),
migrations.AlterUniqueTogether(
name="messagerecipient",
unique_together={
("message", "email_address"),
("message", "phone_number"),
("message", "user"),
},
),
migrations.AddIndex(
model_name="deliverylog",
index=models.Index(
fields=["message", "status"], name="communicati_message_fdf561_idx"
),
),
migrations.AddIndex(
model_name="deliverylog",
index=models.Index(
fields=["recipient", "status"], name="communicati_recipie_8a8767_idx"
),
),
migrations.AddIndex(
model_name="deliverylog",
index=models.Index(
fields=["channel", "started_at"], name="communicati_channel_03e902_idx"
),
),
migrations.AddIndex(
model_name="deliverylog",
index=models.Index(
fields=["status", "started_at"], name="communicati_status_eb46bd_idx"
),
),
migrations.AddIndex(
model_name="deliverylog",
index=models.Index(
fields=["external_id"], name="communicati_externa_64021f_idx"
),
),
migrations.AddIndex(
model_name="notificationtemplate",
index=models.Index(
fields=["tenant", "template_type"],
name="communicati_tenant__c0ae05_idx",
),
),
migrations.AddIndex(
model_name="notificationtemplate",
index=models.Index(
fields=["category", "is_active"], name="communicati_categor_2c7900_idx"
),
),
migrations.AddIndex(
model_name="notificationtemplate",
index=models.Index(
fields=["is_system_template"], name="communicati_is_syst_dae5b7_idx"
),
),
migrations.AddIndex(
model_name="notificationtemplate",
index=models.Index(
fields=["usage_count"], name="communicati_usage_c_d78c30_idx"
),
),
migrations.AlterUniqueTogether(
name="notificationtemplate",
unique_together={("tenant", "name", "template_type")},
),
migrations.AddIndex(
model_name="alertrule",
index=models.Index(
fields=["tenant", "is_active"], name="communicati_tenant__6d58f7_idx"
),
),
migrations.AddIndex(
model_name="alertrule",
index=models.Index(
fields=["trigger_type", "severity"],
name="communicati_trigger_0ab274_idx",
),
),
migrations.AddIndex(
model_name="alertrule",
index=models.Index(
fields=["last_evaluated_at"], name="communicati_last_ev_3529e2_idx"
),
),
migrations.AddIndex(
model_name="alertrule",
index=models.Index(
fields=["is_system_rule"], name="communicati_is_syst_52420d_idx"
),
),
migrations.AlterUniqueTogether(
name="alertrule",
unique_together={("tenant", "name")},
),
]

View File

@ -1,921 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Tenant",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"tenant_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique tenant identifier",
unique=True,
),
),
(
"name",
models.CharField(help_text="Organization name", max_length=200),
),
(
"display_name",
models.CharField(
help_text="Display name for the organization", max_length=200
),
),
(
"description",
models.TextField(
blank=True, help_text="Organization description", null=True
),
),
(
"organization_type",
models.CharField(
choices=[
("HOSPITAL", "Hospital"),
("CLINIC", "Clinic"),
("HEALTH_SYSTEM", "Health System"),
("AMBULATORY", "Ambulatory Care"),
("SPECIALTY", "Specialty Practice"),
("URGENT_CARE", "Urgent Care"),
("REHABILITATION", "Rehabilitation Center"),
("LONG_TERM_CARE", "Long-term Care"),
],
default="HOSPITAL",
max_length=50,
),
),
(
"address_line1",
models.CharField(help_text="Address line 1", max_length=200),
),
(
"address_line2",
models.CharField(
blank=True,
help_text="Address line 2",
max_length=200,
null=True,
),
),
("city", models.CharField(help_text="City", max_length=100)),
(
"state",
models.CharField(help_text="State or province", max_length=100),
),
(
"postal_code",
models.CharField(help_text="Postal code", max_length=20),
),
(
"country",
models.CharField(
default="Saudi Arabia", help_text="Country", max_length=100
),
),
(
"phone_number",
models.CharField(
help_text="Primary phone number",
max_length=20,
validators=[
django.core.validators.RegexValidator(
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.',
regex="^\\+?1?\\d{9,15}$",
)
],
),
),
(
"email",
models.EmailField(
help_text="Primary email address", max_length=254
),
),
(
"website",
models.URLField(
blank=True, help_text="Organization website", null=True
),
),
(
"license_number",
models.CharField(
blank=True,
help_text="Healthcare license number",
max_length=100,
null=True,
),
),
(
"accreditation_body",
models.CharField(
blank=True,
help_text="Accreditation body (e.g., Joint Commission)",
max_length=100,
null=True,
),
),
(
"accreditation_number",
models.CharField(
blank=True,
help_text="Accreditation number",
max_length=100,
null=True,
),
),
(
"accreditation_expiry",
models.DateField(
blank=True, help_text="Accreditation expiry date", null=True
),
),
(
"timezone",
models.CharField(
default="UTC", help_text="Organization timezone", max_length=50
),
),
(
"locale",
models.CharField(
default="en-US", help_text="Organization locale", max_length=10
),
),
(
"currency",
models.CharField(
default="SAR",
help_text="Organization currency code",
max_length=3,
),
),
(
"subscription_plan",
models.CharField(
choices=[
("BASIC", "Basic"),
("STANDARD", "Standard"),
("PREMIUM", "Premium"),
("ENTERPRISE", "Enterprise"),
],
default="BASIC",
max_length=50,
),
),
(
"max_users",
models.PositiveIntegerField(
default=50, help_text="Maximum number of users allowed"
),
),
(
"max_patients",
models.PositiveIntegerField(
default=1000, help_text="Maximum number of patients allowed"
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Tenant is active"),
),
(
"is_trial",
models.BooleanField(default=False, help_text="Tenant is on trial"),
),
(
"trial_expires_at",
models.DateTimeField(
blank=True, help_text="Trial expiration date", null=True
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"verbose_name": "Tenant",
"verbose_name_plural": "Tenants",
"db_table": "core_tenant",
"ordering": ["name"],
},
),
migrations.CreateModel(
name="SystemNotification",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"notification_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique notification identifier",
unique=True,
),
),
(
"title",
models.CharField(help_text="Notification title", max_length=200),
),
("message", models.TextField(help_text="Notification message")),
(
"notification_type",
models.CharField(
choices=[
("INFO", "Information"),
("WARNING", "Warning"),
("ERROR", "Error"),
("SUCCESS", "Success"),
("MAINTENANCE", "Maintenance"),
("SECURITY", "Security Alert"),
("FEATURE", "New Feature"),
("UPDATE", "System Update"),
],
max_length=30,
),
),
(
"priority",
models.CharField(
choices=[
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
("URGENT", "Urgent"),
],
default="MEDIUM",
max_length=20,
),
),
(
"target_audience",
models.CharField(
choices=[
("ALL_USERS", "All Users"),
("ADMINISTRATORS", "Administrators"),
("CLINICAL_STAFF", "Clinical Staff"),
("SUPPORT_STAFF", "Support Staff"),
("SPECIFIC_ROLES", "Specific Roles"),
("SPECIFIC_USERS", "Specific Users"),
],
default="ALL_USERS",
max_length=30,
),
),
(
"target_roles",
models.JSONField(
default=list,
help_text="Target user roles (if target_audience is SPECIFIC_ROLES)",
),
),
(
"is_dismissible",
models.BooleanField(
default=True, help_text="Users can dismiss this notification"
),
),
(
"auto_dismiss_after",
models.PositiveIntegerField(
blank=True, help_text="Auto-dismiss after X seconds", null=True
),
),
(
"show_on_login",
models.BooleanField(
default=False, help_text="Show notification on user login"
),
),
(
"start_date",
models.DateTimeField(
default=django.utils.timezone.now,
help_text="Notification start date",
),
),
(
"end_date",
models.DateTimeField(
blank=True, help_text="Notification end date", null=True
),
),
(
"action_url",
models.URLField(
blank=True,
help_text="Action URL for the notification",
null=True,
),
),
(
"action_text",
models.CharField(
blank=True,
help_text="Action button text",
max_length=100,
null=True,
),
),
(
"is_active",
models.BooleanField(
default=True, help_text="Notification is active"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_notifications",
to=settings.AUTH_USER_MODEL,
),
),
(
"target_users",
models.ManyToManyField(
blank=True,
related_name="targeted_notifications",
to=settings.AUTH_USER_MODEL,
),
),
(
"tenant",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to="core.tenant",
),
),
],
options={
"verbose_name": "System Notification",
"verbose_name_plural": "System Notifications",
"db_table": "core_system_notification",
"ordering": ["-priority", "-created_at"],
},
),
migrations.CreateModel(
name="SystemConfiguration",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"key",
models.CharField(help_text="Configuration key", max_length=200),
),
("value", models.TextField(help_text="Configuration value")),
(
"data_type",
models.CharField(
choices=[
("STRING", "String"),
("INTEGER", "Integer"),
("FLOAT", "Float"),
("BOOLEAN", "Boolean"),
("JSON", "JSON"),
("DATE", "Date"),
("DATETIME", "DateTime"),
],
default="STRING",
max_length=20,
),
),
(
"category",
models.CharField(
help_text="Configuration category", max_length=100
),
),
(
"description",
models.TextField(
blank=True, help_text="Configuration description", null=True
),
),
(
"validation_rules",
models.JSONField(
default=dict,
help_text="Validation rules for the configuration value",
),
),
(
"default_value",
models.TextField(blank=True, help_text="Default value", null=True),
),
(
"is_sensitive",
models.BooleanField(
default=False, help_text="Configuration contains sensitive data"
),
),
(
"is_encrypted",
models.BooleanField(
default=False, help_text="Configuration value is encrypted"
),
),
(
"required_permission",
models.CharField(
blank=True,
help_text="Permission required to modify this configuration",
max_length=100,
null=True,
),
),
(
"is_active",
models.BooleanField(
default=True, help_text="Configuration is active"
),
),
(
"is_readonly",
models.BooleanField(
default=False, help_text="Configuration is read-only"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"updated_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="updated_configurations",
to=settings.AUTH_USER_MODEL,
),
),
(
"tenant",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="configurations",
to="core.tenant",
),
),
],
options={
"verbose_name": "System Configuration",
"verbose_name_plural": "System Configurations",
"db_table": "core_system_configuration",
"ordering": ["category", "key"],
"unique_together": {("tenant", "key")},
},
),
migrations.CreateModel(
name="IntegrationLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"log_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique log identifier",
unique=True,
),
),
(
"integration_type",
models.CharField(
choices=[
("HL7", "HL7 Message"),
("DICOM", "DICOM Communication"),
("API", "API Call"),
("DATABASE", "Database Sync"),
("FILE_TRANSFER", "File Transfer"),
("WEBHOOK", "Webhook"),
("EMAIL", "Email"),
("SMS", "SMS"),
],
max_length=30,
),
),
(
"direction",
models.CharField(
choices=[("INBOUND", "Inbound"), ("OUTBOUND", "Outbound")],
max_length=10,
),
),
(
"external_system",
models.CharField(help_text="External system name", max_length=200),
),
(
"endpoint",
models.CharField(
blank=True,
help_text="Integration endpoint",
max_length=500,
null=True,
),
),
(
"message_type",
models.CharField(
blank=True,
help_text="Message type (e.g., HL7 message type)",
max_length=100,
null=True,
),
),
(
"message_id",
models.CharField(
blank=True,
help_text="Message identifier",
max_length=200,
null=True,
),
),
(
"correlation_id",
models.UUIDField(
blank=True,
help_text="Correlation ID for tracking related messages",
null=True,
),
),
(
"request_data",
models.TextField(
blank=True, help_text="Request data sent", null=True
),
),
(
"response_data",
models.TextField(
blank=True, help_text="Response data received", null=True
),
),
(
"status",
models.CharField(
choices=[
("SUCCESS", "Success"),
("FAILED", "Failed"),
("PENDING", "Pending"),
("TIMEOUT", "Timeout"),
("RETRY", "Retry"),
],
max_length=20,
),
),
(
"error_code",
models.CharField(
blank=True, help_text="Error code", max_length=50, null=True
),
),
(
"error_message",
models.TextField(blank=True, help_text="Error message", null=True),
),
(
"processing_time_ms",
models.PositiveIntegerField(
blank=True,
help_text="Processing time in milliseconds",
null=True,
),
),
(
"timestamp",
models.DateTimeField(
default=django.utils.timezone.now, help_text="Log timestamp"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="integration_logs",
to="core.tenant",
),
),
],
options={
"verbose_name": "Integration Log",
"verbose_name_plural": "Integration Logs",
"db_table": "core_integration_log",
"ordering": ["-timestamp"],
"indexes": [
models.Index(
fields=["tenant", "integration_type", "timestamp"],
name="core_integr_tenant__b44419_idx",
),
models.Index(
fields=["external_system", "status"],
name="core_integr_externa_11a6db_idx",
),
models.Index(
fields=["correlation_id"], name="core_integr_correla_d72107_idx"
),
],
},
),
migrations.CreateModel(
name="AuditLogEntry",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"log_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique log identifier",
unique=True,
),
),
(
"event_type",
models.CharField(
choices=[
("CREATE", "Create"),
("READ", "Read"),
("UPDATE", "Update"),
("DELETE", "Delete"),
("LOGIN", "Login"),
("LOGOUT", "Logout"),
("ACCESS", "Access"),
("EXPORT", "Export"),
("PRINT", "Print"),
("SHARE", "Share"),
("SYSTEM", "System Event"),
("ERROR", "Error"),
("SECURITY", "Security Event"),
],
max_length=50,
),
),
(
"event_category",
models.CharField(
choices=[
("AUTHENTICATION", "Authentication"),
("AUTHORIZATION", "Authorization"),
("DATA_ACCESS", "Data Access"),
("DATA_MODIFICATION", "Data Modification"),
("SYSTEM_ADMINISTRATION", "System Administration"),
("PATIENT_DATA", "Patient Data"),
("CLINICAL_DATA", "Clinical Data"),
("FINANCIAL_DATA", "Financial Data"),
("SECURITY", "Security"),
("INTEGRATION", "Integration"),
("REPORTING", "Reporting"),
],
max_length=50,
),
),
(
"user_email",
models.EmailField(
blank=True,
help_text="User email at time of event",
max_length=254,
null=True,
),
),
(
"user_role",
models.CharField(
blank=True,
help_text="User role at time of event",
max_length=50,
null=True,
),
),
(
"session_key",
models.CharField(
blank=True, help_text="Session key", max_length=40, null=True
),
),
(
"ip_address",
models.GenericIPAddressField(
blank=True, help_text="IP address", null=True
),
),
(
"user_agent",
models.TextField(
blank=True, help_text="User agent string", null=True
),
),
("object_id", models.PositiveIntegerField(blank=True, null=True)),
(
"object_repr",
models.CharField(
blank=True,
help_text="String representation of the object",
max_length=200,
null=True,
),
),
(
"action",
models.CharField(help_text="Action performed", max_length=200),
),
(
"description",
models.TextField(help_text="Detailed description of the event"),
),
(
"changes",
models.JSONField(
default=dict, help_text="Field changes (before/after values)"
),
),
(
"additional_data",
models.JSONField(default=dict, help_text="Additional event data"),
),
(
"patient_id",
models.CharField(
blank=True,
help_text="Patient identifier if applicable",
max_length=50,
null=True,
),
),
(
"patient_mrn",
models.CharField(
blank=True,
help_text="Patient MRN if applicable",
max_length=50,
null=True,
),
),
(
"risk_level",
models.CharField(
choices=[
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
("CRITICAL", "Critical"),
],
default="LOW",
max_length=20,
),
),
(
"hipaa_relevant",
models.BooleanField(
default=False, help_text="Event is HIPAA relevant"
),
),
(
"gdpr_relevant",
models.BooleanField(
default=False, help_text="Event is GDPR relevant"
),
),
(
"is_successful",
models.BooleanField(default=True, help_text="Event was successful"),
),
(
"error_message",
models.TextField(
blank=True, help_text="Error message if event failed", null=True
),
),
(
"timestamp",
models.DateTimeField(
default=django.utils.timezone.now, help_text="Event timestamp"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"content_type",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="contenttypes.contenttype",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="audit_logs",
to=settings.AUTH_USER_MODEL,
),
),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="audit_logs",
to="core.tenant",
),
),
],
options={
"verbose_name": "Audit Log Entry",
"verbose_name_plural": "Audit Log Entries",
"db_table": "core_audit_log_entry",
"ordering": ["-timestamp"],
"indexes": [
models.Index(
fields=["tenant", "event_type", "timestamp"],
name="core_audit__tenant__96449c_idx",
),
models.Index(
fields=["user", "timestamp"],
name="core_audit__user_id_4190d3_idx",
),
models.Index(
fields=["patient_mrn", "timestamp"],
name="core_audit__patient_9afecd_idx",
),
models.Index(
fields=["content_type", "object_id"],
name="core_audit__content_4866d0_idx",
),
models.Index(
fields=["risk_level", "timestamp"],
name="core_audit__risk_le_b2cf34_idx",
),
],
},
),
]

213
create_insurance_tables.py Normal file
View File

@ -0,0 +1,213 @@
"""
Directly create insurance_approvals tables using SQL.
"""
import sqlite3
import os
db_path = 'db.sqlite3'
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
print("Creating insurance_approvals tables directly...")
# Disable foreign key checks
cursor.execute("PRAGMA foreign_keys = OFF;")
# Create tables from migration SQL
tables_sql = [
# InsuranceApprovalRequest table
"""
CREATE TABLE IF NOT EXISTS "insurance_approvals_request" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"approval_id" char(32) NOT NULL UNIQUE,
"approval_number" varchar(30) NOT NULL UNIQUE,
"request_type" varchar(20) NOT NULL,
"service_description" varchar(500) NOT NULL,
"procedure_codes" text NOT NULL,
"diagnosis_codes" text NOT NULL,
"clinical_justification" text NOT NULL,
"medical_necessity" text NULL,
"alternative_treatments_tried" text NULL,
"requested_quantity" integer unsigned NOT NULL CHECK ("requested_quantity" >= 0),
"requested_visits" integer unsigned NULL CHECK ("requested_visits" >= 0),
"requested_units" integer unsigned NULL CHECK ("requested_units" >= 0),
"service_start_date" date NOT NULL,
"service_end_date" date NULL,
"status" varchar(30) NOT NULL,
"priority" varchar(20) NOT NULL,
"submission_method" varchar(20) NULL,
"submitted_date" datetime NULL,
"decision_date" datetime NULL,
"authorization_number" varchar(100) NULL,
"reference_number" varchar(100) NULL,
"approved_quantity" integer unsigned NULL CHECK ("approved_quantity" >= 0),
"approved_visits" integer unsigned NULL CHECK ("approved_visits" >= 0),
"approved_units" integer unsigned NULL CHECK ("approved_units" >= 0),
"approved_amount" decimal NULL,
"effective_date" date NULL,
"expiration_date" date NULL,
"denial_reason" text NULL,
"denial_code" varchar(50) NULL,
"appeal_date" datetime NULL,
"appeal_reason" text NULL,
"appeal_deadline" date NULL,
"last_contact_date" datetime NULL,
"last_contact_method" varchar(20) NULL,
"last_contact_notes" text NULL,
"is_urgent" bool NOT NULL,
"is_expedited" bool NOT NULL,
"requires_peer_review" bool NOT NULL,
"internal_notes" text NULL,
"insurance_notes" text NULL,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL,
"tenant_id" bigint NOT NULL REFERENCES "core_tenant" ("id") DEFERRABLE INITIALLY DEFERRED,
"patient_id" bigint NOT NULL REFERENCES "patients_patient_profile" ("id") DEFERRABLE INITIALLY DEFERRED,
"insurance_info_id" bigint NOT NULL REFERENCES "patients_insurance_info" ("id") DEFERRABLE INITIALLY DEFERRED,
"content_type_id" integer NOT NULL REFERENCES "django_content_type" ("id") DEFERRABLE INITIALLY DEFERRED,
"object_id" integer unsigned NOT NULL CHECK ("object_id" >= 0),
"submitted_by_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED,
"assigned_to_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED,
"requesting_provider_id" integer NOT NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED,
"created_by_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED
);
""",
# ApprovalDocument table
"""
CREATE TABLE IF NOT EXISTS "insurance_approvals_document" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"document_id" char(32) NOT NULL UNIQUE,
"document_type" varchar(30) NOT NULL,
"title" varchar(200) NOT NULL,
"description" text NULL,
"file" varchar(100) NOT NULL,
"file_size" integer unsigned NOT NULL CHECK ("file_size" >= 0),
"mime_type" varchar(100) NOT NULL,
"uploaded_at" datetime NOT NULL,
"approval_request_id" bigint NOT NULL REFERENCES "insurance_approvals_request" ("id") DEFERRABLE INITIALLY DEFERRED,
"uploaded_by_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED
);
""",
# ApprovalStatusHistory table
"""
CREATE TABLE IF NOT EXISTS "insurance_approvals_status_history" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"from_status" varchar(30) NULL,
"to_status" varchar(30) NOT NULL,
"reason" text NULL,
"notes" text NULL,
"changed_at" datetime NOT NULL,
"approval_request_id" bigint NOT NULL REFERENCES "insurance_approvals_request" ("id") DEFERRABLE INITIALLY DEFERRED,
"changed_by_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED
);
""",
# ApprovalCommunicationLog table
"""
CREATE TABLE IF NOT EXISTS "insurance_approvals_communication_log" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"communication_id" char(32) NOT NULL UNIQUE,
"communication_type" varchar(20) NOT NULL,
"contact_person" varchar(200) NULL,
"contact_number" varchar(50) NULL,
"subject" varchar(200) NOT NULL,
"message" text NOT NULL,
"response" text NULL,
"outcome" varchar(200) NULL,
"follow_up_required" bool NOT NULL,
"follow_up_date" date NULL,
"communicated_at" datetime NOT NULL,
"approval_request_id" bigint NOT NULL REFERENCES "insurance_approvals_request" ("id") DEFERRABLE INITIALLY DEFERRED,
"communicated_by_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED
);
""",
# ApprovalTemplate table
"""
CREATE TABLE IF NOT EXISTS "insurance_approvals_template" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"template_id" char(32) NOT NULL UNIQUE,
"name" varchar(200) NOT NULL,
"description" text NULL,
"request_type" varchar(20) NOT NULL,
"insurance_company" varchar(200) NULL,
"clinical_justification_template" text NOT NULL,
"medical_necessity_template" text NULL,
"required_documents" text NOT NULL,
"required_codes" text NOT NULL,
"is_active" bool NOT NULL,
"usage_count" integer unsigned NOT NULL CHECK ("usage_count" >= 0),
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL,
"tenant_id" bigint NOT NULL REFERENCES "core_tenant" ("id") DEFERRABLE INITIALLY DEFERRED,
"created_by_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED,
CONSTRAINT "insurance_approvals_template_tenant_id_name_uniq" UNIQUE ("tenant_id", "name")
);
"""
]
# Create tables
for i, sql in enumerate(tables_sql, 1):
try:
cursor.execute(sql)
print(f"✓ Created table {i}/5")
except sqlite3.OperationalError as e:
if "already exists" in str(e):
print(f"✓ Table {i}/5 already exists")
else:
print(f"✗ Error creating table {i}/5: {e}")
# Create indexes
indexes_sql = [
'CREATE INDEX IF NOT EXISTS "insurance_a_tenant__d6763e_idx" ON "insurance_approvals_request" ("tenant_id", "status");',
'CREATE INDEX IF NOT EXISTS "insurance_a_patient_00ddbd_idx" ON "insurance_approvals_request" ("patient_id", "status");',
'CREATE INDEX IF NOT EXISTS "insurance_a_insuran_438196_idx" ON "insurance_approvals_request" ("insurance_info_id", "status");',
'CREATE INDEX IF NOT EXISTS "insurance_a_approva_cefd6a_idx" ON "insurance_approvals_request" ("approval_number");',
'CREATE INDEX IF NOT EXISTS "insurance_a_authori_f05618_idx" ON "insurance_approvals_request" ("authorization_number");',
'CREATE INDEX IF NOT EXISTS "insurance_a_expirat_ef068e_idx" ON "insurance_approvals_request" ("expiration_date");',
'CREATE INDEX IF NOT EXISTS "insurance_a_assigne_19b007_idx" ON "insurance_approvals_request" ("assigned_to_id", "status");',
'CREATE INDEX IF NOT EXISTS "insurance_a_content_de7404_idx" ON "insurance_approvals_request" ("content_type_id", "object_id");',
'CREATE INDEX IF NOT EXISTS "insurance_a_priorit_0abcbd_idx" ON "insurance_approvals_request" ("priority", "status");',
'CREATE INDEX IF NOT EXISTS "insurance_a_approva_a5298b_idx" ON "insurance_approvals_status_history" ("approval_request_id", "changed_at");',
'CREATE INDEX IF NOT EXISTS "insurance_a_approva_eae9e0_idx" ON "insurance_approvals_document" ("approval_request_id", "document_type");',
'CREATE INDEX IF NOT EXISTS "insurance_a_approva_fa779d_idx" ON "insurance_approvals_communication_log" ("approval_request_id", "communicated_at");',
'CREATE INDEX IF NOT EXISTS "insurance_a_follow__2a3b0a_idx" ON "insurance_approvals_communication_log" ("follow_up_required", "follow_up_date");',
'CREATE INDEX IF NOT EXISTS "insurance_a_tenant__8f9c3a_idx" ON "insurance_approvals_template" ("tenant_id", "is_active");',
'CREATE INDEX IF NOT EXISTS "insurance_a_request_4b3e2f_idx" ON "insurance_approvals_template" ("request_type");',
]
print("\nCreating indexes...")
for i, sql in enumerate(indexes_sql, 1):
try:
cursor.execute(sql)
except sqlite3.OperationalError as e:
print(f" Index {i}: {e}")
print(f"✓ Created {len(indexes_sql)} indexes")
# Mark migration as applied
cursor.execute("""
INSERT OR IGNORE INTO django_migrations (app, name, applied)
VALUES ('insurance_approvals', '0001_initial', datetime('now'))
""")
conn.commit()
# Re-enable foreign key checks
cursor.execute("PRAGMA foreign_keys = ON;")
conn.close()
print("\n" + "="*60)
print("✓ Insurance Approvals tables created successfully!")
print("="*60)
print("\nTables created:")
print(" 1. insurance_approvals_request")
print(" 2. insurance_approvals_document")
print(" 3. insurance_approvals_status_history")
print(" 4. insurance_approvals_communication_log")
print(" 5. insurance_approvals_template")
print("\nModule is ready to use!")
print("Access admin at: http://127.0.0.1:8000/admin/insurance_approvals/")

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,657 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("appointments", "0002_initial"),
("core", "0001_initial"),
("emr", "0001_initial"),
("inpatients", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="encounter",
name="admission",
field=models.ForeignKey(
blank=True,
help_text="Related admission",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="encounters",
to="inpatients.admission",
),
),
migrations.AddField(
model_name="encounter",
name="appointment",
field=models.ForeignKey(
blank=True,
help_text="Related appointment",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="encounters",
to="appointments.appointmentrequest",
),
),
migrations.AddField(
model_name="encounter",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the encounter",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_encounters",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="encounter",
name="patient",
field=models.ForeignKey(
help_text="Patient for this encounter",
on_delete=django.db.models.deletion.CASCADE,
related_name="encounters",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="encounter",
name="provider",
field=models.ForeignKey(
help_text="Primary provider for this encounter",
on_delete=django.db.models.deletion.CASCADE,
related_name="encounters",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="encounter",
name="signed_by",
field=models.ForeignKey(
blank=True,
help_text="Provider who signed off",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="signed_encounters",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="encounter",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="encounters",
to="core.tenant",
),
),
migrations.AddField(
model_name="criticalalert",
name="related_encounter",
field=models.ForeignKey(
blank=True,
help_text="Related encounter",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="critical_alerts",
to="emr.encounter",
),
),
migrations.AddField(
model_name="clinicalrecommendation",
name="related_encounter",
field=models.ForeignKey(
blank=True,
help_text="Related encounter",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="recommendations",
to="emr.encounter",
),
),
migrations.AddField(
model_name="clinicalnote",
name="encounter",
field=models.ForeignKey(
help_text="Associated encounter",
on_delete=django.db.models.deletion.CASCADE,
related_name="clinical_notes",
to="emr.encounter",
),
),
migrations.AddField(
model_name="icd10",
name="parent",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="children",
to="emr.icd10",
),
),
migrations.AddField(
model_name="notetemplate",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the template",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_note_templates",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="notetemplate",
name="previous_version",
field=models.ForeignKey(
blank=True,
help_text="Previous version of this template",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="newer_versions",
to="emr.notetemplate",
),
),
migrations.AddField(
model_name="notetemplate",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="note_templates",
to="core.tenant",
),
),
migrations.AddField(
model_name="clinicalnote",
name="template",
field=models.ForeignKey(
blank=True,
help_text="Template used for this note",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="notes",
to="emr.notetemplate",
),
),
migrations.AddField(
model_name="problemlist",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the problem",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_problems",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="problemlist",
name="diagnosing_provider",
field=models.ForeignKey(
help_text="Provider who diagnosed the problem",
on_delete=django.db.models.deletion.CASCADE,
related_name="diagnosed_problems",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="problemlist",
name="managing_provider",
field=models.ForeignKey(
blank=True,
help_text="Provider managing the problem",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="managed_problems",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="problemlist",
name="patient",
field=models.ForeignKey(
help_text="Patient",
on_delete=django.db.models.deletion.CASCADE,
related_name="problems",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="problemlist",
name="related_encounter",
field=models.ForeignKey(
blank=True,
help_text="Encounter where problem was identified",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="problems_identified",
to="emr.encounter",
),
),
migrations.AddField(
model_name="problemlist",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="problem_lists",
to="core.tenant",
),
),
migrations.AddField(
model_name="problemlist",
name="verified_by",
field=models.ForeignKey(
blank=True,
help_text="Provider who verified the problem",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="verified_problems",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="clinicalrecommendation",
name="related_problems",
field=models.ManyToManyField(
blank=True,
help_text="Related problems",
related_name="recommendations",
to="emr.problemlist",
),
),
migrations.AddField(
model_name="clinicalnote",
name="related_problems",
field=models.ManyToManyField(
blank=True,
help_text="Related problems",
related_name="related_clinical_notes",
to="emr.problemlist",
),
),
migrations.AddField(
model_name="careplan",
name="related_problems",
field=models.ManyToManyField(
blank=True,
help_text="Related problems addressed by this plan",
related_name="care_plans",
to="emr.problemlist",
),
),
migrations.AddField(
model_name="treatmentprotocol",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the protocol",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_protocols",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="treatmentprotocol",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="treatment_protocols",
to="core.tenant",
),
),
migrations.AddField(
model_name="vitalsigns",
name="encounter",
field=models.ForeignKey(
help_text="Associated encounter",
on_delete=django.db.models.deletion.CASCADE,
related_name="vital_signs",
to="emr.encounter",
),
),
migrations.AddField(
model_name="vitalsigns",
name="measured_by",
field=models.ForeignKey(
help_text="Staff member who took measurements",
on_delete=django.db.models.deletion.CASCADE,
related_name="vital_signs_measurements",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="vitalsigns",
name="patient",
field=models.ForeignKey(
help_text="Patient",
on_delete=django.db.models.deletion.CASCADE,
related_name="vital_signs",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="vitalsigns",
name="verified_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who verified measurements",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="verified_vital_signs",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddIndex(
model_name="allergyalert",
index=models.Index(
fields=["tenant", "resolved"], name="emr_allergy_tenant__da9219_idx"
),
),
migrations.AddIndex(
model_name="allergyalert",
index=models.Index(
fields=["patient", "resolved"], name="emr_allergy_patient_674c53_idx"
),
),
migrations.AddIndex(
model_name="allergyalert",
index=models.Index(
fields=["severity"], name="emr_allergy_severit_38d8dd_idx"
),
),
migrations.AddIndex(
model_name="allergyalert",
index=models.Index(
fields=["detected_at"], name="emr_allergy_detecte_97c184_idx"
),
),
migrations.AddIndex(
model_name="clinicalguideline",
index=models.Index(
fields=["tenant", "is_active"], name="emr_clinica_tenant__08a1f7_idx"
),
),
migrations.AddIndex(
model_name="clinicalguideline",
index=models.Index(
fields=["organization"], name="emr_clinica_organiz_107f8d_idx"
),
),
migrations.AddIndex(
model_name="clinicalguideline",
index=models.Index(
fields=["publication_date"], name="emr_clinica_publica_4e35d0_idx"
),
),
migrations.AddIndex(
model_name="diagnosticsuggestion",
index=models.Index(
fields=["tenant", "status"], name="emr_diagnos_tenant__77e6f8_idx"
),
),
migrations.AddIndex(
model_name="diagnosticsuggestion",
index=models.Index(
fields=["patient", "status"], name="emr_diagnos_patient_8c5470_idx"
),
),
migrations.AddIndex(
model_name="diagnosticsuggestion",
index=models.Index(
fields=["confidence"], name="emr_diagnos_confide_f7447d_idx"
),
),
migrations.AddIndex(
model_name="diagnosticsuggestion",
index=models.Index(
fields=["created_at"], name="emr_diagnos_created_34cc79_idx"
),
),
migrations.AddIndex(
model_name="encounter",
index=models.Index(
fields=["tenant", "status"], name="emr_encount_tenant__6734f5_idx"
),
),
migrations.AddIndex(
model_name="encounter",
index=models.Index(
fields=["patient", "start_datetime"],
name="emr_encount_patient_c0f443_idx",
),
),
migrations.AddIndex(
model_name="encounter",
index=models.Index(
fields=["provider"], name="emr_encount_provide_5e06b3_idx"
),
),
migrations.AddIndex(
model_name="encounter",
index=models.Index(
fields=["encounter_type"], name="emr_encount_encount_019f80_idx"
),
),
migrations.AddIndex(
model_name="encounter",
index=models.Index(
fields=["start_datetime"], name="emr_encount_start_d_a01018_idx"
),
),
migrations.AddIndex(
model_name="criticalalert",
index=models.Index(
fields=["tenant", "acknowledged"], name="emr_critica_tenant__a7de09_idx"
),
),
migrations.AddIndex(
model_name="criticalalert",
index=models.Index(
fields=["patient", "acknowledged"],
name="emr_critica_patient_3f3d88_idx",
),
),
migrations.AddIndex(
model_name="criticalalert",
index=models.Index(
fields=["priority"], name="emr_critica_priorit_06ad08_idx"
),
),
migrations.AddIndex(
model_name="criticalalert",
index=models.Index(
fields=["created_at"], name="emr_critica_created_3acbe1_idx"
),
),
migrations.AddIndex(
model_name="notetemplate",
index=models.Index(
fields=["tenant", "is_active"], name="emr_note_te_tenant__caa5d3_idx"
),
),
migrations.AddIndex(
model_name="notetemplate",
index=models.Index(
fields=["note_type", "specialty"], name="emr_note_te_note_ty_d18594_idx"
),
),
migrations.AddIndex(
model_name="notetemplate",
index=models.Index(
fields=["is_default"], name="emr_note_te_is_defa_7b5223_idx"
),
),
migrations.AlterUniqueTogether(
name="notetemplate",
unique_together={("tenant", "note_type", "specialty", "is_default")},
),
migrations.AddIndex(
model_name="problemlist",
index=models.Index(
fields=["tenant", "status"], name="emr_problem_tenant__bb0abf_idx"
),
),
migrations.AddIndex(
model_name="problemlist",
index=models.Index(
fields=["patient", "status"], name="emr_problem_patient_d732d2_idx"
),
),
migrations.AddIndex(
model_name="problemlist",
index=models.Index(
fields=["problem_type"], name="emr_problem_problem_90c9f8_idx"
),
),
migrations.AddIndex(
model_name="problemlist",
index=models.Index(
fields=["priority"], name="emr_problem_priorit_327dd3_idx"
),
),
migrations.AddIndex(
model_name="problemlist",
index=models.Index(
fields=["onset_date"], name="emr_problem_onset_d_de94bd_idx"
),
),
migrations.AddIndex(
model_name="clinicalrecommendation",
index=models.Index(
fields=["tenant", "status"], name="emr_clinica_tenant__9ac4a3_idx"
),
),
migrations.AddIndex(
model_name="clinicalrecommendation",
index=models.Index(
fields=["patient", "status"], name="emr_clinica_patient_6a41b7_idx"
),
),
migrations.AddIndex(
model_name="clinicalrecommendation",
index=models.Index(
fields=["category"], name="emr_clinica_categor_44bb6e_idx"
),
),
migrations.AddIndex(
model_name="clinicalrecommendation",
index=models.Index(
fields=["priority"], name="emr_clinica_priorit_d52001_idx"
),
),
migrations.AddIndex(
model_name="clinicalrecommendation",
index=models.Index(
fields=["created_at"], name="emr_clinica_created_d816a2_idx"
),
),
migrations.AddIndex(
model_name="clinicalnote",
index=models.Index(
fields=["patient", "note_datetime"],
name="emr_clinica_patient_442718_idx",
),
),
migrations.AddIndex(
model_name="clinicalnote",
index=models.Index(
fields=["encounter"], name="emr_clinica_encount_751749_idx"
),
),
migrations.AddIndex(
model_name="clinicalnote",
index=models.Index(
fields=["author"], name="emr_clinica_author__85ec13_idx"
),
),
migrations.AddIndex(
model_name="clinicalnote",
index=models.Index(
fields=["note_type"], name="emr_clinica_note_ty_e6c13c_idx"
),
),
migrations.AddIndex(
model_name="clinicalnote",
index=models.Index(fields=["status"], name="emr_clinica_status_0ba513_idx"),
),
migrations.AddIndex(
model_name="careplan",
index=models.Index(
fields=["tenant", "status"], name="emr_care_pl_tenant__46659b_idx"
),
),
migrations.AddIndex(
model_name="careplan",
index=models.Index(
fields=["patient", "status"], name="emr_care_pl_patient_a85a8d_idx"
),
),
migrations.AddIndex(
model_name="careplan",
index=models.Index(
fields=["primary_provider"], name="emr_care_pl_primary_7b0b7d_idx"
),
),
migrations.AddIndex(
model_name="careplan",
index=models.Index(
fields=["start_date", "end_date"], name="emr_care_pl_start_d_1183e0_idx"
),
),
migrations.AddIndex(
model_name="careplan",
index=models.Index(
fields=["priority"], name="emr_care_pl_priorit_0a41d3_idx"
),
),
migrations.AddIndex(
model_name="treatmentprotocol",
index=models.Index(
fields=["tenant", "is_active"], name="emr_treatme_tenant__1f3aaa_idx"
),
),
migrations.AddIndex(
model_name="treatmentprotocol",
index=models.Index(
fields=["success_rate"], name="emr_treatme_success_d8024a_idx"
),
),
migrations.AddIndex(
model_name="vitalsigns",
index=models.Index(
fields=["patient", "measured_datetime"],
name="emr_vital_s_patient_0fc206_idx",
),
),
migrations.AddIndex(
model_name="vitalsigns",
index=models.Index(
fields=["encounter"], name="emr_vital_s_encount_6b9829_idx"
),
),
migrations.AddIndex(
model_name="vitalsigns",
index=models.Index(
fields=["measured_datetime"], name="emr_vital_s_measure_8badac_idx"
),
),
]

View File

@ -1,36 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-28 13:55
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0001_initial"),
("emr", "0002_initial"),
]
operations = [
migrations.AddField(
model_name="icd10",
name="tenant",
field=models.ForeignKey(
default=1,
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="icd10_codes",
to="core.tenant",
),
preserve_default=False,
),
migrations.AlterField(
model_name="icd10",
name="code",
field=models.CharField(db_index=True, max_length=10),
),
migrations.AlterUniqueTogether(
name="icd10",
unique_together={("tenant", "code")},
),
]

View File

@ -1,33 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-29 13:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("emr", "0003_icd10_tenant_alter_icd10_code_and_more"),
]
operations = [
migrations.AlterField(
model_name="encounter",
name="status",
field=models.CharField(
choices=[
("PLANNED", "Planned"),
("ARRIVED", "Arrived"),
("TRIAGED", "Triaged"),
("IN_PROGRESS", "In Progress"),
("ON_HOLD", "On Hold"),
("COMPLETED", "Completed"),
("CANCELLED", "Cancelled"),
("ENTERED_IN_ERROR", "Entered in Error"),
("UNKNOWN", "Unknown"),
],
default="PLANNED",
help_text="Current encounter status",
max_length=20,
),
),
]

View File

@ -29,7 +29,7 @@
<div class="row">
<div class="col-xl-8">
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel panel-inverse" data-sortable-id="encounter-1">
<div class="panel-heading">
<h4 class="panel-title">Encounter Information</h4>
<div class="panel-heading-btn">
@ -204,7 +204,7 @@
<!-- END panel -->
<!-- BEGIN panel with tabs -->
<div class="panel panel-inverse">
<div class="panel panel-inverse" data-sortable-id="encounter-2">
<div class="panel-heading">
<h4 class="panel-title">Encounter Details</h4>
<div class="panel-heading-btn">
@ -247,6 +247,18 @@
<span class="d-sm-block d-none"><i class="fas fa-x-ray me-1"></i> Radiology</span>
</a>
</li>
<li class="nav-item">
<a href="#problems-tab" data-bs-toggle="tab" class="nav-link">
<span class="d-sm-none"><i class="fas fa-list"></i></span>
<span class="d-sm-block d-none"><i class="fas fa-list me-1"></i> Problems</span>
</a>
</li>
<li class="nav-item">
<a href="#care-plans-tab" data-bs-toggle="tab" class="nav-link">
<span class="d-sm-none"><i class="fas fa-clipboard-list"></i></span>
<span class="d-sm-block d-none"><i class="fas fa-clipboard-list me-1"></i> Care Plans</span>
</a>
</li>
<li class="nav-item">
<a href="#billing-tab" data-bs-toggle="tab" class="nav-link">
<span class="d-sm-none"><i class="fas fa-file-invoice-dollar"></i></span>
@ -659,6 +671,164 @@
</div>
<!-- END radiology-tab -->
<!-- BEGIN problems-tab -->
<div class="tab-pane fade" id="problems-tab">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="m-0">Problems Identified</h5>
<button class="btn btn-sm btn-primary"
hx-get="{% url 'emr:add_problem' object.id %}"
hx-target="#problem-modal .modal-content"
data-bs-toggle="modal"
data-bs-target="#problem-modal">
<i class="fa fa-plus me-1"></i> Add Problem
</button>
</div>
{% if problems %}
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Problem</th>
<th>Type</th>
<th>Priority</th>
<th>Status</th>
<th>Onset Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for problem in problems %}
<tr>
<td>
<strong>{{ problem.problem_name }}</strong>
{% if problem.problem_code %}
<br><small class="text-muted">{{ problem.problem_code }}</small>
{% endif %}
</td>
<td>{{ problem.get_problem_type_display }}</td>
<td>
<span class="badge bg-{% if problem.priority == 'HIGH' or problem.priority == 'URGENT' %}danger{% elif problem.priority == 'MEDIUM' %}warning{% else %}info{% endif %} fs-10px">
{{ problem.get_priority_display }}
</span>
</td>
<td>
<span class="badge bg-{% if problem.status == 'ACTIVE' %}success{% elif problem.status == 'RESOLVED' %}secondary{% else %}info{% endif %} fs-10px">
{{ problem.get_status_display }}
</span>
</td>
<td>
{% if problem.onset_date %}
{{ problem.onset_date|date:"M d, Y" }}
{% else %}
<span class="text-muted">Not specified</span>
{% endif %}
</td>
<td>
<a href="{% url 'emr:problem_detail' problem.pk %}" class="btn btn-xs btn-outline-primary">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4 text-muted">
<i class="fa fa-list fa-3x mb-3"></i>
<p>No problems identified for this encounter.</p>
<button class="btn btn-primary"
hx-get="{% url 'emr:add_problem' object.id %}"
hx-target="#problem-modal .modal-content"
data-bs-toggle="modal"
data-bs-target="#problem-modal">
<i class="fa fa-plus me-2"></i>Add Problem
</button>
</div>
{% endif %}
</div>
<!-- END problems-tab -->
<!-- BEGIN care-plans-tab -->
<div class="tab-pane fade" id="care-plans-tab">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="m-0">Care Plans</h5>
<button class="btn btn-sm btn-primary"
hx-get="{% url 'emr:add_care_plan' object.id %}"
hx-target="#care-plan-modal .modal-content"
data-bs-toggle="modal"
data-bs-target="#care-plan-modal">
<i class="fa fa-plus me-1"></i> Add Care Plan
</button>
</div>
{% if object.patient.care_plans.exists %}
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Title</th>
<th>Type</th>
<th>Status</th>
<th>Priority</th>
<th>Start Date</th>
<th>Progress</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for care_plan in object.patient.care_plans.all %}
<tr>
<td><strong>{{ care_plan.title }}</strong></td>
<td>{{ care_plan.get_plan_type_display }}</td>
<td>
<span class="badge bg-{% if care_plan.status == 'ACTIVE' %}success{% elif care_plan.status == 'COMPLETED' %}primary{% elif care_plan.status == 'DRAFT' %}warning{% else %}secondary{% endif %} fs-10px">
{{ care_plan.get_status_display }}
</span>
</td>
<td>
<span class="badge bg-{% if care_plan.priority == 'STAT' or care_plan.priority == 'URGENT' %}danger{% elif care_plan.priority == 'ROUTINE' %}info{% else %}secondary{% endif %} fs-10px">
{{ care_plan.get_priority_display }}
</span>
</td>
<td>{{ care_plan.start_date|date:"M d, Y" }}</td>
<td>
<div class="progress" style="height: 20px;">
<div class="progress-bar" role="progressbar"
style="width: {{ care_plan.completion_percentage }}%;"
aria-valuenow="{{ care_plan.completion_percentage }}"
aria-valuemin="0" aria-valuemax="100">
{{ care_plan.completion_percentage }}%
</div>
</div>
</td>
<td>
<a href="{% url 'emr:care_plan_detail' care_plan.pk %}" class="btn btn-xs btn-outline-primary">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4 text-muted">
<i class="fa fa-clipboard-list fa-3x mb-3"></i>
<p>No care plans for this patient.</p>
<button class="btn btn-primary"
hx-get="{% url 'emr:add_care_plan' object.id %}"
hx-target="#care-plan-modal .modal-content"
data-bs-toggle="modal"
data-bs-target="#care-plan-modal">
<i class="fa fa-plus me-2"></i>Add Care Plan
</button>
</div>
{% endif %}
</div>
<!-- END care-plans-tab -->
<!-- BEGIN billing-tab -->
<div class="tab-pane fade" id="billing-tab">
<div class="d-flex justify-content-between align-items-center mb-3">
@ -725,7 +895,7 @@
<div class="col-xl-4">
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel panel-inverse" data-sortable-id="encounter-3">
<div class="panel-heading">
<h4 class="panel-title">Quick Actions</h4>
<div class="panel-heading-btn">
@ -767,6 +937,22 @@
<i class="fa fa-file-medical me-2"></i>Add Clinical Note
</a>
<button class="btn btn-outline-success"
hx-get="{% url 'emr:add_problem' object.id %}"
hx-target="#problem-modal .modal-content"
data-bs-toggle="modal"
data-bs-target="#problem-modal">
<i class="fa fa-plus me-2"></i>Add Problem
</button>
<button class="btn btn-outline-info"
hx-get="{% url 'emr:add_care_plan' object.id %}"
hx-target="#care-plan-modal .modal-content"
data-bs-toggle="modal"
data-bs-target="#care-plan-modal">
<i class="fa fa-clipboard-list me-2"></i>Add Care Plan
</button>
<button class="btn btn-outline-info" onclick="printEncounter()">
<i class="fa fa-print me-2"></i>Print Encounter
</button>
@ -776,7 +962,7 @@
<!-- END panel -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel panel-inverse" data-sortable-id="encounter-4">
<div class="panel-heading">
<h4 class="panel-title">Patient Information</h4>
<div class="panel-heading-btn">
@ -830,7 +1016,7 @@
<!-- END panel -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel panel-inverse" data-sortable-id="encounter-5">
<div class="panel-heading">
<h4 class="panel-title">Related Information</h4>
<div class="panel-heading-btn">
@ -878,7 +1064,7 @@
<!-- END panel -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel panel-inverse" data-sortable-id="encounter-6">
<div class="panel-heading">
<h4 class="panel-title">Encounter Timeline</h4>
<div class="panel-heading-btn">
@ -938,6 +1124,24 @@
</div>
</div>
</div>
<!-- Problem Modal -->
<div class="modal fade" id="problem-modal" tabindex="-1" aria-labelledby="problemModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<!-- Content loaded via HTMX -->
</div>
</div>
</div>
<!-- Care Plan Modal -->
<div class="modal fade" id="care-plan-modal" tabindex="-1" aria-labelledby="carePlanModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<!-- Content loaded via HTMX -->
</div>
</div>
</div>
{% endblock %}
{% block js %}
@ -945,6 +1149,8 @@
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'plugins/lity/dist/lity.min.js' %}"></script>
<script src="{% static 'plugins/moment/moment.js' %}"></script>
<script src="{% static 'plugins/toastr/toastr.min.js' %}"
<script>
function updateStatus(newStatus) {
if (confirm('Are you sure you want to update the encounter status?')) {

View File

@ -0,0 +1,163 @@
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-clipboard-list me-2"></i>Add Care Plan</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{% url 'emr:add_care_plan' encounter.id %}">
{% csrf_token %}
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
{% if form.errors %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>Please correct the following errors:</strong>
<ul class="mb-0">
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li>{{ field|title }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-info">
<strong>Patient:</strong> {{ patient.get_full_name }} (MRN: {{ patient.mrn }})
</div>
</div>
</div>
<!-- Title -->
<div class="row mb-3">
<div class="col-12">
<label for="{{ form.title.id_for_label }}" class="form-label">Care Plan Title <span class="text-danger">*</span></label>
{{ form.title }}
</div>
</div>
<!-- Plan Type and Category -->
<div class="row mb-3">
<div class="col-md-6">
<label for="{{ form.plan_type.id_for_label }}" class="form-label">Plan Type <span class="text-danger">*</span></label>
{{ form.plan_type }}
</div>
<div class="col-md-6">
<label for="{{ form.category.id_for_label }}" class="form-label">Category <span class="text-danger">*</span></label>
{{ form.category }}
</div>
</div>
<!-- Status and Priority -->
<div class="row mb-3">
<div class="col-md-6">
<label for="{{ form.status.id_for_label }}" class="form-label">Status <span class="text-danger">*</span></label>
{{ form.status }}
</div>
<div class="col-md-6">
<label for="{{ form.priority.id_for_label }}" class="form-label">Priority <span class="text-danger">*</span></label>
{{ form.priority }}
</div>
</div>
<!-- Dates -->
<div class="row mb-3">
<div class="col-md-4">
<label for="{{ form.start_date.id_for_label }}" class="form-label">Start Date <span class="text-danger">*</span></label>
{{ form.start_date }}
</div>
<div class="col-md-4">
<label for="{{ form.end_date.id_for_label }}" class="form-label">End Date</label>
{{ form.end_date }}
</div>
<div class="col-md-4">
<label for="{{ form.target_completion_date.id_for_label }}" class="form-label">Target Completion</label>
{{ form.target_completion_date }}
</div>
</div>
<!-- Description -->
<div class="row mb-3">
<div class="col-12">
<label for="{{ form.description.id_for_label }}" class="form-label">Description <span class="text-danger">*</span></label>
{{ form.description }}
</div>
</div>
<!-- Goals -->
<div class="row mb-3">
<div class="col-12">
<label for="{{ form.goals.id_for_label }}" class="form-label">Goals</label>
{{ form.goals }}
<small class="form-text text-muted">Enter goals as JSON array, e.g., ["Goal 1", "Goal 2"]</small>
</div>
</div>
<!-- Interventions -->
<div class="row mb-3">
<div class="col-12">
<label for="{{ form.interventions.id_for_label }}" class="form-label">Interventions</label>
{{ form.interventions }}
<small class="form-text text-muted">Enter interventions as JSON array</small>
</div>
</div>
<!-- Monitoring Parameters -->
<div class="row mb-3">
<div class="col-12">
<label for="{{ form.monitoring_parameters.id_for_label }}" class="form-label">Monitoring Parameters</label>
{{ form.monitoring_parameters }}
<small class="form-text text-muted">Enter monitoring parameters as JSON array</small>
</div>
</div>
<!-- Patient Goals and Preferences -->
<div class="row mb-3">
<div class="col-md-6">
<label for="{{ form.patient_goals.id_for_label }}" class="form-label">Patient Goals</label>
{{ form.patient_goals }}
</div>
<div class="col-md-6">
<label for="{{ form.patient_preferences.id_for_label }}" class="form-label">Patient Preferences</label>
{{ form.patient_preferences }}
</div>
</div>
<!-- Related Problems -->
<div class="row mb-3">
<div class="col-12">
<label for="{{ form.related_problems.id_for_label }}" class="form-label">Related Problems</label>
{{ form.related_problems }}
<small class="form-text text-muted">Hold Ctrl/Cmd to select multiple problems</small>
</div>
</div>
<!-- Hidden Fields -->
{{ form.patient.as_hidden }}
{{ form.primary_provider.as_hidden }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>Cancel
</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Create Care Plan
</button>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize JSON fields with empty arrays if they're empty
const jsonFields = ['{{ form.goals.id_for_label }}', '{{ form.interventions.id_for_label }}', '{{ form.monitoring_parameters.id_for_label }}'];
jsonFields.forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field && !field.value.trim()) {
field.value = '[]';
}
});
});
</script>

View File

@ -0,0 +1,183 @@
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-plus-circle me-2"></i>Add Problem</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{% url 'emr:add_problem' encounter.id %}">
{% csrf_token %}
<div class="modal-body">
{% if form.errors %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>Please correct the following errors:</strong>
<ul class="mb-0">
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li>{{ field|title }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-info">
<strong>Patient:</strong> {{ patient.get_full_name }} (MRN: {{ patient.mrn }})
</div>
</div>
</div>
<!-- ICD-10 Search -->
<div class="row mb-3">
<div class="col-12">
<label for="icd10-search" class="form-label">Search ICD-10 Diagnosis</label>
<input type="text"
class="form-control"
id="icd10-search"
placeholder="Type to search ICD-10 codes..."
autocomplete="off">
<div id="icd10-results" class="list-group mt-2" style="max-height: 200px; overflow-y: auto; display: none;"></div>
</div>
</div>
<!-- Problem Name and Code -->
<div class="row mb-3">
<div class="col-md-8">
<label for="{{ form.problem_name.id_for_label }}" class="form-label">Problem Name <span class="text-danger">*</span></label>
{{ form.problem_name }}
</div>
<div class="col-md-4">
<label for="{{ form.problem_code.id_for_label }}" class="form-label">ICD-10 Code</label>
{{ form.problem_code }}
</div>
</div>
<!-- Problem Type and Status -->
<div class="row mb-3">
<div class="col-md-6">
<label for="{{ form.problem_type.id_for_label }}" class="form-label">Problem Type <span class="text-danger">*</span></label>
{{ form.problem_type }}
</div>
<div class="col-md-6">
<label for="{{ form.status.id_for_label }}" class="form-label">Status <span class="text-danger">*</span></label>
{{ form.status }}
</div>
</div>
<!-- Severity and Priority -->
<div class="row mb-3">
<div class="col-md-6">
<label for="{{ form.severity.id_for_label }}" class="form-label">Severity</label>
{{ form.severity }}
</div>
<div class="col-md-6">
<label for="{{ form.priority.id_for_label }}" class="form-label">Priority <span class="text-danger">*</span></label>
{{ form.priority }}
</div>
</div>
<!-- Onset Date and Body Site -->
<div class="row mb-3">
<div class="col-md-6">
<label for="{{ form.onset_date.id_for_label }}" class="form-label">Onset Date</label>
{{ form.onset_date }}
</div>
<div class="col-md-6">
<label for="{{ form.body_site.id_for_label }}" class="form-label">Body Site</label>
{{ form.body_site }}
</div>
</div>
<!-- Clinical Notes -->
<div class="row mb-3">
<div class="col-12">
<label for="{{ form.clinical_notes.id_for_label }}" class="form-label">Clinical Notes</label>
{{ form.clinical_notes }}
</div>
</div>
<!-- Hidden Fields -->
{{ form.patient.as_hidden }}
{{ form.diagnosing_provider.as_hidden }}
{{ form.related_encounter.as_hidden }}
{{ form.coding_system.as_hidden }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>Cancel
</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Add Problem
</button>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('icd10-search');
const resultsDiv = document.getElementById('icd10-results');
const problemNameInput = document.getElementById('{{ form.problem_name.id_for_label }}');
const problemCodeInput = document.getElementById('{{ form.problem_code.id_for_label }}');
const codingSystemInput = document.getElementById('{{ form.coding_system.id_for_label }}');
let searchTimeout;
if (searchInput) {
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (query.length < 2) {
resultsDiv.style.display = 'none';
return;
}
searchTimeout = setTimeout(function() {
fetch(`/emr/api/icd10-search/?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
if (data.results && data.results.length > 0) {
resultsDiv.innerHTML = '';
data.results.forEach(result => {
const item = document.createElement('a');
item.href = '#';
item.className = 'list-group-item list-group-item-action';
item.innerHTML = `<strong>${result.code}</strong> - ${result.description}`;
item.addEventListener('click', function(e) {
e.preventDefault();
problemNameInput.value = result.description;
problemCodeInput.value = result.code;
if (codingSystemInput) {
codingSystemInput.value = 'ICD10';
}
searchInput.value = result.display;
resultsDiv.style.display = 'none';
});
resultsDiv.appendChild(item);
});
resultsDiv.style.display = 'block';
} else {
resultsDiv.innerHTML = '<div class="list-group-item">No results found</div>';
resultsDiv.style.display = 'block';
}
})
.catch(error => {
console.error('Error searching ICD-10:', error);
resultsDiv.innerHTML = '<div class="list-group-item text-danger">Error searching</div>';
resultsDiv.style.display = 'block';
});
}, 300);
});
// Hide results when clicking outside
document.addEventListener('click', function(e) {
if (!searchInput.contains(e.target) && !resultsDiv.contains(e.target)) {
resultsDiv.style.display = 'none';
}
});
}
});
</script>

View File

@ -56,7 +56,8 @@ urlpatterns = [
# Actions
# path('record-create/', views.RecordCreateView.as_view(), name='record_create'),
path('encounter/<uuid:encounter_id>/vitals/add/', views.add_vital_signs, name='add_vital_signs'),
path('patient/<int:patient_id>/problem/add/', views.add_problem, name='add_problem'),
path('encounter/<int:encounter_id>/problem/add/', views.add_problem, name='add_problem'),
path('encounter/<int:encounter_id>/care-plan/add/', views.add_care_plan, name='add_care_plan'),
path('encounter/<int:encounter_id>/status/', views.update_encounter_status, name='update_encounter_status'),
path('note/<int:note_id>/sign/', views.sign_note, name='sign_note'),
path('problem/<int:problem_id>/resolve/', views.resolve_problem, name='resolve_problem'),

View File

@ -229,7 +229,9 @@ class EncounterDetailView(LoginRequiredMixin, DetailView):
).prefetch_related(
'vital_signs__measured_by',
'clinical_notes__author',
'problems_identified__diagnosing_provider'
'problems_identified__diagnosing_provider',
'problems_identified__care_plans',
'problems_identified',
)
def get_context_data(self, **kwargs):
@ -1863,52 +1865,127 @@ def add_vital_signs(request, pk):
@login_required
def add_problem(request, patient_id):
def add_problem(request, encounter_id):
"""
HTMX endpoint for adding a problem.
HTMX endpoint for adding a problem to an encounter.
"""
tenant = request.user.tenant
encounter = get_object_or_404(Encounter, id=encounter_id, tenant=tenant)
if request.method == 'POST':
patient = get_object_or_404(
PatientProfile,
id=patient_id,
tenant=request.user.tenant
)
problem_data = {
'tenant': request.user.tenant,
'patient': patient,
'problem_name': request.POST.get('problem_name'),
'problem_type': request.POST.get('problem_type', 'DIAGNOSIS'),
'severity': request.POST.get('severity'),
'priority': request.POST.get('priority', 'MEDIUM'),
form = ProblemListForm(request.POST, tenant=tenant)
if form.is_valid():
problem = form.save(commit=False)
problem.tenant = tenant
problem.patient = encounter.patient
problem.diagnosing_provider = request.user
problem.related_encounter = encounter
problem.save()
# Log the action
try:
AuditLogEntry.objects.create(
tenant=tenant,
user=request.user,
event_type='CREATE',
event_category='CLINICAL_DATA',
action='Add Problem',
description=f'Problem added: {problem.problem_name}',
content_type=ContentType.objects.get_for_model(ProblemList),
object_id=problem.pk,
object_repr=str(problem),
patient_id=str(encounter.patient.patient_id),
patient_mrn=encounter.patient.mrn,
changes={'problem_type': problem.problem_type, 'status': problem.status}
)
except Exception as e:
print(f"Audit logging failed: {e}")
messages.success(request, f'Problem "{problem.problem_name}" added successfully')
return redirect('emr:encounter_detail', pk=encounter_id)
else:
# Return form with errors
return render(request, 'emr/partials/problem_form_modal.html', {
'form': form,
'encounter': encounter,
'patient': encounter.patient
})
else:
# GET request - show form
form = ProblemListForm(tenant=tenant, initial={
'patient': encounter.patient,
'diagnosing_provider': request.user,
'clinical_notes': request.POST.get('clinical_notes', ''),
}
'related_encounter': encounter,
'status': 'ACTIVE',
'priority': 'MEDIUM'
})
return render(request, 'emr/partials/problem_form_modal.html', {
'form': form,
'encounter': encounter,
'patient': encounter.patient
})
# Add optional fields
if request.POST.get('problem_code'):
problem_data['problem_code'] = request.POST.get('problem_code')
problem_data['coding_system'] = request.POST.get('coding_system', 'ICD10')
if request.POST.get('onset_date'):
problem_data['onset_date'] = request.POST.get('onset_date')
# Create problem
problem = ProblemList.objects.create(**problem_data)
# Log the action
AuditLogger.log_event(
user=request.user,
action='PROBLEM_ADDED',
model='ProblemList',
object_id=problem.id,
details=f"Problem added for {patient.get_full_name()}: {problem.problem_name}"
)
messages.success(request, f'Problem "{problem.problem_name}" added successfully')
return JsonResponse({'success': True, 'problem_id': problem.id})
return JsonResponse({'error': 'Invalid request'}, status=400)
@login_required
def add_care_plan(request, encounter_id):
"""
HTMX endpoint for adding a care plan to an encounter.
"""
tenant = request.user.tenant
encounter = get_object_or_404(Encounter, id=encounter_id, tenant=tenant)
if request.method == 'POST':
form = CarePlanForm(request.POST, tenant=tenant)
if form.is_valid():
care_plan = form.save(commit=False)
care_plan.tenant = tenant
care_plan.patient = encounter.patient
care_plan.primary_provider = request.user
care_plan.save()
form.save_m2m() # Save many-to-many relationships
# Log the action
try:
AuditLogEntry.objects.create(
tenant=tenant,
user=request.user,
event_type='CREATE',
event_category='CARE_PLAN',
action='Add Care Plan',
description=f'Care plan created: {care_plan.title}',
content_type=ContentType.objects.get_for_model(CarePlan),
object_id=care_plan.pk,
object_repr=str(care_plan),
patient_id=str(encounter.patient.patient_id),
patient_mrn=encounter.patient.mrn,
changes={'plan_type': care_plan.plan_type, 'status': care_plan.status}
)
except Exception as e:
print(f"Audit logging failed: {e}")
messages.success(request, f'Care plan "{care_plan.title}" created successfully')
return redirect('emr:encounter_detail', pk=encounter_id)
else:
# Return form with errors
return render(request, 'emr/partials/care_plan_form_modal.html', {
'form': form,
'encounter': encounter,
'patient': encounter.patient
})
else:
# GET request - show form
form = CarePlanForm(tenant=tenant, initial={
'patient': encounter.patient,
'primary_provider': request.user,
'start_date': timezone.now().date(),
'status': 'DRAFT',
'priority': 'ROUTINE'
})
return render(request, 'emr/partials/care_plan_form_modal.html', {
'form': form,
'encounter': encounter,
'patient': encounter.patient
})
@login_required
@ -2012,7 +2089,6 @@ def get_status_class(status):
return status_classes.get(status, 'secondary')
def _norm_code(s: str) -> str:
return (s or "").upper().replace(" ", "")

View File

@ -21,7 +21,6 @@ class BuildingForm(forms.ModelForm):
'name': forms.TextInput(attrs={'class': 'form-control'}),
'code': forms.TextInput(attrs={'class': 'form-control'}),
'building_type': forms.Select(attrs={'class': 'form-select'}),
'airport_code': forms.TextInput(attrs={'class': 'form-control'}),
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'floor_count': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
'total_area_sqm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
@ -185,8 +184,9 @@ class MaintenanceRequestForm(forms.ModelForm):
model = MaintenanceRequest
fields = [
'title', 'description', 'maintenance_type', 'building',
'floor', 'room', 'asset', 'priority', 'scheduled_date', 'actual_cost',
'estimated_cost', 'notes', 'assigned_to', 'scheduled_date', 'estimated_hours', 'status',
'floor', 'room', 'asset', 'priority', 'scheduled_date',
'estimated_cost', 'actual_cost', 'notes', 'assigned_to',
'estimated_hours', 'status', 'completion_notes'
]
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
@ -199,11 +199,12 @@ class MaintenanceRequestForm(forms.ModelForm):
'priority': forms.Select(attrs={'class': 'form-select'}),
'scheduled_date': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
'estimated_cost': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
'actual_cost': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'assigned_to': forms.Select(attrs={'class': 'form-select'}),
'estimated_hours': forms.NumberInput(attrs={'class': 'form-control', 'type': 'number'}),
'estimated_hours': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.5', 'min': '0'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'actual_cost': forms.NumberInput(attrs={'class': 'form-control', 'type': 'number'}),
'completion_notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
}
def __init__(self, *args, **kwargs):
@ -428,4 +429,3 @@ class MaintenanceFilterForm(forms.Form):
super().__init__(*args, **kwargs)
self.fields['assigned_to'].queryset = User.objects.filter(is_active=True)

File diff suppressed because it is too large Load Diff

View File

@ -50,7 +50,7 @@
</div>
<div class="row">
<div class="col-md-6">
<div class="col-md-12">
<div class="mb-3">
<label for="{{ form.building_type.id_for_label }}" class="form-label">Building Type <span class="text-danger">*</span></label>
{{ form.building_type }}
@ -59,16 +59,6 @@
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.airport_code.id_for_label }}" class="form-label">Airport Code <span class="text-danger">*</span></label>
{{ form.airport_code }}
{% if form.airport_code.errors %}
<div class="text-danger small mt-1">{{ form.airport_code.errors.0 }}</div>
{% endif %}
<small class="form-text text-muted">IATA airport code</small>
</div>
</div>
</div>
<div class="mb-3">
@ -256,4 +246,3 @@
</div>
</div>
{% endblock %}

View File

@ -144,6 +144,29 @@
</div>
</div>
</div>
{% if object %}
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.started_date.id_for_label }}" class="form-label">Started Date</label>
{{ form.started_date }}
{% if form.started_date.errors %}
<div class="text-danger small mt-1">{{ form.started_date.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.completed_date.id_for_label }}" class="form-label">Completed Date</label>
{{ form.completed_date }}
{% if form.completed_date.errors %}
<div class="text-danger small mt-1">{{ form.completed_date.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
<!-- Status and Cost -->
<hr>
@ -179,30 +202,8 @@
</div>
<!-- Completion Details -->
{% if object and object.status in 'completed,cancelled' %}
<hr>
<h6 class="fw-bold mb-3">Completion Details</h6>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.completed_date.id_for_label }}" class="form-label">Completed Date</label>
{{ form.completed_date }}
{% if form.completed_date.errors %}
<div class="text-danger small mt-1">{{ form.completed_date.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.actual_hours.id_for_label }}" class="form-label">Actual Hours</label>
{{ form.actual_hours }}
{% if form.actual_hours.errors %}
<div class="text-danger small mt-1">{{ form.actual_hours.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="mb-3">
<label for="{{ form.completion_notes.id_for_label }}" class="form-label">Completion Notes</label>
{{ form.completion_notes }}
@ -211,7 +212,6 @@
{% endif %}
<small class="form-text text-muted">Work performed, parts used, recommendations</small>
</div>
{% endif %}
<!-- Additional Notes -->
<hr>
@ -298,4 +298,3 @@
</div>
</div>
{% endblock %}

58
fix_all_constraints.py Normal file
View File

@ -0,0 +1,58 @@
"""
Comprehensive fix for all database foreign key constraints.
"""
import sqlite3
# Connect to SQLite database
db_path = 'db.sqlite3'
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
print("Fixing all database constraints...")
# Disable foreign key checks
cursor.execute("PRAGMA foreign_keys = OFF;")
# Get all tables with tenant_id that have violations
tables_with_violations = [
'radiology_imaging_study',
'accounts_user',
'hr_department',
'hr_training_program',
'operating_theatre_operating_room',
'patients_patient_profile',
]
for table in tables_with_violations:
try:
# Delete rows with invalid tenant_id
cursor.execute(f"""
DELETE FROM {table}
WHERE tenant_id NOT IN (SELECT id FROM core_tenant)
""")
affected = cursor.rowcount
if affected > 0:
print(f" Deleted {affected} rows from {table} with invalid tenant_id")
except sqlite3.OperationalError as e:
print(f" Error fixing {table}: {e}")
# Commit changes
conn.commit()
# Re-enable foreign key checks
cursor.execute("PRAGMA foreign_keys = ON;")
# Verify constraints
cursor.execute("PRAGMA foreign_key_check;")
violations = cursor.fetchall()
if violations:
print(f"\n⚠ Still have {len(violations)} foreign key violations")
print("First 10 violations:")
for violation in violations[:10]:
print(f" {violation}")
else:
print("\n✓ All foreign key constraints are now valid!")
conn.close()
print("\nDatabase fix completed!")

70
fix_db_constraints.py Normal file
View File

@ -0,0 +1,70 @@
"""
Fix database foreign key constraints before running new migrations.
"""
import os
import django
import sqlite3
# Setup Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
django.setup()
# Connect to SQLite database
db_path = 'db.sqlite3'
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
print("Fixing database constraints...")
# Disable foreign key checks temporarily
cursor.execute("PRAGMA foreign_keys = OFF;")
# Fix operating_theatre_surgical_note_template with invalid tenant_id
print("Fixing operating_theatre_surgical_note_template...")
cursor.execute("""
DELETE FROM operating_theatre_surgical_note_template
WHERE tenant_id NOT IN (SELECT id FROM core_tenant)
""")
affected = cursor.rowcount
print(f" Deleted {affected} rows with invalid tenant_id")
# Check for other tables with similar issues
tables_to_check = [
'facility_management_maintenancerequest',
'facility_management_workorder',
'facility_management_asset',
]
for table in tables_to_check:
try:
cursor.execute(f"""
UPDATE {table}
SET tenant_id = 1
WHERE tenant_id NOT IN (SELECT id FROM core_tenant)
""")
affected = cursor.rowcount
if affected > 0:
print(f" Fixed {affected} rows in {table}")
except sqlite3.OperationalError as e:
# Table might not exist yet
print(f" Skipping {table}: {e}")
# Commit changes
conn.commit()
# Re-enable foreign key checks
cursor.execute("PRAGMA foreign_keys = ON;")
# Verify constraints
cursor.execute("PRAGMA foreign_key_check;")
violations = cursor.fetchall()
if violations:
print("\nRemaining foreign key violations:")
for violation in violations:
print(f" {violation}")
else:
print("\n✓ All foreign key constraints are now valid!")
conn.close()
print("\nDatabase constraints fixed successfully!")

View File

@ -72,6 +72,7 @@ LOCAL_APPS = [
'integration',
'quality',
'facility_management',
'insurance_approvals.apps.InsuranceApprovalsConfig',
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,11 +6,16 @@ from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.db.models import Count, Avg, Sum
from decimal import Decimal
from datetime import date
from datetime import date, timedelta
from .models import *
# ============================================================================
# INLINE ADMIN CLASSES
# ============================================================================
class ScheduleInline(admin.TabularInline):
"""
Inline admin for employee schedules.
@ -46,7 +51,7 @@ class PerformanceReviewInline(admin.TabularInline):
fields = [
'review_date', 'review_type', 'overall_rating', 'status'
]
readonly_fields = ['review_id']
class TrainingRecordInline(admin.TabularInline):
@ -56,12 +61,67 @@ class TrainingRecordInline(admin.TabularInline):
model = TrainingRecord
extra = 0
fields = [
'completion_date',
'program', 'session', 'completion_date',
'status', 'passed'
]
readonly_fields = ['record_id']
class ProgramModuleInline(admin.TabularInline):
"""
Inline admin for training program modules.
"""
model = ProgramModule
extra = 0
fields = ['order', 'title', 'hours']
ordering = ['order']
class ProgramPrerequisiteInline(admin.TabularInline):
"""
Inline admin for training program prerequisites.
"""
model = ProgramPrerequisite
extra = 0
fk_name = 'program'
fields = ['required_program']
class TrainingSessionInline(admin.TabularInline):
"""
Inline admin for training sessions.
"""
model = TrainingSession
extra = 0
fields = [
'title', 'start_at', 'end_at', 'instructor',
'delivery_method', 'capacity'
]
readonly_fields = ['session_id']
class TrainingAttendanceInline(admin.TabularInline):
"""
Inline admin for training attendance.
"""
model = TrainingAttendance
extra = 0
fields = [
'checked_in_at', 'checked_out_at', 'status', 'notes'
]
class TrainingAssessmentInline(admin.TabularInline):
"""
Inline admin for training assessments.
"""
model = TrainingAssessment
extra = 0
fields = [
'name', 'max_score', 'score', 'passed', 'taken_at'
]
@admin.register(Employee)
class EmployeeAdmin(admin.ModelAdmin):
"""
@ -741,121 +801,581 @@ class PerformanceReviewAdmin(admin.ModelAdmin):
return qs.select_related('employee', 'reviewer')
# @admin.register(TrainingRecord)
# class TrainingRecordAdmin(admin.ModelAdmin):
# """
# Admin interface for training records.
# """
# list_display = [
# 'employee_name', 'completion_date', 'status',
# 'passed', 'expiry_status_display'
# ]
#
# search_fields = [
# 'employee__first_name', 'employee__last_name',
# 'employee__employee_id', 'training_name'
# ]
# readonly_fields = [
# 'record_id', 'created_at', 'updated_at'
# ]
# fieldsets = [
# ('Training Information', {
# 'fields': [
# 'record_id', 'employee', 'training_description'
# ]
# }),
#
# ('Training Provider', {
# 'fields': [
# 'training_provider', 'instructor'
# ]
# }),
# ('Training Dates', {
# 'fields': [
# 'completion_date', 'expiry_date'
# ]
# }),
# ('Training Details', {
# 'fields': [
# 'duration_hours', 'credits_earned'
# ]
# }),
# ('Training Status', {
# 'fields': [
# 'status'
# ]
# }),
# ('Results', {
# 'fields': [
# 'score', 'passed'
# ]
# }),
# ('Certification Information', {
# 'fields': [
# 'certificate_number', 'certification_body'
# ]
# }),
# ('Cost Information', {
# 'fields': [
# 'training_cost'
# ]
# }),
# ('Expiry Information', {
# 'fields': [
# 'is_expired', 'days_to_expiry', 'is_due_for_renewal'
# ],
# 'classes': ['collapse']
# }),
# ('Notes', {
# 'fields': [
# 'notes'
# ],
# 'classes': ['collapse']
# }),
# ('Related Information', {
# 'fields': [
# 'employee__tenant'
# ],
# 'classes': ['collapse']
# }),
# ('Metadata', {
# 'fields': [
# 'created_at', 'updated_at', 'created_by'
# ],
# 'classes': ['collapse']
# })
# ]
#
#
# def employee_name(self, obj):
# """Display employee name."""
# return obj.employee.get_full_name()
# employee_name.short_description = 'Employee'
#
# def expiry_status_display(self, obj):
# """Display expiry status with color coding."""
# if not obj.expiry_date:
# return format_html('<span style="color: gray;">No Expiry</span>')
#
# if obj.is_expired:
# return format_html('<span style="color: red;">⚠️ Expired</span>')
#
# if obj.is_due_for_renewal:
# return format_html('<span style="color: orange;">⚠️ Due Soon</span>')
#
# return format_html('<span style="color: green;">✓ Valid</span>')
# expiry_status_display.short_description = 'Expiry Status'
#
# def get_queryset(self, request):
# """Filter by user's tenant."""
# qs = super().get_queryset(request)
# if hasattr(request.user, 'tenant'):
# qs = qs.filter(employee__tenant=request.user.tenant)
# return qs.select_related('employee', 'created_by')
# ============================================================================
# TRAINING ADMIN CLASSES
# ============================================================================
@admin.register(TrainingPrograms)
class TrainingProgramsAdmin(admin.ModelAdmin):
"""
Admin interface for training programs.
"""
list_display = [
'name', 'program_type', 'program_provider', 'instructor',
'duration_hours', 'cost', 'is_certified', 'validity_display'
]
list_filter = [
'tenant', 'program_type', 'is_certified', 'program_provider'
]
search_fields = [
'name', 'description', 'program_provider'
]
readonly_fields = [
'program_id', 'created_at', 'updated_at'
]
fieldsets = [
('Program Information', {
'fields': [
'program_id', 'tenant', 'name', 'description'
]
}),
('Program Details', {
'fields': [
'program_type', 'program_provider', 'instructor'
]
}),
('Schedule Information', {
'fields': [
'start_date', 'end_date', 'duration_hours'
]
}),
('Cost Information', {
'fields': [
'cost'
]
}),
('Certification Information', {
'fields': [
'is_certified', 'validity_days', 'notify_before_days'
]
}),
('Metadata', {
'fields': [
'created_at', 'updated_at', 'created_by'
],
'classes': ['collapse']
})
]
inlines = [ProgramModuleInline, ProgramPrerequisiteInline, TrainingSessionInline]
date_hierarchy = 'start_date'
def validity_display(self, obj):
"""Display validity information."""
if obj.is_certified and obj.validity_days:
return f"{obj.validity_days} days"
return "No expiry"
validity_display.short_description = 'Validity'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(tenant=request.user.tenant)
return qs.select_related('instructor', 'created_by')
@admin.register(ProgramModule)
class ProgramModuleAdmin(admin.ModelAdmin):
"""
Admin interface for program modules.
"""
list_display = [
'program', 'order', 'title', 'hours'
]
list_filter = [
'program__tenant', 'program'
]
search_fields = [
'title', 'program__name'
]
ordering = ['program', 'order']
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(program__tenant=request.user.tenant)
return qs.select_related('program')
@admin.register(ProgramPrerequisite)
class ProgramPrerequisiteAdmin(admin.ModelAdmin):
"""
Admin interface for program prerequisites.
"""
list_display = [
'program', 'required_program'
]
list_filter = [
'program__tenant', 'program'
]
search_fields = [
'program__name', 'required_program__name'
]
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(program__tenant=request.user.tenant)
return qs.select_related('program', 'required_program')
@admin.register(TrainingSession)
class TrainingSessionAdmin(admin.ModelAdmin):
"""
Admin interface for training sessions.
"""
list_display = [
'program', 'title_display', 'instructor', 'start_at',
'end_at', 'delivery_method', 'capacity', 'enrollment_count'
]
list_filter = [
'program__tenant', 'delivery_method', 'start_at'
]
search_fields = [
'title', 'program__name', 'instructor__first_name', 'instructor__last_name'
]
readonly_fields = [
'session_id', 'enrollment_count', 'created_at'
]
fieldsets = [
('Session Information', {
'fields': [
'session_id', 'program', 'title'
]
}),
('Instructor Information', {
'fields': [
'instructor'
]
}),
('Schedule Information', {
'fields': [
'start_at', 'end_at'
]
}),
('Delivery Information', {
'fields': [
'delivery_method', 'location'
]
}),
('Capacity Information', {
'fields': [
'capacity', 'enrollment_count'
]
}),
('Cost Override', {
'fields': [
'cost_override', 'hours_override'
]
}),
('Metadata', {
'fields': [
'created_at', 'created_by'
],
'classes': ['collapse']
})
]
date_hierarchy = 'start_at'
def title_display(self, obj):
"""Display session title or program name."""
return obj.title or obj.program.name
title_display.short_description = 'Title'
def enrollment_count(self, obj):
"""Display enrollment count."""
return obj.enrollments.count()
enrollment_count.short_description = 'Enrollments'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(program__tenant=request.user.tenant)
return qs.select_related('program', 'instructor', 'created_by')
@admin.register(TrainingRecord)
class TrainingRecordAdmin(admin.ModelAdmin):
"""
Admin interface for training records (enrollments).
"""
list_display = [
'employee_name', 'program', 'session', 'enrolled_at',
'completion_date', 'status', 'passed', 'expiry_status_display'
]
list_filter = [
'employee__tenant', 'status', 'passed', 'program__program_type',
'enrolled_at', 'completion_date'
]
search_fields = [
'employee__first_name', 'employee__last_name',
'employee__employee_id', 'program__name'
]
readonly_fields = [
'record_id', 'enrolled_at', 'hours', 'effective_cost',
'eligible_for_certificate', 'completion_percentage',
'created_at', 'updated_at'
]
fieldsets = [
('Enrollment Information', {
'fields': [
'record_id', 'employee', 'program', 'session'
]
}),
('Enrollment Dates', {
'fields': [
'enrolled_at', 'started_at', 'completion_date', 'expiry_date'
]
}),
('Status Information', {
'fields': [
'status', 'passed'
]
}),
('Performance Information', {
'fields': [
'credits_earned', 'score'
]
}),
('Cost Information', {
'fields': [
'cost_paid', 'effective_cost'
]
}),
('Calculated Information', {
'fields': [
'hours', 'eligible_for_certificate', 'completion_percentage'
],
'classes': ['collapse']
}),
('Notes', {
'fields': [
'notes'
],
'classes': ['collapse']
}),
('Metadata', {
'fields': [
'created_at', 'updated_at', 'created_by'
],
'classes': ['collapse']
})
]
inlines = [TrainingAttendanceInline, TrainingAssessmentInline]
date_hierarchy = 'enrolled_at'
def employee_name(self, obj):
"""Display employee name."""
return obj.employee.get_full_name()
employee_name.short_description = 'Employee'
def expiry_status_display(self, obj):
"""Display expiry status with color coding."""
if not obj.expiry_date:
return format_html('<span style="color: gray;">No Expiry</span>')
days_to_expiry = (obj.expiry_date - date.today()).days
if days_to_expiry < 0:
return format_html('<span style="color: red;">⚠️ Expired</span>')
elif days_to_expiry <= 30:
return format_html('<span style="color: orange;">⚠️ Expires Soon</span>')
else:
return format_html('<span style="color: green;">✓ Valid</span>')
expiry_status_display.short_description = 'Expiry Status'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(employee__tenant=request.user.tenant)
return qs.select_related('employee', 'program', 'session', 'created_by')
@admin.register(TrainingAttendance)
class TrainingAttendanceAdmin(admin.ModelAdmin):
"""
Admin interface for training attendance.
"""
list_display = [
'employee_name', 'program_name', 'checked_in_at',
'checked_out_at', 'status', 'duration_display'
]
list_filter = [
'enrollment__employee__tenant', 'status', 'checked_in_at'
]
search_fields = [
'enrollment__employee__first_name', 'enrollment__employee__last_name',
'enrollment__program__name'
]
readonly_fields = [
'duration_display'
]
fieldsets = [
('Attendance Information', {
'fields': [
'enrollment'
]
}),
('Check-in/out Times', {
'fields': [
'checked_in_at', 'checked_out_at'
]
}),
('Status Information', {
'fields': [
'status'
]
}),
('Duration Information', {
'fields': [
'duration_display'
],
'classes': ['collapse']
}),
('Notes', {
'fields': [
'notes'
],
'classes': ['collapse']
})
]
date_hierarchy = 'checked_in_at'
def employee_name(self, obj):
"""Display employee name."""
return obj.enrollment.employee.get_full_name()
employee_name.short_description = 'Employee'
def program_name(self, obj):
"""Display program name."""
return obj.enrollment.program.name
program_name.short_description = 'Program'
def duration_display(self, obj):
"""Display attendance duration."""
if obj.checked_in_at and obj.checked_out_at:
duration = obj.checked_out_at - obj.checked_in_at
hours = duration.total_seconds() / 3600
return f"{hours:.2f} hours"
return "Incomplete"
duration_display.short_description = 'Duration'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(enrollment__employee__tenant=request.user.tenant)
return qs.select_related('enrollment__employee', 'enrollment__program')
@admin.register(TrainingAssessment)
class TrainingAssessmentAdmin(admin.ModelAdmin):
"""
Admin interface for training assessments.
"""
list_display = [
'employee_name', 'program_name', 'name',
'score', 'max_score', 'percentage_display', 'passed', 'taken_at'
]
list_filter = [
'enrollment__employee__tenant', 'passed', 'taken_at'
]
search_fields = [
'enrollment__employee__first_name', 'enrollment__employee__last_name',
'enrollment__program__name', 'name'
]
readonly_fields = [
'percentage_display'
]
fieldsets = [
('Assessment Information', {
'fields': [
'enrollment', 'name'
]
}),
('Score Information', {
'fields': [
'max_score', 'score', 'percentage_display', 'passed'
]
}),
('Date Information', {
'fields': [
'taken_at'
]
}),
('Notes', {
'fields': [
'notes'
],
'classes': ['collapse']
})
]
date_hierarchy = 'taken_at'
def employee_name(self, obj):
"""Display employee name."""
return obj.enrollment.employee.get_full_name()
employee_name.short_description = 'Employee'
def program_name(self, obj):
"""Display program name."""
return obj.enrollment.program.name
program_name.short_description = 'Program'
def percentage_display(self, obj):
"""Display score percentage."""
if obj.score is not None and obj.max_score > 0:
percentage = (obj.score / obj.max_score) * 100
color = 'green' if percentage >= 70 else 'orange' if percentage >= 50 else 'red'
return format_html(
'<span style="color: {};">{:.1f}%</span>',
color, percentage
)
return "N/A"
percentage_display.short_description = 'Percentage'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(enrollment__employee__tenant=request.user.tenant)
return qs.select_related('enrollment__employee', 'enrollment__program')
@admin.register(TrainingCertificates)
class TrainingCertificatesAdmin(admin.ModelAdmin):
"""
Admin interface for training certificates.
"""
list_display = [
'employee_name', 'certificate_name', 'program',
'issued_date', 'expiry_date', 'expiry_status_display',
'certificate_number'
]
list_filter = [
'employee__tenant', 'program', 'issued_date', 'expiry_date'
]
search_fields = [
'employee__first_name', 'employee__last_name',
'certificate_name', 'certificate_number', 'program__name'
]
readonly_fields = [
'certificate_id', 'is_expired', 'days_to_expiry',
'created_at', 'updated_at'
]
fieldsets = [
('Certificate Information', {
'fields': [
'certificate_id', 'employee', 'program', 'enrollment'
]
}),
('Certificate Details', {
'fields': [
'certificate_name', 'certificate_number', 'certification_body'
]
}),
('Date Information', {
'fields': [
'issued_date', 'expiry_date'
]
}),
('File Information', {
'fields': [
'file'
]
}),
('Signature Information', {
'fields': [
'created_by', 'signed_by'
]
}),
('Status Information', {
'fields': [
'is_expired', 'days_to_expiry'
],
'classes': ['collapse']
}),
('Metadata', {
'fields': [
'created_at', 'updated_at'
],
'classes': ['collapse']
})
]
date_hierarchy = 'issued_date'
def employee_name(self, obj):
"""Display employee name."""
return obj.employee.get_full_name()
employee_name.short_description = 'Employee'
def expiry_status_display(self, obj):
"""Display expiry status with color coding."""
if not obj.expiry_date:
return format_html('<span style="color: gray;">No Expiry</span>')
if obj.is_expired:
return format_html('<span style="color: red;">⚠️ Expired</span>')
if obj.days_to_expiry is not None and obj.days_to_expiry <= 30:
return format_html('<span style="color: orange;">⚠️ Expires Soon</span>')
return format_html('<span style="color: green;">✓ Valid</span>')
expiry_status_display.short_description = 'Expiry Status'
def get_queryset(self, request):
"""Filter by user's tenant."""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant'):
qs = qs.filter(employee__tenant=request.user.tenant)
return qs.select_related('employee', 'program', 'enrollment', 'created_by', 'signed_by')
# ============================================================================
# CUSTOM ADMIN ACTIONS
# ============================================================================
def mark_training_completed(modeladmin, request, queryset):
"""Mark selected training records as completed."""
updated = queryset.update(status='COMPLETED', completion_date=date.today())
modeladmin.message_user(request, f'{updated} training records marked as completed.')
mark_training_completed.short_description = "Mark selected training as completed"
def mark_training_passed(modeladmin, request, queryset):
"""Mark selected training records as passed."""
updated = queryset.update(passed=True)
modeladmin.message_user(request, f'{updated} training records marked as passed.')
mark_training_passed.short_description = "Mark selected training as passed"
def generate_certificates(modeladmin, request, queryset):
"""Generate certificates for completed training."""
count = 0
for record in queryset.filter(status='COMPLETED', passed=True):
if record.program.is_certified and not hasattr(record, 'certificate'):
TrainingCertificates.objects.create(
program=record.program,
employee=record.employee,
enrollment=record,
certificate_name=f"{record.program.name} Certificate",
expiry_date=TrainingCertificates.compute_expiry(record.program, date.today()),
created_by=request.user
)
count += 1
modeladmin.message_user(request, f'{count} certificates generated.')
generate_certificates.short_description = "Generate certificates for completed training"
# Add actions to TrainingRecord admin
TrainingRecordAdmin.actions = [mark_training_completed, mark_training_passed, generate_certificates]
# ============================================================================
# ADMIN SITE CUSTOMIZATION
# ============================================================================
# Customize admin site
admin.site.site_header = "Hospital Management System - HR"
admin.site.site_title = "HR Admin"
admin.site.index_title = "Human Resources Administration"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -441,45 +441,7 @@
<!-- Pagination for Card View -->
{% if is_paginated %}
<div class="col-12 mt-3">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if search %}&search={{ search }}{% endif %}{% if selected_schedule %}&schedule={{ selected_schedule }}{% endif %}{% if selected_employee %}&employee={{ selected_employee }}{% endif %}{% if selected_department %}&department={{ selected_department }}{% endif %}{% if selected_shift_type %}&shift_type={{ selected_shift_type }}{% endif %}{% if date_range %}&date_range={{ date_range }}{% endif %}" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search %}&search={{ search }}{% endif %}{% if selected_schedule %}&schedule={{ selected_schedule }}{% endif %}{% if selected_employee %}&employee={{ selected_employee }}{% endif %}{% if selected_department %}&department={{ selected_department }}{% endif %}{% if selected_shift_type %}&shift_type={{ selected_shift_type }}{% endif %}{% if date_range %}&date_range={{ date_range }}{% endif %}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active"><a class="page-link" href="#">{{ num }}</a></li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item"><a class="page-link" href="?page={{ num }}{% if search %}&search={{ search }}{% endif %}{% if selected_schedule %}&schedule={{ selected_schedule }}{% endif %}{% if selected_employee %}&employee={{ selected_employee }}{% endif %}{% if selected_department %}&department={{ selected_department }}{% endif %}{% if selected_shift_type %}&shift_type={{ selected_shift_type }}{% endif %}{% if date_range %}&date_range={{ date_range }}{% endif %}">{{ num }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search %}&search={{ search }}{% endif %}{% if selected_schedule %}&schedule={{ selected_schedule }}{% endif %}{% if selected_employee %}&employee={{ selected_employee }}{% endif %}{% if selected_department %}&department={{ selected_department }}{% endif %}{% if selected_shift_type %}&shift_type={{ selected_shift_type }}{% endif %}{% if date_range %}&date_range={{ date_range }}{% endif %}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search %}&search={{ search }}{% endif %}{% if selected_schedule %}&schedule={{ selected_schedule }}{% endif %}{% if selected_employee %}&employee={{ selected_employee }}{% endif %}{% if selected_department %}&department={{ selected_department }}{% endif %}{% if selected_shift_type %}&shift_type={{ selected_shift_type }}{% endif %}{% if date_range %}&date_range={{ date_range }}{% endif %}" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% include 'partial/pagination.html' %}
{% endif %}
</div>
</div>
@ -494,7 +456,7 @@
{% block js %}
<!-- DataTables JS -->
<script src="{% static 'plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>

View File

@ -188,15 +188,9 @@
<div class="panel panel-inverse" data-sortable-id="index-1">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-clock me-2"></i>Recent Activity
<i class="fas fa-timeline me-2"></i>Departments Tree
</h4>
<div class="panel-heading-btn">
<button type="button" class="btn btn-xs btn-outline-secondary me-2" onclick="refreshActivity()">
<i class="fas fa-sync-alt"></i>
</button>
<a href="#" class="btn btn-xs btn-outline-primary me-2">
<i class="fas fa-list me-1"></i>View All
</a>
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
@ -204,63 +198,64 @@
</div>
</div>
<div class="panel-body">
<div class="timeline" id="activity-timeline">
<!-- Recent Hires -->
{% for employee in recent_hires %}
<div class="timeline-item">
<div class="timeline-marker bg-success"></div>
<div class="timeline-content">
<h6 class="timeline-title">New Employee Hired</h6>
<p class="timeline-text">
<strong>{{ employee.get_full_name }}</strong> joined
{% if employee.department %}{{ employee.department.name }}{% else %}the organization{% endif %}
as {{ employee.job_title|default:"Employee" }}
</p>
<small class="text-muted">{{ employee.hire_date|timesince }} ago</small>
</div>
</div>
{% endfor %}
<!-- Recent Reviews -->
{% for review in recent_reviews %}
<div class="timeline-item">
<div class="timeline-marker bg-info"></div>
<div class="timeline-content">
<h6 class="timeline-title">Performance Review Completed</h6>
<p class="timeline-text">
<strong>{{ review.employee.get_full_name }}</strong> received a performance review
{% if review.overall_rating %}
with rating: {{ review.get_overall_rating_display }}
{% endif %}
</p>
<small class="text-muted">{{ review.review_date|timesince }} ago</small>
</div>
</div>
{% endfor %}
<!-- Recent Training -->
{% for training in recent_training %}
<div class="timeline-item">
<div class="timeline-marker bg-warning"></div>
<div class="timeline-content">
<h6 class="timeline-title">Training Completed</h6>
<p class="timeline-text">
<strong>{{ training.employee.get_full_name }}</strong> completed
"{{ training.training_name }}"
</p>
<small class="text-muted">{{ training.completion_date|timesince }} ago</small>
</div>
</div>
{% endfor %}
{% if not recent_hires and not recent_reviews and not recent_training %}
<div class="text-center py-4">
<i class="fas fa-clock fa-3x text-muted mb-3"></i>
<h6 class="text-muted">No recent activity</h6>
<p class="text-muted">Recent HR activities will appear here</p>
</div>
{% endif %}
</div>
{% include 'hr/departments/department_tree.html' %}
{# <div class="timeline" id="activity-timeline">#}
{# <!-- Recent Hires -->#}
{# {% for employee in recent_hires %}#}
{# <div class="timeline-item">#}
{# <div class="timeline-marker bg-success"></div>#}
{# <div class="timeline-content">#}
{# <h6 class="timeline-title">New Employee Hired</h6>#}
{# <p class="timeline-text">#}
{# <strong>{{ employee.get_full_name }}</strong> joined #}
{# {% if employee.department %}{{ employee.department.name }}{% else %}the organization{% endif %}#}
{# as {{ employee.job_title|default:"Employee" }}#}
{# </p>#}
{# <small class="text-muted">{{ employee.hire_date|timesince }} ago</small>#}
{# </div>#}
{# </div>#}
{# {% endfor %}#}
{##}
{# <!-- Recent Reviews -->#}
{# {% for review in recent_reviews %}#}
{# <div class="timeline-item">#}
{# <div class="timeline-marker bg-info"></div>#}
{# <div class="timeline-content">#}
{# <h6 class="timeline-title">Performance Review Completed</h6>#}
{# <p class="timeline-text">#}
{# <strong>{{ review.employee.get_full_name }}</strong> received a performance review#}
{# {% if review.overall_rating %}#}
{# with rating: {{ review.get_overall_rating_display }}#}
{# {% endif %}#}
{# </p>#}
{# <small class="text-muted">{{ review.review_date|timesince }} ago</small>#}
{# </div>#}
{# </div>#}
{# {% endfor %}#}
{##}
{# <!-- Recent Training -->#}
{# {% for training in recent_training %}#}
{# <div class="timeline-item">#}
{# <div class="timeline-marker bg-warning"></div>#}
{# <div class="timeline-content">#}
{# <h6 class="timeline-title">Training Completed</h6>#}
{# <p class="timeline-text">#}
{# <strong>{{ training.employee.get_full_name }}</strong> completed #}
{# "{{ training.training_name }}"#}
{# </p>#}
{# <small class="text-muted">{{ training.completion_date|timesince }} ago</small>#}
{# </div>#}
{# </div>#}
{# {% endfor %}#}
{##}
{# {% if not recent_hires and not recent_reviews and not recent_training %}#}
{# <div class="text-center py-4">#}
{# <i class="fas fa-clock fa-3x text-muted mb-3"></i>#}
{# <h6 class="text-muted">No recent activity</h6>#}
{# <p class="text-muted">Recent HR activities will appear here</p>#}
{# </div>#}
{# {% endif %}#}
{# </div>#}
</div>
</div>

View File

@ -2,7 +2,63 @@
{% load static %}
{% block title %}Assign Department Head - {{ department.name }}{% endblock %}
{% block css %}
<style>
.avatar-sm {
width: 40px;
height: 40px;
}
.avatar-title {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
}
.table-hover tbody tr:hover {
background-color: rgba(0, 123, 255, 0.1);
}
#userPreview {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
border: 1px solid rgba(0, 0, 0, 0.125);
margin-bottom: 1.5rem;
}
.card-header {
background-color: rgba(0, 123, 255, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
}
@media (max-width: 768px) {
.table-responsive {
font-size: 0.875rem;
}
.avatar-sm {
width: 32px;
height: 32px;
}
.avatar-title {
font-size: 12px;
}
}
</style>
{% endblock %}
{% block content %}
<div class="content">
<div class="container-fluid">
@ -15,7 +71,7 @@
<h6>{{ department.code }} - {{ department.name }}</h6>
</div>
<div class="page-btn">
<a href="{% url 'core:department_detail' department.pk %}" class="btn btn-secondary">
<a href="{% url 'hr:department_detail' department.pk %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to Department
</a>
</div>
@ -140,7 +196,7 @@
<!-- Selected User Preview -->
<div class="row" id="userPreview" style="display: none;">
<div class="col-12">
<div class="alert alert-info">
<div class="note alert-info p-4">
<h6><i class="fas fa-info-circle me-2"></i>Selected User Information</h6>
<div class="row">
<div class="col-md-6">
@ -160,7 +216,7 @@
{% if current_head %}
<div class="row">
<div class="col-12">
<div class="alert alert-warning">
<div class="note alert-warning p-4">
<h6><i class="fas fa-exclamation-triangle me-2"></i>Current Department Head</h6>
<p class="mb-0">
<strong>{{ current_head.get_full_name }}</strong> is currently the head of this department.
@ -175,7 +231,7 @@
<div class="row">
<div class="col-12">
<div class="form-group text-end">
<a href="{% url 'core:department_detail' department.pk %}" class="btn btn-secondary me-2">
<a href="{% url 'hr:department_detail' department.pk %}" class="btn btn-secondary me-2">
<i class="fas fa-times me-1"></i>Cancel
</a>
@ -260,7 +316,7 @@
<p class="text-muted">
There are no active staff members available to assign as department head.
</p>
<a href="{% url 'core:department_detail' department.pk %}" class="btn btn-secondary">
<a href="{% url 'hr:department_detail' department.pk %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to Department
</a>
</div>
@ -270,7 +326,8 @@
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const userSelect = document.getElementById('user_id');
@ -371,60 +428,6 @@ function removeDepartmentHead() {
}
</script>
<style>
.avatar-sm {
width: 40px;
height: 40px;
}
.avatar-title {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
}
.table-hover tbody tr:hover {
background-color: rgba(0, 123, 255, 0.1);
}
#userPreview {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
border: 1px solid rgba(0, 0, 0, 0.125);
margin-bottom: 1.5rem;
}
.card-header {
background-color: rgba(0, 123, 255, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
}
@media (max-width: 768px) {
.table-responsive {
font-size: 0.875rem;
}
.avatar-sm {
width: 32px;
height: 32px;
}
.avatar-title {
font-size: 12px;
}
}
</style>
{% endblock %}

View File

@ -408,13 +408,13 @@
<script src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
<!-- Chart.js -->
<script src="{% static 'plugins/chart.js/dist/chart.js' %}"></script>
<script src="{% static 'plugins/chart.js/dist/chart.umd.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize DataTable
$('#employees-table').DataTable({
responsive: true,
responsive: false,
lengthMenu: [5, 10, 25, 50],
pageLength: 5
});
@ -448,7 +448,7 @@
}]
},
options: {
responsive: true,
responsive: false,
maintainAspectRatio: false,
legend: {
position: 'bottom',

View File

@ -8,7 +8,6 @@
{% block css %}
<!-- Select2 CSS -->
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/select2-bootstrap5-theme/select2-bootstrap5.min.css' %}" rel="stylesheet" />
<style>
.form-floating > .form-control:focus ~ label,
.form-floating > .form-control:not(:placeholder-shown) ~ label {
@ -116,15 +115,15 @@
<!-- Department Code -->
<div class="form-floating mb-3 required-field">
{{ form.department_code }}
<label for="{{ form.department_code.id_for_label }}">Department Code</label>
{% if form.department_code.errors %}
{{ form.code }}
<label for="{{ form.code.id_for_label }}">Department Code</label>
{% if form.code.errors %}
<div class="invalid-feedback d-block">
{{ form.department_code.errors }}
{{ form.code.errors }}
</div>
{% endif %}
{% if form.department_code.help_text %}
<div class="help-text mt-1">{{ form.department_code.help_text }}</div>
{% if form.code.help_text %}
<div class="help-text mt-1">{{ form.code.help_text }}</div>
{% endif %}
</div>
@ -347,11 +346,11 @@
$('#{{ form.name.id_for_label }}').removeClass('is-invalid');
}
if ($('#{{ form.department_code.id_for_label }}').val() === '') {
$('#{{ form.department_code.id_for_label }}').addClass('is-invalid');
if ($('#{{ form.code.id_for_label }}').val() === '') {
$('#{{ form.code.id_for_label }}').addClass('is-invalid');
isValid = false;
} else {
$('#{{ form.department_code.id_for_label }}').removeClass('is-invalid');
$('#{{ form.code.id_for_label }}').removeClass('is-invalid');
}
if (!isValid) {
@ -375,4 +374,3 @@
});
</script>
{% endblock %}

View File

@ -0,0 +1,366 @@
{% load static %}
<div class="department-tree">
{% for department in departments %}
{% include 'hr/departments/department_tree_node.html' with department=department level=0 %}
{% empty %}
<div class="text-center text-muted py-4">
<i class="fas fa-building fa-3x mb-3"></i>
<p class="mb-0">No departments found</p>
<small>Create your first department to get started</small>
</div>
{% endfor %}
</div>
<!-- Department tree node template -->
{% verbatim %}
<script type="text/template" id="department-node-template">
<div class="department-node" data-department-id="{{id}}">
<div class="department-item d-flex align-items-center py-2 px-3 border-bottom">
<div class="department-toggle me-2" style="width: 20px;">
{{#if hasChildren}}
<i class="fas fa-chevron-right toggle-icon" data-bs-toggle="collapse"
data-bs-target="#dept-{{id}}-children" aria-expanded="false"></i>
{{/if}}
</div>
<div class="department-icon me-3">
<i class="fas {{icon}} text-{{color}}"></i>
</div>
<div class="department-info flex-grow-1">
<div class="d-flex align-items-center justify-content-between">
<div>
<h6 class="mb-0 department-name">
<a href="/hr/departments/{{id}}/"
class="text-decoration-none">{{name}}</a>
{{#unless is_active}}
<span class="badge badge-secondary ms-2">Inactive</span>
{{/unless}}
</h6>
<small class="text-muted">
{{department_type}} • {{employee_count}} employees
{{#if department_head}}
• Head: {{department_head}}
{{/if}}
</small>
</div>
<div class="department-actions">
<div class="btn-group btn-group-sm" role="group">
<a href="/hr/departments/{{id}}/"
class="btn btn-outline-primary btn-sm" title="View Details">
<i class="fas fa-eye"></i>
</a>
<a href="/hr/departments/{{id}}/edit/"
class="btn btn-outline-secondary btn-sm" title="Edit">
<i class="fas fa-edit"></i>
</a>
{{#if is_active}}
<button class="btn btn-outline-warning btn-sm"
onclick="deactivateDepartment('{{id}}')" title="Deactivate">
<i class="fas fa-pause"></i>
</button>
{{else}}
<button class="btn btn-outline-success btn-sm"
onclick="activateDepartment('{{id}}')" title="Activate">
<i class="fas fa-play"></i>
</button>
{{/if}}
</div>
</div>
</div>
</div>
</div>
{{#if hasChildren}}
<div class="collapse" id="dept-{{id}}-children">
<div class="department-children ps-4">
<!-- Children will be loaded here -->
</div>
</div>
{{/if}}
</div>
</script>
{% endverbatim %}
<style>
.department-tree {
background: #fff;
border-radius: 0.375rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.department-node {
border-left: 3px solid transparent;
transition: all 0.2s ease;
}
.department-node:hover {
background-color: #f8f9fa;
border-left-color: #007bff;
}
.department-node.active {
background-color: #e3f2fd;
border-left-color: #2196f3;
}
.department-item {
cursor: pointer;
transition: all 0.2s ease;
}
.department-toggle {
cursor: pointer;
user-select: none;
}
.toggle-icon {
transition: transform 0.2s ease;
color: #6c757d;
}
.toggle-icon:hover {
color: #495057;
}
.toggle-icon.expanded {
transform: rotate(90deg);
}
.department-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
border-radius: 50%;
border: 2px solid #e9ecef;
}
.department-info .department-name a {
color: #212529;
font-weight: 500;
}
.department-info .department-name a:hover {
color: #007bff;
}
.department-actions {
opacity: 0;
transition: opacity 0.2s ease;
}
.department-node:hover .department-actions {
opacity: 1;
}
.department-children {
border-left: 1px dashed #dee2e6;
margin-left: 20px;
}
.department-children .department-node {
border-left-width: 2px;
}
.department-children .department-children .department-node {
border-left-width: 1px;
}
/* Department type colors */
.department-clinical { color: #dc3545; }
.department-administrative { color: #6f42c1; }
.department-support { color: #fd7e14; }
.department-ancillary { color: #20c997; }
.department-executive { color: #0d6efd; }
/* Responsive adjustments */
@media (max-width: 768px) {
.department-actions {
opacity: 1;
}
.department-actions .btn-group {
flex-direction: column;
}
.department-info small {
font-size: 0.75rem;
}
}
/* Loading states */
.department-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
color: #6c757d;
}
.department-loading .spinner-border {
width: 1.5rem;
height: 1.5rem;
margin-right: 0.5rem;
}
/* Empty state */
.department-tree-empty {
text-align: center;
padding: 3rem 1rem;
color: #6c757d;
}
.department-tree-empty .fa-building {
color: #dee2e6;
margin-bottom: 1rem;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize department tree functionality
initializeDepartmentTree();
});
function initializeDepartmentTree() {
// Handle toggle clicks
document.addEventListener('click', function(e) {
if (e.target.classList.contains('toggle-icon')) {
e.preventDefault();
e.stopPropagation();
const icon = e.target;
const targetId = icon.getAttribute('data-bs-target');
const target = document.querySelector(targetId);
if (target) {
// Toggle icon rotation
icon.classList.toggle('expanded');
// Load children if not already loaded
if (!target.hasAttribute('data-loaded')) {
loadDepartmentChildren(target, icon.getAttribute('data-department-id'));
}
}
}
});
// Handle department node clicks
document.addEventListener('click', function(e) {
if (e.target.closest('.department-item') && !e.target.closest('.department-actions')) {
const departmentItem = e.target.closest('.department-item');
const departmentNode = departmentItem.closest('.department-node');
// Remove active class from all nodes
document.querySelectorAll('.department-node.active').forEach(node => {
node.classList.remove('active');
});
// Add active class to clicked node
departmentNode.classList.add('active');
// Emit custom event for other components
const departmentId = departmentNode.getAttribute('data-department-id');
document.dispatchEvent(new CustomEvent('departmentSelected', {
detail: { departmentId: departmentId }
}));
}
});
}
function loadDepartmentChildren(container, departmentId) {
// Show loading state
container.innerHTML = '<div class="department-loading"><div class="spinner-border spinner-border-sm" role="status"></div>Loading...</div>';
// Make HTMX request to load children
htmx.ajax('GET', `/hr/api/departments/${departmentId}/children/`, {
target: container,
swap: 'innerHTML'
}).then(() => {
container.setAttribute('data-loaded', 'true');
}).catch(() => {
container.innerHTML = '<div class="text-danger p-2"><i class="fas fa-exclamation-triangle"></i> Failed to load children</div>';
});
}
function activateDepartment(departmentId) {
if (confirm('Are you sure you want to activate this department?')) {
htmx.ajax('POST', `/hr/departments/${departmentId}/activate/`, {
headers: {
'X-CSRFToken': getCsrfToken()
}
}).then(() => {
// Refresh the tree
refreshDepartmentTree();
showToast('Department activated successfully', 'success');
}).catch(() => {
showToast('Failed to activate department', 'error');
});
}
}
function deactivateDepartment(departmentId) {
if (confirm('Are you sure you want to deactivate this department? This will affect all associated employees and schedules.')) {
htmx.ajax('POST', `/hr/departments/${departmentId}/deactivate/`, {
headers: {
'X-CSRFToken': getCsrfToken()
}
}).then(() => {
// Refresh the tree
refreshDepartmentTree();
showToast('Department deactivated successfully', 'warning');
}).catch(() => {
showToast('Failed to deactivate department', 'error');
});
}
}
function refreshDepartmentTree() {
htmx.ajax('GET', '/hr/departments/tree/', {
target: '.department-tree',
swap: 'innerHTML'
});
}
function expandAllDepartments() {
document.querySelectorAll('.toggle-icon').forEach(icon => {
if (!icon.classList.contains('expanded')) {
icon.click();
}
});
}
function collapseAllDepartments() {
document.querySelectorAll('.toggle-icon.expanded').forEach(icon => {
icon.click();
});
}
function getCsrfToken() {
return document.querySelector('[name=csrfmiddlewaretoken]').value;
}
function showToast(message, type = 'info') {
// Use your existing toast notification system
if (typeof HospitalApp !== 'undefined' && HospitalApp.utils && HospitalApp.utils.showToast) {
HospitalApp.utils.showToast(message, type);
} else {
// Fallback to basic alert
alert(message);
}
}
// Export functions for external use
window.DepartmentTree = {
refresh: refreshDepartmentTree,
expandAll: expandAllDepartments,
collapseAll: collapseAllDepartments,
activate: activateDepartment,
deactivate: deactivateDepartment
};
</script>

View File

@ -0,0 +1,301 @@
{% load static %}
<div class="department-node" data-department-id="{{ department.department_id }}"
style="margin-left: {{ level|add:0 }}rem;">
<div class="department-item d-flex align-items-center py-2 px-3 border-bottom">
<div class="department-toggle me-2" style="width: 20px;">
{% if department.sub_departments.exists %}
<i class="fas fa-chevron-right toggle-icon"
data-bs-toggle="collapse"
data-bs-target="#dept-{{ department.department_id }}-children"
data-department-id="{{ department.department_id }}"
aria-expanded="false"></i>
{% endif %}
</div>
<div class="department-icon me-3">
<i class="fas
{% if department.department_type == 'CLINICAL' %}fa-stethoscope text-danger
{% elif department.department_type == 'ADMINISTRATIVE' %}fa-users-cog text-primary
{% elif department.department_type == 'SUPPORT' %}fa-tools text-warning
{% elif department.department_type == 'ANCILLARY' %}fa-microscope text-info
{% elif department.department_type == 'EXECUTIVE' %}fa-crown text-success
{% else %}fa-building text-secondary
{% endif %}"></i>
</div>
<div class="department-info flex-grow-1">
<div class="d-flex align-items-center justify-content-between">
<div>
<h6 class="mb-0 department-name">
<a href="{% url 'hr:department_detail' pk=department.pk %}"
class="text-decoration-none">{{ department.name }}</a>
{% if not department.is_active %}
<span class="badge bg-secondary ms-2">Inactive</span>
{% endif %}
{% if department.department_head %}
<span class="badge bg-info ms-1" title="Department Head">
<i class="fas fa-user-tie"></i>
</span>
{% endif %}
</h6>
<small class="text-muted">
<span class="department-type">{{ department.get_department_type_display }}</span>
<span class="employee-count">{{ department.employee_count }} employee{{ department.employee_count|pluralize }}</span>
{% if department.department_head %}
• Head: {{ department.department_head.get_full_name }}
{% endif %}
{% if department.cost_center %}
• Cost Center: {{ department.cost_center }}
{% endif %}
</small>
</div>
<div class="department-actions">
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'hr:department_detail' pk=department.pk %}"
class="btn btn-outline-primary btn-sm"
title="View Details"
data-bs-toggle="tooltip">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'hr:department_update' pk=department.pk %}"
class="btn btn-outline-secondary btn-sm"
title="Edit Department"
data-bs-toggle="tooltip">
<i class="fas fa-edit"></i>
</a>
{% if department.is_active %}
<button class="btn btn-outline-warning btn-sm"
onclick="deactivateDepartment('{{ department.department_id }}')"
title="Deactivate Department"
data-bs-toggle="tooltip">
<i class="fas fa-pause"></i>
</button>
{% else %}
<button class="btn btn-outline-success btn-sm"
onclick="activateDepartment('{{ department.department_id }}')"
title="Activate Department"
data-bs-toggle="tooltip">
<i class="fas fa-play"></i>
</button>
{% endif %}
<!-- Additional actions dropdown -->
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
data-bs-toggle="dropdown" aria-expanded="false" title="More Actions">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="{% url 'hr:assign_department_head' pk=department.pk %}">
<i class="fas fa-user-tie me-2"></i>Assign Head
</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'hr:employee_list' %}?department={{ department.pk }}">
<i class="fas fa-users me-2"></i>View Employees
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="{% url 'hr:department_create' %}?parent={{ department.pk }}">
<i class="fas fa-plus me-2"></i>Add Sub-Department
</a>
</li>
{% if department.employee_count == 0 and not department.sub_departments.exists %}
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item text-danger"
href="{% url 'hr:department_delete' pk=department.pk %}"
onclick="return confirm('Are you sure you want to delete this department?')">
<i class="fas fa-trash me-2"></i>Delete
</a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
<!-- Department Statistics Bar -->
{% if department.authorized_positions > 0 %}
<div class="mt-2">
<div class="d-flex align-items-center justify-content-between mb-1">
<small class="text-muted">Staffing Level</small>
<small class="text-muted">{{ department.employee_count }}/{{ department.authorized_positions }}</small>
</div>
<div class="progress" style="height: 4px;">
{% with staffing_percentage=department.staffing_percentage %}
<div class="progress-bar
{% if staffing_percentage >= 90 %}bg-success
{% elif staffing_percentage >= 70 %}bg-warning
{% else %}bg-danger
{% endif %}"
role="progressbar"
style="width: {{ staffing_percentage|floatformat:0 }}%"
aria-valuenow="{{ staffing_percentage|floatformat:0 }}"
aria-valuemin="0"
aria-valuemax="100">
</div>
{% endwith %}
</div>
</div>
{% endif %}
</div>
</div>
<!-- Children Container -->
{% if department.sub_departments.exists %}
<div class="collapse" id="dept-{{ department.department_id }}-children">
<div class="department-children ps-4">
{% for child_department in department.sub_departments.all %}
{% include 'hr/departments/department_tree_node.html' with department=child_department level=level|add:1 %}
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Add some specific styles for this node -->
<style>
.department-node[data-department-id="{{ department.department_id }}"] {
{% if not department.is_active %}
opacity: 0.7;
background-color: #f8f9fa;
{% endif %}
}
.department-node[data-department-id="{{ department.department_id }}"] .department-icon {
{% if department.department_type == 'CLINICAL' %}
background-color: #ffeaea;
border-color: #ffcdd2;
{% elif department.department_type == 'ADMINISTRATIVE' %}
background-color: #e8f2ff;
border-color: #bbdefb;
{% elif department.department_type == 'SUPPORT' %}
background-color: #fff8e1;
border-color: #ffe082;
{% elif department.department_type == 'ANCILLARY' %}
background-color: #e0f7fa;
border-color: #80deea;
{% elif department.department_type == 'EXECUTIVE' %}
background-color: #e8f5e8;
border-color: #a5d6a7;
{% endif %}
}
/* Depth-based left border styling */
.department-node[data-department-id="{{ department.department_id }}"] {
{% if level == 0 %}
border-left-width: 4px;
{% elif level == 1 %}
border-left-width: 3px;
{% elif level == 2 %}
border-left-width: 2px;
{% else %}
border-left-width: 1px;
{% endif %}
}
/* Department level specific styling */
{% if level > 0 %}
.department-node[data-department-id="{{ department.department_id }}"] .department-item {
padding-left: {{ level|add:1 }}rem;
}
.department-node[data-department-id="{{ department.department_id }}"] .department-icon {
width: {% if level == 1 %}35px{% elif level == 2 %}30px{% else %}25px{% endif %};
height: {% if level == 1 %}35px{% elif level == 2 %}30px{% else %}25px{% endif %};
}
.department-node[data-department-id="{{ department.department_id }}"] .department-name {
font-size: {% if level == 1 %}0.95rem{% elif level == 2 %}0.9rem{% else %}0.85rem{% endif %};
}
{% endif %}
</style>
<script>
// Initialize tooltips for this specific department node
document.addEventListener('DOMContentLoaded', function() {
const departmentNode = document.querySelector('[data-department-id="{{ department.department_id }}"]');
if (departmentNode) {
// Initialize Bootstrap tooltips
const tooltipTriggerList = departmentNode.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
// Add click handlers for this specific node
const toggleIcon = departmentNode.querySelector('.toggle-icon');
if (toggleIcon) {
toggleIcon.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const target = document.querySelector(this.getAttribute('data-bs-target'));
if (target) {
// Toggle Bootstrap collapse
const bsCollapse = new bootstrap.Collapse(target, {
toggle: true
});
// Rotate icon
this.classList.toggle('expanded');
// Track expansion state
const isExpanded = target.classList.contains('show') || this.classList.contains('expanded');
this.setAttribute('aria-expanded', isExpanded);
}
});
}
}
});
// Department-specific utility functions
window.Department_{{ department.department_id|escapejs }} = {
activate: function() {
activateDepartment('{{ department.department_id|escapejs }}');
},
deactivate: function() {
deactivateDepartment('{{ department.department_id|escapejs }}');
},
expand: function() {
const node = document.querySelector('[data-department-id="{{ department.department_id|escapejs }}"]');
const toggleIcon = node?.querySelector('.toggle-icon');
if (toggleIcon && !toggleIcon.classList.contains('expanded')) {
toggleIcon.click();
}
},
collapse: function() {
const node = document.querySelector('[data-department-id="{{ department.department_id|escapejs }}"]');
const toggleIcon = node?.querySelector('.toggle-icon');
if (toggleIcon && toggleIcon.classList.contains('expanded')) {
toggleIcon.click();
}
},
select: function() {
const node = document.querySelector('[data-department-id="{{ department.department_id|escapejs }}"]');
if (node) {
// Remove active class from all nodes
document.querySelectorAll('.department-node.active').forEach(n => n.classList.remove('active'));
// Add active class to this node
node.classList.add('active');
// Scroll into view if needed
node.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
// Emit selection event
document.dispatchEvent(new CustomEvent('departmentSelected', {
detail: {
departmentId: '{{ department.department_id|escapejs }}',
departmentName: '{{ department.name|escapejs }}',
departmentType: '{{ department.department_type|escapejs }}'
}
}));
}
}
};
</script>

View File

@ -2,7 +2,62 @@
{% load static %}
{% block title %}Delete Employee - {{ employee.get_full_name }} - {{ block.super }}{% endblock %}
{% block css %}
<style>
.font-monospace {
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
.list-group-item {
border: none;
padding: 0.5rem 0;
}
.form-check-label {
cursor: pointer;
}
.form-check-input:checked {
background-color: #dc3545;
border-color: #dc3545;
}
.alert-heading {
margin-bottom: 0.5rem;
}
dl.row dt {
font-weight: 600;
color: #495057;
}
dl.row dd {
color: #6c757d;
}
.card.border-danger {
border-color: #dc3545 !important;
}
.card.border-warning {
border-color: #ffc107 !important;
}
@media (max-width: 768px) {
.d-flex.justify-content-between {
flex-direction: column;
gap: 1rem;
}
.btn-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
@ -238,7 +293,9 @@
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle data handling radio buttons
@ -327,59 +384,6 @@ window.addEventListener('beforeunload', function(e) {
});
</script>
<style>
.font-monospace {
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
.list-group-item {
border: none;
padding: 0.5rem 0;
}
.form-check-label {
cursor: pointer;
}
.form-check-input:checked {
background-color: #dc3545;
border-color: #dc3545;
}
.alert-heading {
margin-bottom: 0.5rem;
}
dl.row dt {
font-weight: 600;
color: #495057;
}
dl.row dd {
color: #6c757d;
}
.card.border-danger {
border-color: #dc3545 !important;
}
.card.border-warning {
border-color: #ffc107 !important;
}
@media (max-width: 768px) {
.d-flex.justify-content-between {
flex-direction: column;
gap: 1rem;
}
.btn-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
}
</style>
{% endblock %}

View File

@ -51,15 +51,7 @@ Delete Performance Review | HR Management
{% endblock %}
{% block content %}
<!-- begin breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
<li class="breadcrumb-item"><a href="{% url 'hr:performance_review_list' %}">Performance Reviews</a></li>
<li class="breadcrumb-item"><a href="{% url 'hr:performance_review_detail' review.id %}">{{ review.employee.get_full_name }}</a></li>
<li class="breadcrumb-item active">Delete</li>
</ol>
<!-- end breadcrumb -->
<!-- begin page-header -->
<h1 class="page-header">

View File

@ -13,17 +13,7 @@
<!-- Summernote CSS -->
<link href="{% static 'plugins/summernote/dist/summernote-lite.css' %}" rel="stylesheet" />
<style>
.form-section {
background-color: #f8f9fa;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
}
.form-section-title {
border-bottom: 1px solid #dee2e6;
padding-bottom: 10px;
margin-bottom: 20px;
}
.category-card {
border: 1px solid #dee2e6;
border-radius: 5px;
@ -44,10 +34,7 @@
padding: 15px;
margin-bottom: 10px;
}
.remove-goal {
color: #dc3545;
cursor: pointer;
}
.help-sidebar {
position: sticky;
top: 20px;
@ -69,16 +56,7 @@
{% endblock %}
{% block content %}
<!-- begin breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
<li class="breadcrumb-item"><a href="{% url 'hr:performance_review_list' %}">Performance Reviews</a></li>
<li class="breadcrumb-item active">
{% if form.instance.id %}Edit{% else %}Create{% endif %} Review
</li>
</ol>
<!-- end breadcrumb -->
<!-- begin page-header -->
<h1 class="page-header">
@ -101,6 +79,7 @@
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-close"><i class="fa fa-times"></i></a>
</div>
</div>
<!-- end panel-heading -->
@ -128,8 +107,9 @@
{% csrf_token %}
<!-- Basic Information Section -->
<div class="form-section">
<h5 class="form-section-title">Basic Information</h5>
<div class="card mb-3">
<h6 class="card-header">Basic Information</h6>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.employee.id_for_label }}" class="form-label">Employee <span class="text-danger">*</span></label>
@ -162,8 +142,11 @@
<div class="col-md-6 mb-3">
<label for="review_period" class="form-label">Review Period <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="review_period" name="review_period">
<input type="hidden" id="{{ form.period_start.id_for_label }}" name="{{ form.period_start.html_name }}" value="{{ form.period_start.value|date:'Y-m-d'|default:'' }}">
<input type="hidden" id="{{ form.period_end.id_for_label }}" name="{{ form.period_end.html_name }}" value="{{ form.period_end.value|date:'Y-m-d'|default:'' }}">
{{ form.period_start }}
{{ form.period_end }}
<!-- Hidden model fields -->
{{ form.review_period_start }}
{{ form.review_period_end }}
{% if form.period_start.errors or form.period_end.errors %}
<div class="invalid-feedback d-block">
{{ form.period_start.errors }}
@ -174,16 +157,19 @@
<div class="col-md-6 mb-3">
<label for="{{ form.due_date.id_for_label }}" class="form-label">Due Date <span class="text-danger">*</span></label>
{{ form.due_date }}
{{ form.review_date }}
{% if form.due_date.errors %}
<div class="invalid-feedback d-block">{{ form.due_date.errors }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Performance Categories Section -->
<div class="form-section">
<h5 class="form-section-title">Performance Categories</h5>
<div class="card mb-3">
<h6 class="card-header">Performance Categories</h6>
<div class="card-body">
<div id="categories-container">
<!-- Categories will be added here dynamically -->
{% if categories %}
@ -226,10 +212,12 @@
</button>
</div>
</div>
</div>
<!-- Goals and Achievements Section -->
<div class="form-section">
<h5 class="form-section-title">Goals and Achievements</h5>
<div class="card mb-3">
<h6 class="card-header">Goals and Achievements</h6>
<div class="card-body">
<div id="goals-container">
<!-- Goals will be added here dynamically -->
{% if goals %}
@ -278,10 +266,12 @@
</button>
</div>
</div>
</div>
<!-- Strengths and Areas for Improvement Section -->
<div class="form-section">
<h5 class="form-section-title">Strengths and Areas for Improvement</h5>
<div class="card mb-3">
<h6 class="card-header">Strengths and Areas for Improvement</h6>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.strengths.id_for_label }}" class="form-label">Strengths</label>
@ -301,10 +291,12 @@
</div>
</div>
</div>
</div>
<!-- Development Plan Section -->
<div class="form-section">
<h5 class="form-section-title">Development Plan</h5>
<div class="card mb-3">
<h6 class="card-header">Development Plan</h6>
<div class="card-body">
<div class="row">
<div class="col-md-12 mb-3">
<label for="{{ form.development_plan.id_for_label }}" class="form-label">Development Plan</label>
@ -315,14 +307,17 @@
</div>
</div>
</div>
</div>
<!-- Comments Section -->
<div class="form-section">
<h5 class="form-section-title">Comments</h5>
<div class="card mb-3">
<h6 class="card-header">Comments</h6>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.reviewer_comments.id_for_label }}" class="form-label">Reviewer Comments</label>
{{ form.reviewer_comments }}
{{ form.notes }}
{% if form.reviewer_comments.errors %}
<div class="invalid-feedback d-block">{{ form.reviewer_comments.errors }}</div>
{% endif %}
@ -336,10 +331,12 @@
</div>
</div>
</div>
</div>
<!-- Overall Score Section -->
<div class="form-section">
<h5 class="form-section-title">Overall Score</h5>
<div class="card mb-3">
<h6 class="card-header">Overall Score</h6>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.overall_score.id_for_label }}" class="form-label">Overall Score</label>
@ -347,6 +344,8 @@
{{ form.overall_score }}
<span class="input-group-text">/5</span>
</div>
{{ form.overall_rating }}
{{ form.competency_ratings }}
<div class="form-text">Leave blank to calculate automatically from categories</div>
{% if form.overall_score.errors %}
<div class="invalid-feedback d-block">{{ form.overall_score.errors }}</div>
@ -365,10 +364,12 @@
</div>
</div>
</div>
</div>
<!-- Attachments Section -->
<div class="form-section">
<h5 class="form-section-title">Attachments</h5>
<div class="card mb-3">
<h6 class="card-header">Attachments</h6>
<div class="card-body">
<div class="row">
<div class="col-md-12 mb-3">
<label for="{{ form.attachments.id_for_label }}" class="form-label">Attachments</label>
@ -401,6 +402,12 @@
</div>
</div>
{% endif %}
<!-- Include hidden model fields for goals -->
{{ form.goals_achieved }}
{{ form.goals_not_achieved }}
{{ form.future_goals }}
</div>
</div>
<!-- Form Actions -->
@ -429,11 +436,20 @@
<div class="col-xl-3">
<div class="help-sidebar">
<!-- Help Card -->
<div class="card help-card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Help & Guidelines</h5>
<div class="panel panel-inverse">
<!-- begin panel-heading -->
<div class="panel-heading">
<h4 class="panel-title">
Help & Guidelines
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-close"><i class="fa fa-times"></i></a>
</div>
<div class="card-body">
</div>
<div class="panel-body">
<h6>Performance Review Process</h6>
<p>Follow these steps to complete a performance review:</p>
<ol>
@ -466,11 +482,20 @@
</div>
<!-- Status Card -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Review Status</h5>
<!-- begin panel-heading -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
Review Status
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-close"><i class="fa fa-times"></i></a>
</div>
<div class="card-body">
</div>
<div class="panel-body">
<p>The review status determines the workflow:</p>
<ul>
<li><strong>Draft</strong> - Initial creation, not visible to employee</li>
@ -527,6 +552,9 @@
$(this).val(picker.startDate.format('YYYY-MM-DD') + ' - ' + picker.endDate.format('YYYY-MM-DD'));
$('#{{ form.period_start.id_for_label }}').val(picker.startDate.format('YYYY-MM-DD'));
$('#{{ form.period_end.id_for_label }}').val(picker.endDate.format('YYYY-MM-DD'));
// Also update the hidden model fields
$('#{{ form.review_period_start.id_for_label }}').val(picker.startDate.format('YYYY-MM-DD'));
$('#{{ form.review_period_end.id_for_label }}').val(picker.endDate.format('YYYY-MM-DD'));
});
// Handle date range picker cancel event
@ -534,6 +562,8 @@
$(this).val('');
$('#{{ form.period_start.id_for_label }}').val('');
$('#{{ form.period_end.id_for_label }}').val('');
$('#{{ form.review_period_start.id_for_label }}').val('');
$('#{{ form.review_period_end.id_for_label }}').val('');
});
// Initialize DatePicker for due date
@ -555,6 +585,8 @@
// Handle due date picker apply event
$('#{{ form.due_date.id_for_label }}').on('apply.daterangepicker', function(ev, picker) {
$(this).val(picker.startDate.format('YYYY-MM-DD'));
// Also update the hidden model field
$('#{{ form.review_date.id_for_label }}').val(picker.startDate.format('YYYY-MM-DD'));
});
// Initialize Summernote for rich text editors
@ -569,6 +601,12 @@
]
});
// Update overall score when overall_score field changes
$('#{{ form.overall_score.id_for_label }}').on('change', function() {
var score = $(this).val();
$('#{{ form.overall_rating.id_for_label }}').val(score);
});
// Handle star ratings
$('.rating-stars').each(function() {
var $stars = $(this);
@ -729,4 +767,3 @@
});
</script>
{% endblock %}

View File

@ -67,14 +67,6 @@
{% endblock %}
{% block content %}
<!-- begin breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
<li class="breadcrumb-item"><a href="{% url 'hr:schedule_list' %}">Schedules</a></li>
<li class="breadcrumb-item active">{% if form.instance.id %}Edit{% else %}Create{% endif %} Schedule</li>
</ol>
<!-- end breadcrumb -->
<!-- begin page-header -->
<h1 class="page-header">{% if form.instance.id %}Edit{% else %}Create{% endif %} Schedule</h1>
@ -437,4 +429,3 @@
});
</script>
{% endblock %}

View File

@ -211,43 +211,7 @@
<!-- Pagination -->
{% if is_paginated %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if search %}&search={{ search }}{% endif %}{% if status %}&status={{ status }}{% endif %}{% if department %}&department={{ department }}{% endif %}" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search %}&search={{ search }}{% endif %}{% if status %}&status={{ status }}{% endif %}{% if department %}&department={{ department }}{% endif %}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active"><a class="page-link" href="#">{{ num }}</a></li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item"><a class="page-link" href="?page={{ num }}{% if search %}&search={{ search }}{% endif %}{% if status %}&status={{ status }}{% endif %}{% if department %}&department={{ department }}{% endif %}">{{ num }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search %}&search={{ search }}{% endif %}{% if status %}&status={{ status }}{% endif %}{% if department %}&department={{ department }}{% endif %}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search %}&search={{ search }}{% endif %}{% if status %}&status={{ status }}{% endif %}{% if department %}&department={{ department }}{% endif %}" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% include 'partial/pagination.html' %}
{% endif %}
</div>

View File

@ -7,11 +7,10 @@
{% block css %}
<!-- DateTimePicker CSS -->
<link href="{% static 'plugins/bootstrap-datepicker/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/bootstrap-timepicker/css/bootstrap-timepicker.min.css' %}" rel="stylesheet" />
<!-- Select2 CSS -->
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme.min.css' %}" rel="stylesheet" />
<style>
.help-sidebar {
background-color: #f8f9fa;
@ -363,7 +362,7 @@
{% block js %}
<!-- DateTimePicker JS -->
<script src="{% static 'plugins/bootstrap-datepicker/js/bootstrap-datepicker.min.js' %}"></script>
<script src="{% static 'plugins/bootstrap-datepicker/js/bootstrap-datepicker.js' %}"></script>
<script src="{% static 'plugins/bootstrap-timepicker/js/bootstrap-timepicker.min.js' %}"></script>
<!-- Select2 JS -->
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>

View File

@ -29,15 +29,26 @@
</ol>
</nav>
<h1 class="h3 mb-0">
{% if object %}Edit Training Session{% else %}Schedule Training Session{% endif %}
</h1>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-body">
<div class="panel panel-inverse" data-sortable-id="index-1">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-file-edit me-2"></i>{% if object %}Edit Training Session{% else %}Schedule Training Session{% endif %}
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<form method="post" novalidate>
{% csrf_token %}
@ -203,7 +214,7 @@
Cost Override
</label>
<div class="input-group">
<span class="input-group-text">$</span>
<span class="input-group-text py-1 px-1"><span class="symbol m-0">&#xea;</span></span>
{{ form.cost_override }}
</div>
<div class="form-text">Override program cost for this session</div>

View File

@ -6,15 +6,9 @@
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
<li class="breadcrumb-item active">Training Management</li>
</ul>
<div class="row align-items-center mb-3">
<div class="col">
<h1 class="page-header">Training Management</h1>
<h1 class="page-header"><i class="fas fa-graduation-cap me-2"></i>Training<span class="fw-light">Management</span></h1>
<p class="text-muted">Employee training records and certification tracking</p>
</div>
<div class="col-auto">
@ -25,71 +19,77 @@
</div>
<!-- Overview -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body d-flex align-items-center">
<div class="flex-grow-1">
<h4 class="mb-0">{{ total_records }}</h4>
<p class="mb-0">Total Records</p>
</div>
<i class="fa fa-database fa-2x ms-3"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body d-flex align-items-center">
<div class="flex-grow-1">
<h4 class="mb-0">{{ completed_trainings }}</h4>
<p class="mb-0">Completed</p>
</div>
<i class="fa fa-check-circle fa-2x ms-3"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body d-flex align-items-center">
<div class="flex-grow-1">
<h4 class="mb-0">{{ pending_trainings }}</h4>
<p class="mb-0">Scheduled / In Progress</p>
</div>
<i class="fa fa-clock fa-2x ms-3"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-danger text-white">
<div class="card-body d-flex align-items-center">
<div class="flex-grow-1">
<h4 class="mb-0">{{ overdue_trainings }}</h4>
<p class="mb-0">Expired / Overdue</p>
</div>
<i class="fa fa-exclamation-triangle fa-2x ms-3"></i>
</div>
</div>
<div class="row mb-4">
<div class="col-lg-3 col-sm-6">
<div class="widget widget-stats bg-primary mb-7px">
<div class="stats-icon stats-icon-lg"><i class="fa fa-database fa-fw"></i></div>
<div class="stats-content">
<div class="stats-title">Total Records</div>
<div class="stats-number">{{ total_records }}</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="widget widget-stats bg-success mb-7px">
<div class="stats-icon stats-icon-lg"><i class="fa fa-check-circle fa-fw"></i></div>
<div class="stats-content">
<div class="stats-title">Completed</div>
<div class="stats-number">{{ completed_trainings }}</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="widget widget-stats bg-warning mb-7px">
<div class="stats-icon stats-icon-lg"><i class="fa fa-clock fa-fw"></i></div>
<div class="stats-content">
<div class="stats-title">Scheduled / In Progress</div>
<div class="stats-number">{{ pending_trainings }}</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="widget widget-stats bg-danger mb-7px">
<div class="stats-icon stats-icon-lg"><i class="fa fa-exclamation-triangle fa-fw"></i></div>
<div class="stats-content">
<div class="stats-title">Expired / Overdue</div>
<div class="stats-number">{{ overdue_trainings }}</div>
</div>
</div>
</div>
</div>
<!-- Tabs -->
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" id="trainingTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="records-tab" data-bs-toggle="tab" data-bs-target="#records" type="button" role="tab">
<i class="fa fa-list me-2"></i>Training Records
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="compliance-tab" data-bs-toggle="tab" data-bs-target="#compliance" type="button" role="tab">
<i class="fa fa-shield-alt me-2"></i>Compliance
</button>
</li>
</ul>
</div>
<div class="panel panel-inverse" data-sortable-id="index-1">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-graduation-cap me-2"></i>Training<span class="fw-light">Records</span>
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="card mb-4" >
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" id="trainingTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="records-tab" data-bs-toggle="tab" data-bs-target="#records" type="button" role="tab">
<i class="fa fa-list me-2"></i>Training Records
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="compliance-tab" data-bs-toggle="tab" data-bs-target="#compliance" type="button" role="tab">
<i class="fa fa-shield-alt me-2"></i>Compliance
</button>
</li>
</ul>
<div class="card-body">
</div>
<div class="card-body">
<div class="tab-content" id="trainingTabContent">
<!-- Training Records -->
@ -323,6 +323,8 @@
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -85,16 +85,6 @@
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
<li class="breadcrumb-item"><a href="{% url 'hr:training_record_list' %}">Training Records</a></li>
<li class="breadcrumb-item active">
{% if form.instance.id %}Edit{% else %}Create{% endif %} Record
</li>
</ol>
</nav>
<h1 class="h3 mb-0">
<i class="fas fa-graduation-cap text-primary me-2"></i>
{% if form.instance.id %}Edit{% else %}Create{% endif %} Training Record
@ -130,72 +120,87 @@
</ul>
</div>
{% endif %}
<div class="panel panel-inverse" data-sortable-id="form-stuff-1">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-graduation-cap me-2"></i>{% if form.instance.id %}Edit{% else %}Create{% endif %} Training Record
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<form method="post" id="trainingRecordForm" novalidate>
{% csrf_token %}
<!-- Basic Information Section -->
<div class="form-section">
<h5 class="form-section-title">
<div class="card border border-primary mb-3">
<h6 class="card-header bg-primary text-white">
<i class="fas fa-info-circle me-2"></i>Basic Information
</h5>
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.employee.id_for_label }}" class="form-label">
Employee <span class="required-field">*</span>
</label>
{{ form.employee }}
{% if form.employee.help_text %}
<div class="field-help">{{ form.employee.help_text }}</div>
{% endif %}
{% if form.employee.errors %}
<div class="invalid-feedback d-block">{{ form.employee.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.program.id_for_label }}" class="form-label">
Training Program <span class="required-field">*</span>
</label>
{{ form.program }}
{% if form.program.help_text %}
<div class="field-help">{{ form.program.help_text }}</div>
{% endif %}
{% if form.program.errors %}
<div class="invalid-feedback d-block">{{ form.program.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.session.id_for_label }}" class="form-label">
Training Session
</label>
{{ form.session }}
{% if form.session.help_text %}
<div class="field-help">{{ form.session.help_text }}</div>
{% endif %}
{% if form.session.errors %}
<div class="invalid-feedback d-block">{{ form.session.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.status.id_for_label }}" class="form-label">
Status <span class="required-field">*</span>
</label>
{{ form.status }}
{% if form.status.help_text %}
<div class="field-help">{{ form.status.help_text }}</div>
{% endif %}
{% if form.status.errors %}
<div class="invalid-feedback d-block">{{ form.status.errors.0 }}</div>
{% endif %}
</h6>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.employee.id_for_label }}" class="form-label">
Employee <span class="required-field">*</span>
</label>
{{ form.employee }}
{% if form.employee.help_text %}
<div class="field-help">{{ form.employee.help_text }}</div>
{% endif %}
{% if form.employee.errors %}
<div class="invalid-feedback d-block">{{ form.employee.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.program.id_for_label }}" class="form-label">
Training Program <span class="required-field">*</span>
</label>
{{ form.program }}
{% if form.program.help_text %}
<div class="field-help">{{ form.program.help_text }}</div>
{% endif %}
{% if form.program.errors %}
<div class="invalid-feedback d-block">{{ form.program.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.session.id_for_label }}" class="form-label">
Training Session
</label>
{{ form.session }}
{% if form.session.help_text %}
<div class="field-help">{{ form.session.help_text }}</div>
{% endif %}
{% if form.session.errors %}
<div class="invalid-feedback d-block">{{ form.session.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.status.id_for_label }}" class="form-label">
Status <span class="required-field">*</span>
</label>
{{ form.status }}
{% if form.status.help_text %}
<div class="field-help">{{ form.status.help_text }}</div>
{% endif %}
{% if form.status.errors %}
<div class="invalid-feedback d-block">{{ form.status.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Schedule Information Section -->
<div class="form-section">
<h5 class="form-section-title">
<div class="card border border-success mb-3">
<h6 class="card-header bg-success text-white">
<i class="fas fa-calendar-alt me-2"></i>Schedule Information
</h5>
</h6>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.started_at.id_for_label }}" class="form-label">
@ -221,12 +226,14 @@
</div>
</div>
</div>
</div>
<!-- Progress & Assessment Section -->
<div class="form-section">
<h5 class="form-section-title">
<div class="card border border-info mb-3">
<h6 class="card-header bg-info text-white">
<i class="fas fa-chart-line me-2"></i>Progress & Assessment
</h5>
</h6>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
<label for="{{ form.score.id_for_label }}" class="form-label">
@ -284,12 +291,14 @@
</div>
</div>
</div>
</div>
<!-- Notes Section -->
<div class="form-section">
<h5 class="form-section-title">
<div class="card border border-warning mb-3">
<h6 class="card-header bg-warning text-white">
<i class="fas fa-sticky-note me-2"></i>Additional Notes
</h5>
</h6>
<div class="card-body">
<div class="row">
<div class="col-md-12 mb-3">
<label for="{{ form.notes.id_for_label }}" class="form-label">
@ -305,6 +314,7 @@
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="d-flex justify-content-between align-items-center">
@ -325,18 +335,26 @@
</div>
</form>
</div>
</div>
</div>
<!-- Help Sidebar -->
<div class="col-xl-4">
<div class="help-sidebar">
<!-- Help Card -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="fas fa-question-circle me-2"></i>Help & Guidelines
</h5>
</div>
<div class="card-body">
<div class="panel panel-inverse" data-sortable-id="form-stuff-2">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-question-circle me-2"></i>Help & Guidelines
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<h6 class="text-primary">Training Record Guidelines</h6>
<p class="small">Follow these steps to create a comprehensive training record:</p>
<ol class="small">
@ -375,13 +393,19 @@
</div>
<!-- Tips Card -->
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h5 class="card-title mb-0">
<i class="fas fa-lightbulb me-2"></i>Tips
</h5>
</div>
<div class="card-body">
<div class="panel panel-inverse" data-sortable-id="form-stuff-3">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-lightbulb me-2"></i>Tips
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<ul class="small mb-0">
<li>Select a training session to automatically populate program details</li>
<li>Completion date is required for completed training</li>
@ -395,13 +419,19 @@
<!-- Program Info Card -->
{% if form.instance.program %}
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0">
<i class="fas fa-info-circle me-2"></i>Program Information
</h5>
</div>
<div class="card-body">
<div class="panel panel-inverse" data-sortable-id="form-stuff-4">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-info-circle me-2"></i>Program Information
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<h6>{{ form.instance.program.name }}</h6>
<p class="small text-muted">{{ form.instance.program.description|truncatewords:20 }}</p>
<div class="row small">

View File

@ -36,7 +36,8 @@ urlpatterns = [
path('departments/bulk-deactivate/', views.bulk_deactivate_departments, name='bulk_deactivate_departments'),
path('departments/<int:pk>/assign-head/', views.assign_department_head, name='assign_department_head'),
path('api/department-hierarchy/', views.get_department_hierarchy, name='get_department_hierarchy'),
path('htmx/department-tree/', views.department_tree, name='department_tree'),
path('api/departments/<uuid:department_id>/children/', views.department_children, name='department_children'),
path('departments/tree/', views.department_tree, name='department_tree'),
path('search/departments/', views.department_search, name='department_search'),
# ============================================================================
@ -46,6 +47,7 @@ urlpatterns = [
path('schedules/create/', views.ScheduleCreateView.as_view(), name='schedule_create'),
path('schedules/<int:pk>/', views.ScheduleDetailView.as_view(), name='schedule_detail'),
path('schedules/<int:pk>/update/', views.ScheduleUpdateView.as_view(), name='schedule_update'),
path('schedules/<int:pk>/delete/', views.ScheduleAssignmentDeleteView.as_view(), name='schedule_assignment_delete'),
# Note: No delete view for schedules - use status updates instead
# ============================================================================
@ -54,7 +56,9 @@ urlpatterns = [
path('assignments/', views.ScheduleAssignmentListView.as_view(), name='schedule_assignment_list'),
path('assignments/create/', views.ScheduleAssignmentCreateView.as_view(), name='schedule_assignment_create'),
path('assignments/<int:pk>/update/', views.ScheduleAssignmentUpdateView.as_view(), name='schedule_assignment_update'),
# Note: No detail/delete views for assignments - managed via schedules
path('assignments/<int:pk>/delete/', views.ScheduleAssignmentDeleteView.as_view(), name='schedule_assignment_delete'),
path('assignments/export/', views.export_assignments, name='export_assignments'),
# Note: No detail views for assignments - managed via schedules
# ============================================================================
# TIME ENTRY URLS (RESTRICTED CRUD - Operational Data)
@ -142,6 +146,9 @@ urlpatterns = [
# ============================================================================
path('ajax/get-program-sessions/', views.get_program_sessions, name='get_program_sessions'),
path('ajax/get-program-details/', views.get_program_details, name='get_program_details'),
path('ajax/check-time-entry-conflicts/', views.check_time_entry_conflicts, name='check_time_entry_conflicts'),
path('ajax/get-employee-schedule-assignments/', views.get_employee_schedule_assignments, name='get_employee_schedule_assignments'),
path('ajax/get-schedule-assignment-details/', views.get_schedule_assignment_details, name='get_schedule_assignment_details'),
# ============================================================================
# API ENDPOINTS

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,14 @@
# scripts/hr_data_generator.py
"""
Comprehensive HR Data Generator for Hospital Management System
Generates realistic Saudi healthcare HR data including:
- Employees with complete profiles
- Departments with organizational structure
- Work schedules and assignments
- Time tracking entries
- Performance reviews
- Complete training management system
"""
import os
import django
@ -16,6 +26,7 @@ import random
from datetime import datetime, timedelta, date, time
from decimal import Decimal
from django.utils import timezone as django_timezone
from django.core.exceptions import ValidationError
from hr.models import (
Employee, Department, Schedule, ScheduleAssignment, TimeEntry, PerformanceReview,
@ -215,122 +226,186 @@ def create_or_update_saudi_employees(tenants, departments_by_tenant, employees_p
# Create more users if we need more employees
to_create = max(0, employees_per_tenant - num_existing)
print(f"Creating {to_create} new employees for {tenant.name} (existing: {num_existing})")
for _ in range(to_create):
gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE])
first = random.choice(SAUDI_FIRST_NAMES_MALE if gender == Employee.Gender.MALE else SAUDI_FIRST_NAMES_FEMALE)
father = random.choice(SAUDI_FIRST_NAMES_MALE)
grandfather = random.choice(SAUDI_FIRST_NAMES_MALE)
last = random.choice(SAUDI_LAST_NAMES)
base_username = f"{first.lower()}.{last.lower().replace('-', '').replace('al', '')}"
username = tenant_scoped_unique_username(tenant, base_username)
email = f"{username}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa"
for i in range(to_create):
try:
gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE])
first = random.choice(SAUDI_FIRST_NAMES_MALE if gender == Employee.Gender.MALE else SAUDI_FIRST_NAMES_FEMALE)
father = random.choice(SAUDI_FIRST_NAMES_MALE)
grandfather = random.choice(SAUDI_FIRST_NAMES_MALE)
last = random.choice(SAUDI_LAST_NAMES)
base_username = f"{first.lower()}.{last.lower().replace('-', '').replace('al', '')}"
username = tenant_scoped_unique_username(tenant, base_username)
email = f"{username}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa"
u = User.objects.create(
tenant=tenant,
username=username,
email=email,
first_name=first,
# father_name=father,
# grandfather_name=grandfather,
last_name=last,
is_active=True,
)
u.set_password('Hospital@123')
u.save()
tenant_users.append(u) # signal creates Employee
# Now (re)populate employee HR data
for u in tenant_users:
emp = getattr(u, 'employee_profile', None)
if not emp:
u = User.objects.create(
tenant=tenant,
username=username,
email=email,
first_name=first,
last_name=last,
is_active=True,
)
u.set_password('Hospital@123')
u.save()
tenant_users.append(u) # signal creates Employee
if (i + 1) % 50 == 0:
print(f" Created {i + 1}/{to_create} users for {tenant.name}")
except Exception as e:
print(f"Error creating user {i+1} for {tenant.name}: {e}")
continue
# Basic personal
if not emp.first_name:
emp.first_name = u.first_name or emp.first_name
if not emp.last_name:
emp.last_name = u.last_name or emp.last_name
if not emp.email:
emp.email = u.email
# Now (re)populate employee HR data
print(f"Updating employee profiles for {tenant.name}...")
for i, u in enumerate(tenant_users):
try:
emp = getattr(u, 'employee_profile', None)
if not emp:
continue
# Demographics
if not emp.gender:
emp.gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE, Employee.Gender.OTHER])
if not emp.marital_status:
emp.marital_status = random.choice([
Employee.MaritalStatus.SINGLE, Employee.MaritalStatus.MARRIED,
Employee.MaritalStatus.DIVORCED, Employee.MaritalStatus.WIDOWED,
Employee.MaritalStatus.SEPARATED, Employee.MaritalStatus.OTHER
])
if not emp.date_of_birth:
# 2255 years old
emp.date_of_birth = (django_timezone.now().date()
- timedelta(days=random.randint(22*365, 55*365)))
# Basic personal
if not emp.first_name:
emp.first_name = u.first_name or emp.first_name
if not emp.last_name:
emp.last_name = u.last_name or emp.last_name
if not emp.email:
emp.email = u.email
# Contact E.164 (both are mobiles by model design)
if not emp.phone:
emp.phone = e164_ksa_mobile()
if not emp.mobile_phone:
emp.mobile_phone = e164_ksa_mobile()
# Add father and grandfather names
if not emp.father_name:
emp.father_name = random.choice(SAUDI_FIRST_NAMES_MALE)
if not emp.grandfather_name:
emp.grandfather_name = random.choice(SAUDI_FIRST_NAMES_MALE)
# Address
if not emp.address_line_1:
emp.address_line_1 = f"{random.randint(1, 999)} {random.choice(['King Fahd Rd', 'Prince Sultan St', 'Olaya St'])}"
if not emp.city:
emp.city = random.choice(SAUDI_CITIES)
if not emp.postal_code:
emp.postal_code = f"{random.randint(10000, 99999)}"
if not emp.country:
emp.country = 'Saudi Arabia'
# ID information
if not emp.identification_number:
emp.identification_number = f"{random.randint(1000000000, 9999999999)}"
emp.id_type = random.choice([Employee.IdNumberTypes.NATIONAL_ID, Employee.IdNumberTypes.IQAMA])
# Org
if not emp.department:
emp.department = random.choice(depts)
if not emp.job_title:
emp.job_title = pick_job_title_for_department(emp.department)
# Demographics
if not emp.gender:
emp.gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE])
if not emp.marital_status:
emp.marital_status = random.choice([
Employee.MaritalStatus.SINGLE, Employee.MaritalStatus.MARRIED,
Employee.MaritalStatus.DIVORCED, Employee.MaritalStatus.WIDOWED,
Employee.MaritalStatus.SEPARATED
])
if not emp.date_of_birth:
# 2255 years old
emp.date_of_birth = (django_timezone.now().date()
- timedelta(days=random.randint(22*365, 55*365)))
# Role (derive from job title if not set)
if not emp.role or emp.role == Employee.Role.GUEST:
emp.role = infer_role_from_title(emp.job_title)
# Contact E.164 (both are mobiles by model design)
if not emp.phone:
emp.phone = e164_ksa_mobile()
if not emp.mobile_phone:
emp.mobile_phone = e164_ksa_mobile()
# Employment
if not emp.employment_type:
emp.employment_type = random.choice([
Employee.EmploymentType.FULL_TIME, Employee.EmploymentType.PART_TIME,
Employee.EmploymentType.CONTRACT, Employee.EmploymentType.TEMPORARY,
Employee.EmploymentType.INTERN, Employee.EmploymentType.VOLUNTEER,
Employee.EmploymentType.PER_DIEM, Employee.EmploymentType.CONSULTANT
])
if not emp.hire_date:
emp.hire_date = django_timezone.now().date() - timedelta(days=random.randint(30, 2000))
if not emp.employment_status:
emp.employment_status = random.choices(
[Employee.EmploymentStatus.ACTIVE, Employee.EmploymentStatus.INACTIVE, Employee.EmploymentStatus.LEAVE],
weights=[85, 10, 5]
)[0]
# If terminated, set a termination_date after hire_date
if emp.employment_status == Employee.EmploymentStatus.TERMINATED and not emp.termination_date:
emp.termination_date = emp.hire_date + timedelta(days=random.randint(30, 1000))
# Address
if not emp.address_line_1:
emp.address_line_1 = f"{random.randint(1, 999)} {random.choice(['King Fahd Rd', 'Prince Sultan St', 'Olaya St', 'King Abdul Aziz Rd', 'Tahlia St'])}"
if not emp.address_line_2:
if random.choice([True, False]):
emp.address_line_2 = f"Apt {random.randint(1, 50)}"
if not emp.city:
emp.city = random.choice(SAUDI_CITIES)
if not emp.postal_code:
emp.postal_code = f"{random.randint(10000, 99999)}"
if not emp.country:
emp.country = 'Saudi Arabia'
# Licensure (optional by role/title)
jt_lower = (emp.job_title or '').lower()
if not emp.license_number and any(k in jt_lower for k in ['physician', 'nurse', 'pharmacist', 'radiolog']):
emp.license_number = f"LIC-{random.randint(100000, 999999)}"
emp.license_expiry_date = django_timezone.now().date() + timedelta(days=random.randint(180, 1095))
emp.license_state = random.choice(['Riyadh Province', 'Makkah Province', 'Eastern Province', 'Asir Province'])
if 'physician' in jt_lower and not emp.npi_number:
emp.npi_number = f"SA{random.randint(1000000, 9999999)}"
# Org
if not emp.department:
emp.department = random.choice(depts)
if not emp.job_title:
emp.job_title = pick_job_title_for_department(emp.department)
# Preferences
emp.user_timezone = 'Asia/Riyadh'
if not emp.language:
emp.language = random.choice(['ar', 'en', 'ar_SA'])
if not emp.theme:
emp.theme = random.choice([Employee.Theme.LIGHT, Employee.Theme.DARK, Employee.Theme.AUTO])
# Role (derive from job title if not set)
if not emp.role or emp.role == Employee.Role.GUEST:
emp.role = infer_role_from_title(emp.job_title)
emp.save()
all_employees.append(emp)
# Employment
if not emp.employment_type:
emp.employment_type = random.choices([
Employee.EmploymentType.FULL_TIME, Employee.EmploymentType.PART_TIME,
Employee.EmploymentType.CONTRACT, Employee.EmploymentType.TEMPORARY,
Employee.EmploymentType.INTERN, Employee.EmploymentType.CONSULTANT
], weights=[70, 15, 8, 4, 2, 1])[0]
if not emp.hire_date:
emp.hire_date = django_timezone.now().date() - timedelta(days=random.randint(30, 2000))
if not emp.employment_status:
emp.employment_status = random.choices(
[Employee.EmploymentStatus.ACTIVE, Employee.EmploymentStatus.INACTIVE, Employee.EmploymentStatus.LEAVE],
weights=[85, 10, 5]
)[0]
# If terminated, set a termination_date after hire_date
if emp.employment_status == Employee.EmploymentStatus.TERMINATED and not emp.termination_date:
emp.termination_date = emp.hire_date + timedelta(days=random.randint(30, 1000))
# Compensation
if not emp.hourly_rate and emp.employment_type in [Employee.EmploymentType.PART_TIME, Employee.EmploymentType.PER_DIEM]:
emp.hourly_rate = Decimal(str(random.randint(50, 300)))
if not emp.annual_salary and emp.employment_type == Employee.EmploymentType.FULL_TIME:
emp.annual_salary = Decimal(str(random.randint(60000, 500000)))
if not emp.fte_percentage:
if emp.employment_type == Employee.EmploymentType.PART_TIME:
emp.fte_percentage = Decimal(str(random.choice([25, 50, 75])))
else:
emp.fte_percentage = Decimal('100.00')
# Licensure (optional by role/title)
jt_lower = (emp.job_title or '').lower()
if not emp.license_number and any(k in jt_lower for k in ['physician', 'nurse', 'pharmacist', 'radiolog']):
emp.license_number = f"LIC-{random.randint(100000, 999999)}"
emp.license_expiry_date = django_timezone.now().date() + timedelta(days=random.randint(180, 1095))
emp.license_state = random.choice(['Riyadh Province', 'Makkah Province', 'Eastern Province', 'Asir Province'])
if 'physician' in jt_lower and not emp.npi_number:
emp.npi_number = f"SA{random.randint(1000000, 9999999)}"
# Emergency contact
if not emp.emergency_contact_name:
contact_gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE])
contact_first = random.choice(SAUDI_FIRST_NAMES_MALE if contact_gender == Employee.Gender.MALE else SAUDI_FIRST_NAMES_FEMALE)
contact_last = random.choice(SAUDI_LAST_NAMES)
emp.emergency_contact_name = f"{contact_first} {contact_last}"
emp.emergency_contact_relationship = random.choice(['Spouse', 'Parent', 'Sibling', 'Child', 'Friend'])
emp.emergency_contact_phone = e164_ksa_mobile()
# Bio for senior staff
if any(k in jt_lower for k in ['chief', 'director', 'manager', 'senior', 'consultant']):
if not emp.bio:
years_exp = random.randint(5, 25)
emp.bio = f"Experienced healthcare professional with {years_exp} years in {emp.department.name if emp.department else 'healthcare'}. Specialized in {random.choice(['patient care', 'clinical excellence', 'team leadership', 'quality improvement'])}."
# Preferences
emp.user_timezone = 'Asia/Riyadh'
if not emp.language:
emp.language = random.choices(['ar', 'en'], weights=[70, 30])[0]
if not emp.theme:
emp.theme = random.choice([Employee.Theme.LIGHT, Employee.Theme.DARK, Employee.Theme.AUTO])
# Approval status for active employees
if emp.employment_status == Employee.EmploymentStatus.ACTIVE:
emp.is_verified = True
emp.is_approved = True
emp.approval_date = emp.hire_date + timedelta(days=random.randint(1, 30))
emp.save()
all_employees.append(emp)
if (i + 1) % 100 == 0:
print(f" Updated {i + 1}/{len(tenant_users)} employee profiles for {tenant.name}")
except Exception as e:
print(f"Error updating employee profile for user {u.username}: {e}")
continue
print(f"Employees ready for {tenant.name}: {len([e for e in all_employees if e.tenant == tenant])}")
@ -1148,7 +1223,8 @@ def create_training_certificates(training_records, employees):
# Select signer from same tenant
tenant_signers = [s for s in potential_signers if s.tenant == record.employee.tenant]
signer = random.choice(tenant_signers) if tenant_signers else None
signer_employee = random.choice(tenant_signers) if tenant_signers else None
signer_user = signer_employee.user if signer_employee else None
try:
certificate = TrainingCertificates.objects.create(
@ -1159,13 +1235,17 @@ def create_training_certificates(training_records, employees):
certificate_number=certificate_number,
certification_body=certification_body,
expiry_date=expiry_date,
signed_by=signer,
signed_by=signer_user,
created_by=signer_user,
created_at=django_timezone.now() - timedelta(days=random.randint(0, 7)),
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 3))
)
certificates.append(certificate)
print(f"Created certificate {certificate_number} for {record.employee.get_full_name()}")
except Exception as e:
print(f"Error creating certificate for {record.employee.get_full_name()}: {e}")
import traceback
traceback.print_exc()
print(f"Created {len(certificates)} training certificates")
return certificates

File diff suppressed because it is too large Load Diff

View File

@ -1,429 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("core", "0001_initial"),
("facility_management", "0001_initial"),
("inpatients", "0001_initial"),
("operating_theatre", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="surgeryschedule",
name="operating_room",
field=models.ForeignKey(
blank=True,
help_text="Operating room assignment",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="surgery_operations",
to="operating_theatre.operatingroom",
),
),
migrations.AddField(
model_name="surgeryschedule",
name="patient",
field=models.ForeignKey(
help_text="Patient undergoing surgery",
on_delete=django.db.models.deletion.CASCADE,
related_name="surgeries",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="surgeryschedule",
name="primary_surgeon",
field=models.ForeignKey(
help_text="Primary surgeon",
on_delete=django.db.models.deletion.CASCADE,
related_name="primary_surgeries",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="surgeryschedule",
name="scrub_nurse",
field=models.ForeignKey(
blank=True,
help_text="Scrub nurse",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="inpatient_scrub_cases",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="surgeryschedule",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="surgery_schedules",
to="core.tenant",
),
),
migrations.AddField(
model_name="transfer",
name="admission",
field=models.ForeignKey(
help_text="Associated admission",
on_delete=django.db.models.deletion.CASCADE,
related_name="transfers",
to="inpatients.admission",
),
),
migrations.AddField(
model_name="transfer",
name="approved_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who approved transfer",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="approved_transfers",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfer",
name="completed_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who completed transfer",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="completed_transfers",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfer",
name="from_bed",
field=models.ForeignKey(
blank=True,
help_text="Source bed",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="transfers_from",
to="inpatients.bed",
),
),
migrations.AddField(
model_name="transfer",
name="patient",
field=models.ForeignKey(
help_text="Patient being transferred",
on_delete=django.db.models.deletion.CASCADE,
related_name="transfers",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="transfer",
name="requested_by",
field=models.ForeignKey(
help_text="Staff member who requested transfer",
on_delete=django.db.models.deletion.CASCADE,
related_name="requested_transfers",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfer",
name="to_bed",
field=models.ForeignKey(
blank=True,
help_text="Destination bed",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="transfers_to",
to="inpatients.bed",
),
),
migrations.AddField(
model_name="transfer",
name="transport_team",
field=models.ManyToManyField(
blank=True,
help_text="Transport team members",
related_name="transport_assignments",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="ward",
name="attending_physicians",
field=models.ManyToManyField(
blank=True,
help_text="Attending physicians for this ward",
related_name="attending_wards",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="ward",
name="building",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wards",
to="facility_management.building",
),
),
migrations.AddField(
model_name="ward",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the ward",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_wards",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="ward",
name="floor",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wards_floor",
to="facility_management.floor",
),
),
migrations.AddField(
model_name="ward",
name="nurse_manager",
field=models.ForeignKey(
blank=True,
help_text="Nurse manager for this ward",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="managed_wards",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="ward",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="wards",
to="core.tenant",
),
),
migrations.AddField(
model_name="transfer",
name="from_ward",
field=models.ForeignKey(
help_text="Source ward",
on_delete=django.db.models.deletion.CASCADE,
related_name="transfers_from",
to="inpatients.ward",
),
),
migrations.AddField(
model_name="transfer",
name="to_ward",
field=models.ForeignKey(
help_text="Destination ward",
on_delete=django.db.models.deletion.CASCADE,
related_name="transfers_to",
to="inpatients.ward",
),
),
migrations.AddField(
model_name="bed",
name="ward",
field=models.ForeignKey(
help_text="Ward containing this bed",
on_delete=django.db.models.deletion.CASCADE,
related_name="beds",
to="inpatients.ward",
),
),
migrations.AddField(
model_name="admission",
name="current_ward",
field=models.ForeignKey(
help_text="Current ward assignment",
on_delete=django.db.models.deletion.CASCADE,
related_name="current_admissions",
to="inpatients.ward",
),
),
migrations.AddIndex(
model_name="dischargesummary",
index=models.Index(
fields=["admission"], name="inpatients__admissi_0ccfc4_idx"
),
),
migrations.AddIndex(
model_name="dischargesummary",
index=models.Index(
fields=["discharge_date"], name="inpatients__dischar_275061_idx"
),
),
migrations.AddIndex(
model_name="dischargesummary",
index=models.Index(
fields=["discharging_physician"], name="inpatients__dischar_8244b8_idx"
),
),
migrations.AddIndex(
model_name="surgeryschedule",
index=models.Index(
fields=["tenant", "status"], name="inpatients__tenant__ba70e0_idx"
),
),
migrations.AddIndex(
model_name="surgeryschedule",
index=models.Index(
fields=["patient"], name="inpatients__patient_7254b1_idx"
),
),
migrations.AddIndex(
model_name="surgeryschedule",
index=models.Index(
fields=["admission"], name="inpatients__admissi_cb9ef2_idx"
),
),
migrations.AddIndex(
model_name="surgeryschedule",
index=models.Index(
fields=["scheduled_date"], name="inpatients__schedul_43b664_idx"
),
),
migrations.AddIndex(
model_name="surgeryschedule",
index=models.Index(
fields=["primary_surgeon"], name="inpatients__primary_5238a2_idx"
),
),
migrations.AddIndex(
model_name="surgeryschedule",
index=models.Index(
fields=["operating_room"], name="inpatients__operati_dd9f2e_idx"
),
),
migrations.AddIndex(
model_name="ward",
index=models.Index(
fields=["tenant", "ward_type"], name="inpatients__tenant__338f37_idx"
),
),
migrations.AddIndex(
model_name="ward",
index=models.Index(
fields=["specialty"], name="inpatients__special_149fd4_idx"
),
),
migrations.AddIndex(
model_name="ward",
index=models.Index(
fields=["is_active", "is_accepting_admissions"],
name="inpatients__is_acti_7dc57f_idx",
),
),
migrations.AlterUniqueTogether(
name="ward",
unique_together={("tenant", "ward_id")},
),
migrations.AddIndex(
model_name="transfer",
index=models.Index(
fields=["admission", "status"], name="inpatients__admissi_08e4ca_idx"
),
),
migrations.AddIndex(
model_name="transfer",
index=models.Index(
fields=["patient"], name="inpatients__patient_9e7d37_idx"
),
),
migrations.AddIndex(
model_name="transfer",
index=models.Index(
fields=["from_ward", "to_ward"], name="inpatients__from_wa_466f5f_idx"
),
),
migrations.AddIndex(
model_name="transfer",
index=models.Index(
fields=["requested_datetime"], name="inpatients__request_2b3b18_idx"
),
),
migrations.AddIndex(
model_name="transfer",
index=models.Index(
fields=["priority"], name="inpatients__priorit_4b9e10_idx"
),
),
migrations.AddIndex(
model_name="bed",
index=models.Index(
fields=["ward", "status"], name="inpatients__ward_id_87bbcc_idx"
),
),
migrations.AddIndex(
model_name="bed",
index=models.Index(
fields=["current_admission"], name="inpatients__current_46ec0b_idx"
),
),
migrations.AddIndex(
model_name="bed",
index=models.Index(
fields=["bed_type", "room_type"], name="inpatients__bed_typ_7caad7_idx"
),
),
migrations.AddIndex(
model_name="bed",
index=models.Index(fields=["status"], name="inpatients__status_e98d75_idx"),
),
migrations.AlterUniqueTogether(
name="bed",
unique_together={("ward", "bed_number")},
),
migrations.AddIndex(
model_name="admission",
index=models.Index(
fields=["tenant", "status"], name="inpatients__tenant__71213b_idx"
),
),
migrations.AddIndex(
model_name="admission",
index=models.Index(
fields=["patient", "status"], name="inpatients__patient_87f767_idx"
),
),
migrations.AddIndex(
model_name="admission",
index=models.Index(
fields=["current_ward"], name="inpatients__current_8e363a_idx"
),
),
migrations.AddIndex(
model_name="admission",
index=models.Index(
fields=["admission_datetime"], name="inpatients__admissi_632f0b_idx"
),
),
migrations.AddIndex(
model_name="admission",
index=models.Index(
fields=["attending_physician"], name="inpatients__attendi_19fb85_idx"
),
),
]

View File

@ -7,7 +7,7 @@
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<div>
<h1 class="h2">
<i class="fas fa-procedures me-2"></i></i>Surgeries<span class="fw-light">Schedule</span>
<i class="fas fa-procedures me-2"></i>Surgeries<span class="fw-light">Schedule</span>
</h1>
<p class="text-muted">Comprehensive discharge planning and coordination.</p>
</div>

View File

@ -78,10 +78,11 @@ urlpatterns = [
path('surgery/create/', views.SurgeryScheduleCreateView.as_view(), name='surgery_create'),
path('surgery/calendar/', views.surgery_calendar, name='surgery_calendar'),
path('surgery/<int:pk>/cancel/', views.cancel_surgery, name='cancel_surgery'),
path('surgery/<int:pk>/complete/', views.mark_surgery_completed, name='complete_surgery'),
path('surgery/<int:pk>/complete/', views.complete_surgery, name='complete_surgery'),
path('surgery/<int:pk>/confirm/', views.confirm_surgery, name='confirm_surgery'),
path('surgery/<int:pk>/prep/', views.prep_surgery, name='prep_surgery'),
path('surgery/<int:pk>/postpone/', views.postpone_surgery, name='postpone_surgery'),
path('surgery/<int:pk>/start/', views.start_surgery, name='start_surgery'),
# Actions

View File

@ -85,6 +85,7 @@ class InpatientDashboardView(LoginRequiredMixin, ListView):
return context
class WardListView(LoginRequiredMixin, ListView):
"""
List view for wards.
@ -970,10 +971,10 @@ class SurgeryScheduleListView(LoginRequiredMixin, ListView):
tenant = self.request.user.tenant
# Get statuses for filter dropdown
context['statuses'] = SurgerySchedule.STATUS_CHOICES
context['statuses'] = SurgerySchedule.SurgeryStatus.choices
# Get priorities for filter dropdown
context['priorities'] = SurgerySchedule.PRIORITY_CHOICES
context['priorities'] = SurgerySchedule.SurgeryPriority.choices
# Get surgeons for filter dropdown
context['surgeons'] = User.objects.filter(
@ -2167,7 +2168,7 @@ def clean_bed(request, pk):
return render(request, 'inpatients/clean_bed.html', {
'bed': bed,
'cleaning_levels': Bed.CLEANING_LEVEL_CHOICES,
'cleaning_levels': Bed.CleaningLevel.choices,
'next': request.GET.get('next', reverse('inpatients:bed_detail', kwargs={'pk': bed.pk}))
})
@ -3178,13 +3179,13 @@ class TransferListView(LoginRequiredMixin, ListView):
tenant = self.request.user.tenant
# Get statuses for filter dropdown
context['statuses'] = Transfer.STATUS_CHOICES
context['statuses'] = Transfer.TransferStatus.choices
# Get transfer types for filter dropdown
context['transfer_types'] = Transfer.TRANSFER_TYPE_CHOICES
context['transfer_types'] = Transfer.TransferType.choices
# Get priorities for filter dropdown
context['priorities'] = Transfer.PRIORITY_CHOICES
context['priorities'] = Transfer.TransferPriority.choices
# Get wards for filter dropdown
context['wards'] = Ward.objects.filter(
@ -3301,9 +3302,6 @@ class TransferUpdateView(LoginRequiredMixin, UpdateView):
return reverse('inpatients:transfer_detail', kwargs={'pk': self.object.pk})
@login_required
# @permission_required('inpatients.change_surgeryschedule')
def mark_surgery_completed(request, pk):
@ -3508,6 +3506,95 @@ def postpone_surgery(request, pk):
})
@login_required
# @permission_required('inpatients.change_surgeryschedule')
def start_surgery(request, pk):
"""
Start a surgery (change status to IN_PROGRESS).
"""
surgery = get_object_or_404(
SurgerySchedule,
pk=pk,
admission__tenant=request.user.tenant
)
# Only surgeries in PREP status can be started
if surgery.status != 'PREP':
messages.error(request, _('Only surgeries in prep status can be started'))
return redirect('inpatients:surgery_detail', pk=surgery.pk)
if request.method == 'POST':
surgery.status = 'IN_PROGRESS'
surgery.actual_start_time = timezone.now()
surgery.save()
# Log the action
AuditLogger.log_event(
actor=request.user,
action='SURGERY_STARTED',
target=surgery,
target_repr=str(surgery),
description=f"Surgery started for {surgery.patient.get_full_name()}"
)
messages.success(request, _('Surgery started successfully'))
return redirect('inpatients:surgery_schedule')
return render(request, 'inpatients/surgeries/surgery_schedule.html', {
'surgery': surgery
})
@login_required
# @permission_required('inpatients.change_surgeryschedule')
def complete_surgery(request, pk):
"""
Complete a surgery.
"""
surgery = get_object_or_404(
SurgerySchedule,
pk=pk,
admission__tenant=request.user.tenant
)
# Only surgeries in IN_PROGRESS status can be completed
if surgery.status != 'IN_PROGRESS':
messages.error(request, _('Only surgeries in progress can be completed'))
return redirect('inpatients:surgery_detail', pk=surgery.pk)
if request.method == 'POST':
notes = request.POST.get('notes', '')
surgery.status = 'COMPLETED'
surgery.actual_end_time = timezone.now()
if notes:
surgery.notes = (surgery.notes or "") + f"\n\nCompletion Notes ({timezone.now().strftime('%Y-%m-%d %H:%M')}):\n{notes}"
# Calculate actual duration
if surgery.actual_start_time:
duration = surgery.actual_end_time - surgery.actual_start_time
surgery.actual_duration_minutes = int(duration.total_seconds() / 60)
surgery.save()
# Log the action
AuditLogger.log_event(
actor=request.user,
action='SURGERY_COMPLETED',
target=surgery,
target_repr=str(surgery),
description=f"Surgery completed for {surgery.patient.get_full_name()}"
)
messages.success(request, _('Surgery completed successfully'))
return redirect('inpatients:surgery_schedule')
return render(request, 'inpatients/surgeries/surgery_schedule.html', {
'surgery': surgery
})
@login_required
def inpatient_stats(request):
"""

View File

@ -0,0 +1,394 @@
# Insurance Approval Request Module
## Overview
The Insurance Approval Request Module is a comprehensive system for managing insurance pre-authorization and approval requests for any type of medical order in the hospital management system. It provides a universal approval workflow that works with Lab Orders, Radiology Orders, Prescriptions, and any other order type.
## Features
### ✅ Core Functionality
- **Universal Order Support**: Works with any order type using Django's GenericForeignKey
- **Complete Workflow**: 13 status states from Draft to Approved/Denied/Appealed
- **Priority Management**: Routine, Urgent, STAT, Emergency levels
- **Document Management**: Upload and track supporting documents
- **Communication Tracking**: Log all interactions with insurance companies
- **Template System**: Reusable templates for common approval requests
- **Audit Trail**: Complete history of all status changes
- **Multi-tenant**: Full tenant isolation and scoping
- **Expiration Tracking**: Automatic warnings for expiring approvals
### 📊 Status Workflow
```
DRAFT → PENDING_SUBMISSION → SUBMITTED → UNDER_REVIEW
┌─────────┴─────────┐
↓ ↓
APPROVED DENIED
↓ ↓
PARTIALLY_APPROVED APPEAL_SUBMITTED
↓ ↓
EXPIRED APPEAL_APPROVED
APPEAL_DENIED
```
## Installation
### 1. App is Already Configured
The app is already added to `INSTALLED_APPS` in `hospital_management/settings.py`:
```python
INSTALLED_APPS = [
# ... other apps ...
'insurance_approvals',
]
```
### 2. Create Database Tables
Run the table creation script:
```bash
python3 create_insurance_tables.py
```
This will create 5 tables:
- `insurance_approvals_request`
- `insurance_approvals_document`
- `insurance_approvals_status_history`
- `insurance_approvals_communication_log`
- `insurance_approvals_template`
### 3. Add URLs to Main Project
Add to `hospital_management/urls.py`:
```python
urlpatterns = [
# ... other patterns ...
path('insurance-approvals/', include('insurance_approvals.urls')),
]
```
## Usage
### Creating an Approval Request from a Lab Order
```python
from insurance_approvals.models import InsuranceApprovalRequest
from django.contrib.contenttypes.models import ContentType
# Get the lab order
lab_order = LabOrder.objects.get(pk=order_id)
# Create approval request
approval = InsuranceApprovalRequest.objects.create(
tenant=lab_order.tenant,
patient=lab_order.patient,
insurance_info=lab_order.patient.insurance_info.first(),
content_type=ContentType.objects.get_for_model(lab_order),
object_id=lab_order.id,
request_type='LABORATORY',
service_description=f"Lab Tests: {', '.join([t.test_name for t in lab_order.tests.all()])}",
procedure_codes=lab_order.get_procedure_codes(),
diagnosis_codes=lab_order.get_diagnosis_codes(),
clinical_justification="Patient presents with symptoms requiring diagnostic testing...",
requesting_provider=request.user,
service_start_date=lab_order.order_datetime.date(),
priority='ROUTINE',
requested_quantity=lab_order.tests.count()
)
```
### Creating from a Radiology Order
```python
# Get the imaging order
imaging_order = ImagingStudy.objects.get(pk=order_id)
# Create approval request
approval = InsuranceApprovalRequest.objects.create(
tenant=imaging_order.tenant,
patient=imaging_order.patient,
insurance_info=imaging_order.patient.insurance_info.first(),
content_type=ContentType.objects.get_for_model(imaging_order),
object_id=imaging_order.id,
request_type='RADIOLOGY',
service_description=imaging_order.study_description,
procedure_codes=imaging_order.procedure_code,
diagnosis_codes=imaging_order.get_diagnosis_codes(),
clinical_justification=imaging_order.clinical_indication,
requesting_provider=imaging_order.ordering_provider,
service_start_date=imaging_order.requested_datetime.date(),
priority=imaging_order.priority,
requested_quantity=1
)
```
### Creating from a Prescription
```python
# Get the prescription
prescription = Prescription.objects.get(pk=prescription_id)
# Create approval request
approval = InsuranceApprovalRequest.objects.create(
tenant=prescription.tenant,
patient=prescription.patient,
insurance_info=prescription.patient.insurance_info.first(),
content_type=ContentType.objects.get_for_model(prescription),
object_id=prescription.id,
request_type='PHARMACY',
service_description=f"{prescription.medication.display_name} - {prescription.dosage_instructions}",
procedure_codes=prescription.medication.ndc_code,
diagnosis_codes=prescription.get_diagnosis_codes(),
clinical_justification=prescription.indication or "As prescribed for patient condition",
medical_necessity="Medication required for treatment of diagnosed condition",
requesting_provider=prescription.prescriber,
service_start_date=prescription.date_written,
requested_quantity=prescription.quantity,
requested_units=prescription.quantity,
priority='ROUTINE'
)
```
### Updating Status
```python
# Approve a request
approval.status = 'APPROVED'
approval.authorization_number = 'AUTH123456'
approval.approved_quantity = approval.requested_quantity
approval.effective_date = timezone.now().date()
approval.expiration_date = timezone.now().date() + timedelta(days=90)
approval.save()
# Deny a request
approval.status = 'DENIED'
approval.denial_reason = "Medical necessity not established"
approval.denial_code = "D001"
approval.save()
```
### Uploading Documents
```python
from insurance_approvals.models import ApprovalDocument
document = ApprovalDocument.objects.create(
approval_request=approval,
document_type='MEDICAL_RECORDS',
title="Patient Medical History",
description="Complete medical history for past 6 months",
file=uploaded_file,
uploaded_by=request.user
)
```
### Logging Communications
```python
from insurance_approvals.models import ApprovalCommunicationLog
communication = ApprovalCommunicationLog.objects.create(
approval_request=approval,
communication_type='PHONE',
contact_person="Jane Smith, Insurance Rep",
contact_number="1-800-555-0123",
subject="Status inquiry for authorization",
message="Called to check on status of pending authorization",
response="Authorization is under review, expect decision within 48 hours",
outcome="Pending - follow up in 2 days",
follow_up_required=True,
follow_up_date=timezone.now().date() + timedelta(days=2),
communicated_by=request.user
)
```
## Integration with Existing Order Models
Add these methods to your order models (LabOrder, ImagingStudy, Prescription, etc.):
```python
from django.contrib.contenttypes.fields import GenericRelation
class LabOrder(models.Model):
# ... existing fields ...
# Add this field
approval_requests = GenericRelation(
'insurance_approvals.InsuranceApprovalRequest',
content_type_field='content_type',
object_id_field='object_id',
related_query_name='lab_order'
)
def requires_insurance_approval(self):
"""Check if this order requires insurance approval."""
return True # All orders require approval per requirements
def get_active_approval(self):
"""Get the active approval for this order."""
from django.utils import timezone
return self.approval_requests.filter(
status__in=['APPROVED', 'PARTIALLY_APPROVED', 'APPEAL_APPROVED'],
expiration_date__gte=timezone.now().date()
).first()
def has_valid_approval(self):
"""Check if order has a valid, non-expired approval."""
approval = self.get_active_approval()
return approval is not None and not approval.is_expired
def create_approval_request(self, user):
"""Helper method to create an approval request for this order."""
from insurance_approvals.models import InsuranceApprovalRequest
from django.contrib.contenttypes.models import ContentType
return InsuranceApprovalRequest.objects.create(
tenant=self.tenant,
patient=self.patient,
insurance_info=self.patient.insurance_info.first(),
content_type=ContentType.objects.get_for_model(self),
object_id=self.id,
request_type='LABORATORY',
service_description=self.get_service_description(),
procedure_codes=self.get_procedure_codes(),
diagnosis_codes=self.get_diagnosis_codes(),
clinical_justification=self.get_clinical_justification(),
requesting_provider=user,
service_start_date=self.order_datetime.date(),
priority=self.priority or 'ROUTINE'
)
```
## URL Patterns
The module provides these URL patterns:
```
/insurance-approvals/ # Dashboard
/insurance-approvals/list/ # List all approvals
/insurance-approvals/<id>/ # Detail view
/insurance-approvals/create/ # Create new approval
/insurance-approvals/<id>/edit/ # Edit approval
/insurance-approvals/<id>/submit/ # Submit for approval
/insurance-approvals/create-from-order/<ct_id>/<obj_id>/ # Create from order
/insurance-approvals/htmx/<id>/update-status/ # HTMX: Update status
/insurance-approvals/htmx/<id>/upload-document/ # HTMX: Upload document
/insurance-approvals/htmx/<id>/log-communication/ # HTMX: Log communication
/insurance-approvals/htmx/dashboard-stats/ # HTMX: Dashboard stats
```
## Admin Interface
Access the admin interface at:
```
http://127.0.0.1:8000/admin/insurance_approvals/
```
Features:
- Color-coded status badges
- Priority indicators
- Expiration warnings
- Inline document management
- Status history tracking
- Communication logs
- Advanced filtering and search
## Models
### InsuranceApprovalRequest
Main model for approval requests with:
- Patient and insurance information
- Service details and codes
- Clinical justification
- Status workflow
- Approval/denial details
- Expiration tracking
### ApprovalDocument
Supporting documents with:
- Document type classification
- File upload
- Metadata tracking
### ApprovalStatusHistory
Audit trail with:
- Status transitions
- Change reasons
- User tracking
- Timestamps
### ApprovalCommunicationLog
Communication tracking with:
- Communication type
- Contact information
- Message and response
- Follow-up management
### ApprovalTemplate
Reusable templates with:
- Insurance company specific
- Procedure specific
- Required documentation
- Usage tracking
## API Integration (Future)
The module is designed to support REST API integration:
```python
# Example API endpoint structure
GET /api/insurance-approvals/ # List approvals
POST /api/insurance-approvals/ # Create approval
GET /api/insurance-approvals/{id}/ # Get approval details
PUT /api/insurance-approvals/{id}/ # Update approval
PATCH /api/insurance-approvals/{id}/ # Partial update
DELETE /api/insurance-approvals/{id}/ # Delete approval
POST /api/insurance-approvals/{id}/submit/ # Submit approval
POST /api/insurance-approvals/{id}/documents/ # Upload document
GET /api/insurance-approvals/{id}/history/ # Get status history
```
## Best Practices
1. **Always create approval requests before processing orders**
2. **Upload all required documents before submission**
3. **Log all communications with insurance companies**
4. **Set appropriate priorities for urgent cases**
5. **Monitor expiration dates and renew before expiry**
6. **Use templates for common approval types**
7. **Keep clinical justifications detailed and specific**
8. **Track denial reasons for appeal preparation**
## Troubleshooting
### Common Issues
**Issue**: Approval request not showing in list
- **Solution**: Check tenant filtering, ensure user has correct tenant access
**Issue**: Cannot upload documents
- **Solution**: Check MEDIA_ROOT and MEDIA_URL settings, ensure directory permissions
**Issue**: Status not updating
- **Solution**: Check user permissions, ensure status transition is valid
**Issue**: Related order not showing
- **Solution**: Ensure GenericRelation is added to order model
## Support
For issues or questions:
1. Check this README
2. Review the admin interface
3. Check model documentation in `models.py`
4. Review view logic in `views.py`
## License
Part of the Hospital Management System v4 project.

View File

Some files were not shown because too many files have changed in this diff Show More