update
This commit is contained in:
parent
23158e9fbf
commit
84c1fb798e
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
@ -207,6 +207,26 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
("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)),
|
||||
],
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 17:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("analytics", "0002_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="datasource",
|
||||
name="last_test_duration_seconds",
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="datasource",
|
||||
name="last_test_end_at",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="datasource",
|
||||
name="last_test_error_message",
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="datasource",
|
||||
name="last_test_start_at",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="datasource",
|
||||
name="last_test_status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("PENDING", "Pending"),
|
||||
("RUNNING", "Running"),
|
||||
("SUCCESS", "Success"),
|
||||
("FAILURE", "Failure"),
|
||||
],
|
||||
default="PENDING",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -10,12 +10,20 @@ app_name = 'appointments'
|
||||
urlpatterns = [
|
||||
# Main views
|
||||
path('', views.AppointmentDashboardView.as_view(), name='dashboard'),
|
||||
path('list/', views.AppointmentListView.as_view(), name='appointment_list'),
|
||||
path('create/', views.AppointmentRequestCreateView.as_view(), name='appointment_create'),
|
||||
path('detail/<int:pk>/', views.AppointmentDetailView.as_view(), name='appointment_detail'),
|
||||
path('requests/', views.AppointmentListView.as_view(), name='appointment_list'),
|
||||
path('requests/create/', views.AppointmentRequestCreateView.as_view(), name='appointment_create'),
|
||||
path('requests/<int:pk>/detail/', views.AppointmentDetailView.as_view(), name='appointment_detail'),
|
||||
path('calendar/', views.SchedulingCalendarView.as_view(), name='scheduling_calendar'),
|
||||
path('queue/', views.QueueManagementView.as_view(), name='queue_management'),
|
||||
|
||||
# Telemedicine
|
||||
path('telemedicine/', views.TelemedicineView.as_view(), name='telemedicine'),
|
||||
path('telemedicine/create/', views.TelemedicineSessionCreateView.as_view(), name='telemedicine_session_create'),
|
||||
path('telemedicine/<int:pk>/', views.TelemedicineSessionDetailView.as_view(), name='telemedicine_session_detail'),
|
||||
path('telemedicine/<int:pk>/update/', views.TelemedicineSessionUpdateView.as_view(), name='telemedicine_session_update'),
|
||||
path('telemedicine/<int:pk>/start/', views.start_telemedicine_session, name='start_telemedicine_session'),
|
||||
path('telemedicine/<int:pk>/end/', views.end_telemedicine_session, name='stop_telemedicine_session'),
|
||||
path('telemedicine/<int:pk>/cancel/', views.cancel_telemedicine_session, name='cancel_telemedicine_session'),
|
||||
|
||||
# HTMX endpoints
|
||||
path('search/', views.appointment_search, name='appointment_search'),
|
||||
@ -27,9 +35,10 @@ urlpatterns = [
|
||||
# Actions
|
||||
path('check-in/<int:appointment_id>/', views.check_in_patient, name='check_in_patient'),
|
||||
path('queue/<int:queue_id>/call-next/', views.call_next_patient, name='call_next_patient'),
|
||||
path('telemedicine/<uuid:session_id>/start/', views.start_telemedicine_session, name='start_telemedicine_session'),
|
||||
|
||||
path('complete/<int:appointment_id>/', views.complete_appointment, name='complete_appointment'),
|
||||
path('reschedule/<int:appointment_id>/', views.reschedule_appointment, name='reschedule_appointment'),
|
||||
path('cancel/<int:appointment_id>/', views.cancel_appointment, name='cancel_appointment'),
|
||||
|
||||
# API endpoints
|
||||
# path('api/', include('appointments.api.urls')),
|
||||
|
||||
@ -31,10 +31,6 @@ from core.utils import AuditLogger
|
||||
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DASHBOARD VIEW
|
||||
# ============================================================================
|
||||
|
||||
class AppointmentDashboardView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
Appointment dashboard view.
|
||||
@ -85,10 +81,6 @@ class AppointmentDashboardView(LoginRequiredMixin, TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# APPOINTMENT REQUEST VIEWS (RESTRICTED CRUD - Clinical Data)
|
||||
# ============================================================================
|
||||
|
||||
class AppointmentRequestListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List appointment requests.
|
||||
@ -199,7 +191,7 @@ class AppointmentRequestCreateView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
"""
|
||||
model = AppointmentRequest
|
||||
form_class = AppointmentRequestForm
|
||||
template_name = 'appointments/appointment_request_form.html'
|
||||
template_name = 'appointments/requests/appointment_form.html'
|
||||
permission_required = 'appointments.add_appointmentrequest'
|
||||
success_url = reverse_lazy('appointments:appointment_request_list')
|
||||
|
||||
@ -235,7 +227,7 @@ class AppointmentRequestUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
"""
|
||||
model = AppointmentRequest
|
||||
form_class = AppointmentRequestForm
|
||||
template_name = 'appointments/appointment_request_form.html'
|
||||
template_name = 'appointments/requests/appointment_form.html'
|
||||
permission_required = 'appointments.change_appointmentrequest'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -276,7 +268,7 @@ class AppointmentRequestDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
Cancel appointment request.
|
||||
"""
|
||||
model = AppointmentRequest
|
||||
template_name = 'appointments/appointment_request_confirm_delete.html'
|
||||
template_name = 'appointments/requests/appointment_confirm_delete.html'
|
||||
permission_required = 'appointments.delete_appointmentrequest'
|
||||
success_url = reverse_lazy('appointments:appointment_request_list')
|
||||
|
||||
@ -309,10 +301,6 @@ class AppointmentRequestDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SLOT AVAILABILITY VIEWS (LIMITED CRUD - Operational Data)
|
||||
# ============================================================================
|
||||
|
||||
class SlotAvailabilityListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List slot availability.
|
||||
@ -514,10 +502,6 @@ class SlotAvailabilityDeleteView(LoginRequiredMixin, PermissionRequiredMixin, De
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# WAITING QUEUE VIEWS (FULL CRUD - Operational Data)
|
||||
# ============================================================================
|
||||
|
||||
class WaitingQueueListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List waiting queues.
|
||||
@ -733,10 +717,6 @@ class WaitingQueueDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Delete
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# QUEUE ENTRY VIEWS (LIMITED CRUD - Operational Data)
|
||||
# ============================================================================
|
||||
|
||||
class QueueEntryListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List queue entries.
|
||||
@ -878,10 +858,6 @@ class QueueEntryUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVi
|
||||
return response
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TELEMEDICINE SESSION VIEWS (RESTRICTED CRUD - Clinical Data)
|
||||
# ============================================================================
|
||||
|
||||
class TelemedicineSessionListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List telemedicine sessions.
|
||||
@ -935,7 +911,7 @@ class TelemedicineSessionDetailView(LoginRequiredMixin, DetailView):
|
||||
Display telemedicine session details.
|
||||
"""
|
||||
model = TelemedicineSession
|
||||
template_name = 'appointments/telemedicine_session_detail.html'
|
||||
template_name = 'appointments/telemedicine/telemedicine_session_detail.html'
|
||||
context_object_name = 'session'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -1021,10 +997,6 @@ class TelemedicineSessionUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
return response
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# APPOINTMENT TEMPLATE VIEWS (FULL CRUD - Master Data)
|
||||
# ============================================================================
|
||||
|
||||
class AppointmentTemplateListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List appointment templates.
|
||||
@ -1217,10 +1189,6 @@ class AppointmentTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HTMX VIEWS FOR REAL-TIME UPDATES
|
||||
# ============================================================================
|
||||
|
||||
@login_required
|
||||
def appointment_search(request):
|
||||
"""
|
||||
@ -1370,7 +1338,7 @@ def queue_status(request, queue_id):
|
||||
default=Value(None), output_field=IntegerField()
|
||||
),
|
||||
)
|
||||
.select_related('assigned_provider', 'patient', 'appointment') # adjust if you need more
|
||||
.select_related('assigned_provider', 'patient', 'appointment')
|
||||
.order_by('queue_position', 'updated_at')
|
||||
)
|
||||
|
||||
@ -1434,20 +1402,19 @@ def calendar_appointments(request):
|
||||
)
|
||||
|
||||
if provider_id:
|
||||
queryset = queryset.filter(provider_id=provider_id)
|
||||
queryset = queryset.filter(provider__id=provider_id)
|
||||
|
||||
appointments = queryset.order_by('scheduled_datetime')
|
||||
# providers = queryset.order_by('provider__first_name')
|
||||
|
||||
|
||||
return render(request, 'appointments/partials/calendar_appointments.html', {
|
||||
'appointments': appointments,
|
||||
'selected_date': selected_date
|
||||
'selected_date': selected_date,
|
||||
# 'providers': providers
|
||||
})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ACTION VIEWS FOR WORKFLOW OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
@login_required
|
||||
def confirm_appointment(request, pk):
|
||||
"""
|
||||
@ -1543,6 +1510,38 @@ def complete_appointment(request, pk):
|
||||
return redirect('appointments:appointment_request_detail', pk=pk)
|
||||
|
||||
|
||||
@login_required
|
||||
def cancel_appointment(request, pk):
|
||||
"""
|
||||
Complete an appointment.
|
||||
"""
|
||||
tenant = getattr(request, 'tenant', None)
|
||||
if not tenant:
|
||||
messages.error(request, 'No tenant found.')
|
||||
return redirect('appointments:appointment_request_list')
|
||||
|
||||
appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
|
||||
|
||||
appointment.status = 'CANCELLED'
|
||||
# appointment.actual_end_time = timezone.now()
|
||||
appointment.save()
|
||||
|
||||
# Log completion
|
||||
AuditLogger.log_event(
|
||||
tenant=tenant,
|
||||
event_type='UPDATE',
|
||||
event_category='APPOINTMENT_MANAGEMENT',
|
||||
action='Cancel Appointment',
|
||||
description=f'Cancelled appointment: {appointment.patient} with {appointment.provider}',
|
||||
user=request.user,
|
||||
content_object=appointment,
|
||||
request=request
|
||||
)
|
||||
|
||||
messages.success(request, f'Appointment for {appointment.patient} cancelled successfully.')
|
||||
return redirect('appointments:appointment_request_detail', pk=pk)
|
||||
|
||||
|
||||
@login_required
|
||||
def next_in_queue(request, queue_id):
|
||||
"""
|
||||
@ -1663,7 +1662,7 @@ def start_telemedicine_session(request, pk):
|
||||
)
|
||||
|
||||
messages.success(request, f'Telemedicine session started successfully.')
|
||||
return redirect('appointments:telemedicine_session_detail', pk=pk)
|
||||
return redirect('appointments:telemedicine_session_detail', pk=session.pk)
|
||||
|
||||
|
||||
@login_required
|
||||
@ -1702,18 +1701,17 @@ def end_telemedicine_session(request, pk):
|
||||
request=request
|
||||
)
|
||||
|
||||
messages.success(request, f'Telemedicine session ended successfully.')
|
||||
return redirect('appointments:telemedicine_session_detail', pk=pk)
|
||||
messages.success(request, 'Telemedicine session ended successfully.')
|
||||
return redirect('appointments:telemedicine_session_detail', pk=session.pk)
|
||||
|
||||
|
||||
|
||||
# Missing Views - Placeholder implementations
|
||||
class AppointmentListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List view for appointments.
|
||||
"""
|
||||
model = AppointmentRequest
|
||||
template_name = 'appointments/appointment_list.html'
|
||||
template_name = 'appointments/requests/appointment_list.html'
|
||||
context_object_name = 'appointments'
|
||||
paginate_by = 20
|
||||
|
||||
@ -1728,7 +1726,7 @@ class AppointmentDetailView(LoginRequiredMixin, DetailView):
|
||||
Detail view for appointments.
|
||||
"""
|
||||
model = AppointmentRequest
|
||||
template_name = 'appointments/appointment_detail.html'
|
||||
template_name = 'appointments/requests/appointment_request_detail.html'
|
||||
context_object_name = 'appointment'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -1775,11 +1773,19 @@ class QueueManagementView(LoginRequiredMixin, ListView):
|
||||
return context
|
||||
|
||||
|
||||
class TelemedicineView(LoginRequiredMixin, TemplateView):
|
||||
class TelemedicineView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
Telemedicine appointments view.
|
||||
"""
|
||||
template_name = 'appointments/telemedicine.html'
|
||||
model = TelemedicineSession
|
||||
template_name = 'appointments/telemedicine/telemedicine.html'
|
||||
context_object_name = 'sessions'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
return TelemedicineSession.objects.filter(
|
||||
appointment__tenant=self.request.user.tenant
|
||||
).order_by('-created_at')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -1790,6 +1796,38 @@ class TelemedicineView(LoginRequiredMixin, TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
@login_required
|
||||
def cancel_telemedicine_session(request, pk):
|
||||
tenant = getattr(request, 'tenant', None)
|
||||
if not tenant:
|
||||
messages.error(request, 'No tenant found.')
|
||||
return redirect('appointments:telemedicine_session_list')
|
||||
|
||||
session = get_object_or_404(TelemedicineSession, pk=pk)
|
||||
session.status = 'CANCELLED'
|
||||
session.save()
|
||||
|
||||
# Update appointment status
|
||||
session.appointment.status = 'CANCELLED'
|
||||
session.appointment.save()
|
||||
|
||||
# Log session start
|
||||
AuditLogger.log_event(
|
||||
tenant=tenant,
|
||||
event_type='UPDATE',
|
||||
event_category='APPOINTMENT_MANAGEMENT',
|
||||
action='Cancel Telemedicine Session',
|
||||
description='Cancelled telemedicine session',
|
||||
user=request.user,
|
||||
content_object=session,
|
||||
request=request
|
||||
)
|
||||
|
||||
messages.success(request, 'Telemedicine session cancelled successfully.')
|
||||
return redirect('appointments:telemedicine_session_detail', pk=session.pk)
|
||||
|
||||
|
||||
@login_required
|
||||
def check_in_patient(request, appointment_id):
|
||||
"""
|
||||
Check in a patient for their appointment.
|
||||
@ -1815,13 +1853,13 @@ def call_next_patient(request, queue_id):
|
||||
return redirect('appointments:queue_management')
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
def reschedule_appointment(request, appointment_id):
|
||||
"""
|
||||
Reschedule an appointment.
|
||||
"""
|
||||
appointment = get_object_or_404(AppointmentRequest,
|
||||
request_id=appointment_id,
|
||||
pk=appointment_id,
|
||||
tenant=request.user.tenant
|
||||
)
|
||||
|
||||
@ -1835,8 +1873,8 @@ def reschedule_appointment(request, appointment_id):
|
||||
appointment.status = 'RESCHEDULED'
|
||||
appointment.save()
|
||||
|
||||
messages.success(request, f'Appointment has been rescheduled to {new_date} at {new_time}.')
|
||||
return redirect('appointments:appointment_detail', pk=appointment_id)
|
||||
messages.success(request, 'Appointment has been rescheduled')
|
||||
return redirect('appointments:appointment_detail', pk=appointment.pk)
|
||||
|
||||
return render(request, 'appointments/reschedule_appointment.html', {
|
||||
'appointment': appointment
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import billing.utils
|
||||
import django.db.models.deletion
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -30,6 +30,9 @@ urlpatterns = [
|
||||
path('claims/', views.InsuranceClaimListView.as_view(), name='claim_list'),
|
||||
path('claims/<uuid:claim_id>/', views.InsuranceClaimDetailView.as_view(), name='claim_detail'),
|
||||
path('claims/create/', views.InsuranceClaimCreateView.as_view(), name='claim_create'),
|
||||
path('claims/<uuid:claim_id>/edit/', views.InsuranceClaimUpdateView.as_view(), name='claim_update'),
|
||||
path('claims/<uuid:claim_id>/appeal', views.claim_appeal, name='claim_appeal'),
|
||||
|
||||
path('bills/<uuid:bill_id>/claims/create/', views.InsuranceClaimCreateView.as_view(), name='bill_claim_create'),
|
||||
path('payments/<uuid:payment_id>/receipt/', views.payment_receipt, name='payment_receipt'),
|
||||
path('payments/<uuid:payment_id>/email/', views.payment_email, name='payment_email'),
|
||||
|
||||
631
billing/views.py
631
billing/views.py
@ -33,12 +33,9 @@ from patients.models import PatientProfile, InsuranceInfo
|
||||
from accounts.models import User
|
||||
from emr.models import Encounter
|
||||
from inpatients.models import Admission
|
||||
from core.utils import AuditLogger
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DASHBOARD VIEW
|
||||
# ============================================================================
|
||||
|
||||
class BillingDashboardView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
Billing dashboard view with comprehensive statistics and recent activity.
|
||||
@ -103,10 +100,6 @@ class BillingDashboardView(LoginRequiredMixin, TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MEDICAL BILL VIEWS
|
||||
# ============================================================================
|
||||
|
||||
class MedicalBillListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List view for medical bills with filtering and search.
|
||||
@ -311,10 +304,6 @@ class MedicalBillDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteV
|
||||
return response
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# INSURANCE CLAIM VIEWS
|
||||
# ============================================================================
|
||||
|
||||
class InsuranceClaimListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List view for insurance claims with filtering and search.
|
||||
@ -461,9 +450,65 @@ class InsuranceClaimCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
|
||||
return reverse('billing:claim_detail', kwargs={'claim_id': self.object.claim_id})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PAYMENT VIEWS
|
||||
# ============================================================================
|
||||
class InsuranceClaimUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||||
model = InsuranceClaim
|
||||
form_class = InsuranceClaimForm
|
||||
template_name = 'billing/claims/claim_form.html'
|
||||
permission_required = 'billing.add_insuranceclaim'
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['user'] = self.request.user
|
||||
|
||||
# Get medical bill from URL parameter
|
||||
bill_id = self.kwargs.get('bill_id')
|
||||
if bill_id:
|
||||
try:
|
||||
medical_bill = MedicalBill.objects.get(
|
||||
bill_id=bill_id,
|
||||
tenant=getattr(self.request, 'tenant', None)
|
||||
)
|
||||
kwargs['medical_bill'] = medical_bill
|
||||
except MedicalBill.DoesNotExist:
|
||||
pass
|
||||
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
# Prefer URL bill_id; otherwise read from POST("medical_bill")
|
||||
bill_id = self.kwargs.get('bill_id') or self.request.POST.get('medical_bill')
|
||||
if bill_id:
|
||||
try:
|
||||
medical_bill = MedicalBill.objects.get(
|
||||
bill_id=bill_id,
|
||||
tenant=getattr(self.request, 'tenant', None)
|
||||
)
|
||||
form.instance.medical_bill = medical_bill
|
||||
except MedicalBill.DoesNotExist:
|
||||
messages.error(self.request, 'Medical bill not found.')
|
||||
return redirect('billing:bill_list')
|
||||
else:
|
||||
messages.error(self.request, 'Please select a medical bill.')
|
||||
return redirect('billing:claim_create')
|
||||
|
||||
form.instance.created_by = self.request.user
|
||||
|
||||
if not form.instance.claim_number:
|
||||
form.instance.claim_number = form.instance.generate_claim_number()
|
||||
|
||||
response = super().form_valid(form)
|
||||
messages.success(self.request, f'Insurance claim {self.object.claim_number} updated successfully.')
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
tenant = getattr(self.request, 'tenant', None)
|
||||
ctx['available_bills'] = MedicalBill.objects.filter(tenant=tenant).select_related('patient')
|
||||
return ctx
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('billing:claim_detail', kwargs={'claim_id': self.object.claim_id})
|
||||
|
||||
|
||||
class PaymentListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
@ -598,40 +643,37 @@ class PaymentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
|
||||
return reverse('billing:payment_detail', kwargs={'payment_id': self.object.payment_id})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HTMX VIEWS
|
||||
# ============================================================================
|
||||
|
||||
# @login_required
|
||||
# def htmx_billing_stats(request):
|
||||
# """
|
||||
# HTMX endpoint for billing statistics.
|
||||
# """
|
||||
# tenant = getattr(request, 'tenant', None)
|
||||
# if not tenant:
|
||||
# return JsonResponse({'error': 'No tenant found'})
|
||||
#
|
||||
# today = timezone.now().date()
|
||||
#
|
||||
# # Calculate statistics
|
||||
# bills = MedicalBill.objects.filter(tenant=tenant)
|
||||
# stats = {
|
||||
# 'total_bills': bills.count(),
|
||||
# 'total_revenue': float(bills.aggregate(
|
||||
# total=Sum('total_amount')
|
||||
# )['total'] or 0),
|
||||
# 'total_paid': float(bills.aggregate(
|
||||
# total=Sum('paid_amount')
|
||||
# )['total'] or 0),
|
||||
# 'overdue_bills': bills.filter(
|
||||
# due_date__lt=today,
|
||||
# status__in=['draft', 'sent', 'partial_payment']
|
||||
# ).count()
|
||||
# }
|
||||
#
|
||||
# stats['total_outstanding'] = stats['total_revenue'] - stats['total_paid']
|
||||
#
|
||||
# return JsonResponse(stats)
|
||||
@login_required
|
||||
def htmx_billing_stats(request):
|
||||
"""
|
||||
HTMX endpoint for billing statistics.
|
||||
"""
|
||||
tenant = getattr(request, 'tenant', None)
|
||||
if not tenant:
|
||||
return JsonResponse({'error': 'No tenant found'})
|
||||
|
||||
today = timezone.now().date()
|
||||
|
||||
# Calculate statistics
|
||||
bills = MedicalBill.objects.filter(tenant=tenant)
|
||||
stats = {
|
||||
'total_bills': bills.count(),
|
||||
'total_revenue': float(bills.aggregate(
|
||||
total=Sum('total_amount')
|
||||
)['total'] or 0),
|
||||
'total_paid': float(bills.aggregate(
|
||||
total=Sum('paid_amount')
|
||||
)['total'] or 0),
|
||||
'overdue_bills': bills.filter(
|
||||
due_date__lt=today,
|
||||
status__in=['draft', 'sent', 'partial_payment']
|
||||
).count()
|
||||
}
|
||||
|
||||
stats['total_outstanding'] = stats['total_revenue'] - stats['total_paid']
|
||||
|
||||
return JsonResponse(stats)
|
||||
|
||||
@login_required
|
||||
def billing_stats(request):
|
||||
@ -715,6 +757,7 @@ def bill_details_api(request, bill_id):
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
@login_required
|
||||
def bill_search(request):
|
||||
"""
|
||||
@ -738,10 +781,6 @@ def bill_search(request):
|
||||
return render(request, 'billing/partials/bill_list.html', {'bills': bills})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ACTION VIEWS
|
||||
# ============================================================================
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def submit_bill(request, bill_id):
|
||||
@ -768,50 +807,65 @@ def submit_bill(request, bill_id):
|
||||
return JsonResponse({'success': False, 'error': 'Bill not found'})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# EXPORT VIEWS
|
||||
# ============================================================================
|
||||
|
||||
@login_required
|
||||
def export_bills(request):
|
||||
"""
|
||||
Export bills to CSV.
|
||||
Export medical bills to CSV.
|
||||
"""
|
||||
tenant = getattr(request, 'tenant', None)
|
||||
if not tenant:
|
||||
return HttpResponse('No tenant found', status=400)
|
||||
tenant = request.user.tenant
|
||||
|
||||
# Create CSV response
|
||||
# Create HTTP response with CSV content type
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
response['Content-Disposition'] = 'attachment; filename="medical_bills.csv"'
|
||||
|
||||
writer = csv.writer(response)
|
||||
|
||||
# Write header row
|
||||
writer.writerow([
|
||||
'Bill Number', 'Patient Name', 'Bill Date', 'Due Date',
|
||||
'Total Amount', 'Paid Amount', 'Balance', 'Status'
|
||||
'Bill Number', 'Patient Name', 'MRN', 'Bill Type', 'Bill Date', 'Due Date',
|
||||
'Service Date From', 'Service Date To', 'Subtotal', 'Tax Amount', 'Total Amount',
|
||||
'Paid Amount', 'Balance Amount', 'Status', 'Attending Provider', 'Created Date'
|
||||
])
|
||||
|
||||
bills = MedicalBill.objects.filter(tenant=tenant).select_related('patient')
|
||||
# Write data rows
|
||||
bills = MedicalBill.objects.filter(
|
||||
tenant=tenant
|
||||
).select_related(
|
||||
'patient', 'attending_provider'
|
||||
).order_by('-bill_date')
|
||||
|
||||
for bill in bills:
|
||||
writer.writerow([
|
||||
bill.bill_number,
|
||||
bill.patient.get_full_name(),
|
||||
bill.patient.mrn,
|
||||
bill.get_bill_type_display(),
|
||||
bill.bill_date.strftime('%Y-%m-%d'),
|
||||
bill.due_date.strftime('%Y-%m-%d') if bill.due_date else '',
|
||||
bill.due_date.strftime('%Y-%m-%d'),
|
||||
bill.service_date_from.strftime('%Y-%m-%d'),
|
||||
bill.service_date_to.strftime('%Y-%m-%d'),
|
||||
str(bill.subtotal),
|
||||
str(bill.tax_amount),
|
||||
str(bill.total_amount),
|
||||
str(bill.paid_amount),
|
||||
str(bill.balance_amount),
|
||||
bill.get_status_display()
|
||||
bill.get_status_display(),
|
||||
bill.attending_provider.get_full_name() if bill.attending_provider else '',
|
||||
bill.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
])
|
||||
|
||||
# Log audit event
|
||||
AuditLogger.log_event(
|
||||
request.user,
|
||||
'BILLS_EXPORTED',
|
||||
'MedicalBill',
|
||||
None,
|
||||
f"Exported {bills.count()} medical bills to CSV"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PRINT VIEWS
|
||||
# ============================================================================
|
||||
|
||||
@login_required
|
||||
def print_bills(request):
|
||||
"""
|
||||
@ -1127,7 +1181,7 @@ def payment_receipt(request, payment_id):
|
||||
# Get payment with related objects
|
||||
payment = Payment.objects.select_related(
|
||||
'medical_bill', 'medical_bill__patient', 'processed_by'
|
||||
).get(id=payment_id, medical_bill__tenant=tenant)
|
||||
).get(payment_id=payment_id, medical_bill__tenant=tenant)
|
||||
|
||||
# Calculate payment details
|
||||
payment_details = {
|
||||
@ -1555,8 +1609,23 @@ def bill_line_items_api(request, bill_id=None):
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def claim_appeal(request, claim_id):
|
||||
tenant = getattr(request, 'tenant', None)
|
||||
if not tenant:
|
||||
return HttpResponse('No tenant found', status=400)
|
||||
|
||||
#
|
||||
claim = get_object_or_404(
|
||||
InsuranceClaim,
|
||||
medical_bill__tenant=tenant,
|
||||
claim_id=claim_id
|
||||
)
|
||||
if claim.status in ['DENIED', 'REJECTED']:
|
||||
claim.status = 'APPEALED'
|
||||
claim.save()
|
||||
messages.success(request, 'Claim has already been appealed.')
|
||||
return redirect('billing:claim_detail', claim_id=claim.claim_id)
|
||||
return JsonResponse({'success': False, 'error': 'check claim status'}, status=400)
|
||||
#
|
||||
# """
|
||||
# Billing app views for hospital management system.
|
||||
@ -2040,83 +2109,83 @@ def bill_line_items_api(request, bill_id=None):
|
||||
# }
|
||||
#
|
||||
# return render(request, 'billing/partials/billing_stats.html', {'stats': stats})
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# def htmx_bill_search(request):
|
||||
# """
|
||||
# HTMX view for medical bill search.
|
||||
# """
|
||||
# tenant = request.user.tenant
|
||||
# search = request.GET.get('search', '')
|
||||
#
|
||||
# bills = MedicalBill.objects.filter(tenant=tenant)
|
||||
#
|
||||
# if search:
|
||||
# bills = bills.filter(
|
||||
# Q(bill_number__icontains=search) |
|
||||
# Q(patient__first_name__icontains=search) |
|
||||
# Q(patient__last_name__icontains=search) |
|
||||
# Q(patient__mrn__icontains=search)
|
||||
# )
|
||||
#
|
||||
# bills = bills.select_related(
|
||||
# 'patient', 'encounter', 'attending_provider'
|
||||
# ).order_by('-bill_date')[:10]
|
||||
#
|
||||
# return render(request, 'billing/partials/bill_list.html', {'bills': bills})
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# def htmx_payment_search(request):
|
||||
# """
|
||||
# HTMX view for payment search.
|
||||
# """
|
||||
# tenant = request.user.tenant
|
||||
# search = request.GET.get('search', '')
|
||||
#
|
||||
# payments = Payment.objects.filter(medical_bill__tenant=tenant)
|
||||
#
|
||||
# if search:
|
||||
# payments = payments.filter(
|
||||
# Q(payment_number__icontains=search) |
|
||||
# Q(medical_bill__bill_number__icontains=search) |
|
||||
# Q(medical_bill__patient__first_name__icontains=search) |
|
||||
# Q(medical_bill__patient__last_name__icontains=search)
|
||||
# )
|
||||
#
|
||||
# payments = payments.select_related(
|
||||
# 'medical_bill', 'medical_bill__patient'
|
||||
# ).order_by('-payment_date')[:10]
|
||||
#
|
||||
# return render(request, 'billing/partials/payment_list.html', {'payments': payments})
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# def htmx_claim_search(request):
|
||||
# """
|
||||
# HTMX view for insurance claim search.
|
||||
# """
|
||||
# tenant = request.user.tenant
|
||||
# search = request.GET.get('search', '')
|
||||
#
|
||||
# claims = InsuranceClaim.objects.filter(medical_bill__tenant=tenant)
|
||||
#
|
||||
# if search:
|
||||
# claims = claims.filter(
|
||||
# Q(claim_number__icontains=search) |
|
||||
# Q(medical_bill__bill_number__icontains=search) |
|
||||
# Q(medical_bill__patient__first_name__icontains=search) |
|
||||
# Q(medical_bill__patient__last_name__icontains=search)
|
||||
# )
|
||||
#
|
||||
# claims = claims.select_related(
|
||||
# 'medical_bill', 'medical_bill__patient', 'insurance_info'
|
||||
# ).order_by('-submission_date')[:10]
|
||||
#
|
||||
# return render(request, 'billing/partials/claim_list.html', {'claims': claims})
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
@login_required
|
||||
def htmx_bill_search(request):
|
||||
"""
|
||||
HTMX view for medical bill search.
|
||||
"""
|
||||
tenant = request.user.tenant
|
||||
search = request.GET.get('search', '')
|
||||
|
||||
bills = MedicalBill.objects.filter(tenant=tenant)
|
||||
|
||||
if search:
|
||||
bills = bills.filter(
|
||||
Q(bill_number__icontains=search) |
|
||||
Q(patient__first_name__icontains=search) |
|
||||
Q(patient__last_name__icontains=search) |
|
||||
Q(patient__mrn__icontains=search)
|
||||
)
|
||||
|
||||
bills = bills.select_related(
|
||||
'patient', 'encounter', 'attending_provider'
|
||||
).order_by('-bill_date')[:10]
|
||||
|
||||
return render(request, 'billing/partials/bill_list.html', {'bills': bills})
|
||||
|
||||
|
||||
@login_required
|
||||
def htmx_payment_search(request):
|
||||
"""
|
||||
HTMX view for payment search.
|
||||
"""
|
||||
tenant = request.user.tenant
|
||||
search = request.GET.get('search', '')
|
||||
|
||||
payments = Payment.objects.filter(medical_bill__tenant=tenant)
|
||||
|
||||
if search:
|
||||
payments = payments.filter(
|
||||
Q(payment_number__icontains=search) |
|
||||
Q(medical_bill__bill_number__icontains=search) |
|
||||
Q(medical_bill__patient__first_name__icontains=search) |
|
||||
Q(medical_bill__patient__last_name__icontains=search)
|
||||
)
|
||||
|
||||
payments = payments.select_related(
|
||||
'medical_bill', 'medical_bill__patient'
|
||||
).order_by('-payment_date')[:10]
|
||||
|
||||
return render(request, 'billing/partials/payment_list.html', {'payments': payments})
|
||||
|
||||
|
||||
@login_required
|
||||
def htmx_claim_search(request):
|
||||
"""
|
||||
HTMX view for insurance claim search.
|
||||
"""
|
||||
tenant = request.user.tenant
|
||||
search = request.GET.get('search', '')
|
||||
|
||||
claims = InsuranceClaim.objects.filter(medical_bill__tenant=tenant)
|
||||
|
||||
if search:
|
||||
claims = claims.filter(
|
||||
Q(claim_number__icontains=search) |
|
||||
Q(medical_bill__bill_number__icontains=search) |
|
||||
Q(medical_bill__patient__first_name__icontains=search) |
|
||||
Q(medical_bill__patient__last_name__icontains=search)
|
||||
)
|
||||
|
||||
claims = claims.select_related(
|
||||
'medical_bill', 'medical_bill__patient', 'insurance_info'
|
||||
).order_by('-submission_date')[:10]
|
||||
|
||||
return render(request, 'billing/partials/claim_list.html', {'claims': claims})
|
||||
|
||||
|
||||
# # Action Views
|
||||
# @login_required
|
||||
# @require_http_methods(["POST"])
|
||||
@ -2146,171 +2215,115 @@ def bill_line_items_api(request, bill_id=None):
|
||||
# messages.success(request, 'Medical bill submitted successfully')
|
||||
#
|
||||
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def process_payment(request, bill_id):
|
||||
"""
|
||||
Process payment for medical bill.
|
||||
"""
|
||||
bill = get_object_or_404(
|
||||
MedicalBill,
|
||||
bill_id=bill_id,
|
||||
tenant=request.user.tenant
|
||||
)
|
||||
|
||||
payment_amount = Decimal(request.POST.get('payment_amount', '0.00'))
|
||||
payment_method = request.POST.get('payment_method', 'CASH')
|
||||
payment_source = request.POST.get('payment_source', 'PATIENT')
|
||||
|
||||
if payment_amount > 0:
|
||||
# Create payment record
|
||||
payment = Payment.objects.create(
|
||||
medical_bill=bill,
|
||||
payment_amount=payment_amount,
|
||||
payment_method=payment_method,
|
||||
payment_source=payment_source,
|
||||
payment_date=timezone.now().date(),
|
||||
received_by=request.user,
|
||||
processed_by=request.user,
|
||||
status='PROCESSED'
|
||||
)
|
||||
|
||||
# Update bill paid amount and status
|
||||
bill.paid_amount += payment_amount
|
||||
bill.balance_amount = bill.total_amount - bill.paid_amount
|
||||
|
||||
if bill.balance_amount <= 0:
|
||||
bill.status = 'PAID'
|
||||
elif bill.paid_amount > 0:
|
||||
bill.status = 'PARTIAL_PAID'
|
||||
|
||||
bill.save()
|
||||
|
||||
# Log audit event
|
||||
AuditLogger.log_event(
|
||||
request.user,
|
||||
'PAYMENT_PROCESSED',
|
||||
'Payment',
|
||||
str(payment.payment_id),
|
||||
f"Processed payment {payment.payment_number} for ${payment_amount} on bill {bill.bill_number}"
|
||||
)
|
||||
|
||||
messages.success(request, f'Payment of ${payment_amount} processed successfully')
|
||||
|
||||
return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# @require_http_methods(["POST"])
|
||||
# def process_payment(request, bill_id):
|
||||
# """
|
||||
# Process payment for medical bill.
|
||||
# """
|
||||
# bill = get_object_or_404(
|
||||
# MedicalBill,
|
||||
# bill_id=bill_id,
|
||||
# tenant=request.user.tenant
|
||||
# )
|
||||
#
|
||||
# payment_amount = Decimal(request.POST.get('payment_amount', '0.00'))
|
||||
# payment_method = request.POST.get('payment_method', 'CASH')
|
||||
# payment_source = request.POST.get('payment_source', 'PATIENT')
|
||||
#
|
||||
# if payment_amount > 0:
|
||||
# # Create payment record
|
||||
# payment = Payment.objects.create(
|
||||
# medical_bill=bill,
|
||||
# payment_amount=payment_amount,
|
||||
# payment_method=payment_method,
|
||||
# payment_source=payment_source,
|
||||
# payment_date=timezone.now().date(),
|
||||
# received_by=request.user,
|
||||
# processed_by=request.user,
|
||||
# status='PROCESSED'
|
||||
# )
|
||||
#
|
||||
# # Update bill paid amount and status
|
||||
# bill.paid_amount += payment_amount
|
||||
# bill.balance_amount = bill.total_amount - bill.paid_amount
|
||||
#
|
||||
# if bill.balance_amount <= 0:
|
||||
# bill.status = 'PAID'
|
||||
# elif bill.paid_amount > 0:
|
||||
# bill.status = 'PARTIAL_PAID'
|
||||
#
|
||||
# bill.save()
|
||||
#
|
||||
# # Log audit event
|
||||
# AuditLogger.log_event(
|
||||
# request.user,
|
||||
# 'PAYMENT_PROCESSED',
|
||||
# 'Payment',
|
||||
# str(payment.payment_id),
|
||||
# f"Processed payment {payment.payment_number} for ${payment_amount} on bill {bill.bill_number}"
|
||||
# )
|
||||
#
|
||||
# messages.success(request, f'Payment of ${payment_amount} processed successfully')
|
||||
#
|
||||
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# @require_http_methods(["POST"])
|
||||
# def submit_insurance_claim(request, bill_id):
|
||||
# """
|
||||
# Submit insurance claim for medical bill.
|
||||
# """
|
||||
# bill = get_object_or_404(
|
||||
# MedicalBill,
|
||||
# bill_id=bill_id,
|
||||
# tenant=request.user.tenant
|
||||
# )
|
||||
#
|
||||
# insurance_type = request.POST.get('insurance_type', 'PRIMARY')
|
||||
#
|
||||
# # Determine which insurance to use
|
||||
# if insurance_type == 'PRIMARY' and bill.primary_insurance:
|
||||
# insurance_info = bill.primary_insurance
|
||||
# claim_type = 'PRIMARY'
|
||||
# elif insurance_type == 'SECONDARY' and bill.secondary_insurance:
|
||||
# insurance_info = bill.secondary_insurance
|
||||
# claim_type = 'SECONDARY'
|
||||
# else:
|
||||
# messages.error(request, 'No insurance information available for claim submission')
|
||||
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
||||
#
|
||||
# # Create insurance claim
|
||||
# claim = InsuranceClaim.objects.create(
|
||||
# medical_bill=bill,
|
||||
# insurance_info=insurance_info,
|
||||
# claim_type=claim_type,
|
||||
# submission_date=timezone.now().date(),
|
||||
# service_date_from=bill.service_date_from,
|
||||
# service_date_to=bill.service_date_to,
|
||||
# billed_amount=bill.total_amount,
|
||||
# status='SUBMITTED',
|
||||
# created_by=request.user
|
||||
# )
|
||||
#
|
||||
# # Log audit event
|
||||
# AuditLogger.log_event(
|
||||
# request.user,
|
||||
# 'INSURANCE_CLAIM_SUBMITTED',
|
||||
# 'InsuranceClaim',
|
||||
# str(claim.claim_id),
|
||||
# f"Submitted {claim_type.lower()} insurance claim {claim.claim_number} for bill {bill.bill_number}"
|
||||
# )
|
||||
#
|
||||
# messages.success(request, f'{claim_type.title()} insurance claim submitted successfully')
|
||||
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def submit_insurance_claim(request, bill_id):
|
||||
"""
|
||||
Submit insurance claim for medical bill.
|
||||
"""
|
||||
bill = get_object_or_404(
|
||||
MedicalBill,
|
||||
bill_id=bill_id,
|
||||
tenant=request.user.tenant
|
||||
)
|
||||
|
||||
insurance_type = request.POST.get('insurance_type', 'PRIMARY')
|
||||
|
||||
# Determine which insurance to use
|
||||
if insurance_type == 'PRIMARY' and bill.primary_insurance:
|
||||
insurance_info = bill.primary_insurance
|
||||
claim_type = 'PRIMARY'
|
||||
elif insurance_type == 'SECONDARY' and bill.secondary_insurance:
|
||||
insurance_info = bill.secondary_insurance
|
||||
claim_type = 'SECONDARY'
|
||||
else:
|
||||
messages.error(request, 'No insurance information available for claim submission')
|
||||
return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
||||
|
||||
# Create insurance claim
|
||||
claim = InsuranceClaim.objects.create(
|
||||
medical_bill=bill,
|
||||
insurance_info=insurance_info,
|
||||
claim_type=claim_type,
|
||||
submission_date=timezone.now().date(),
|
||||
service_date_from=bill.service_date_from,
|
||||
service_date_to=bill.service_date_to,
|
||||
billed_amount=bill.total_amount,
|
||||
status='SUBMITTED',
|
||||
created_by=request.user
|
||||
)
|
||||
|
||||
# Log audit event
|
||||
AuditLogger.log_event(
|
||||
request.user,
|
||||
'INSURANCE_CLAIM_SUBMITTED',
|
||||
'InsuranceClaim',
|
||||
str(claim.claim_id),
|
||||
f"Submitted {claim_type.lower()} insurance claim {claim.claim_number} for bill {bill.bill_number}"
|
||||
)
|
||||
|
||||
messages.success(request, f'{claim_type.title()} insurance claim submitted successfully')
|
||||
return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
||||
#
|
||||
#
|
||||
# # Export Views
|
||||
# @login_required
|
||||
# def export_bills(request):
|
||||
# """
|
||||
# Export medical bills to CSV.
|
||||
# """
|
||||
# tenant = request.user.tenant
|
||||
#
|
||||
# # Create HTTP response with CSV content type
|
||||
# response = HttpResponse(content_type='text/csv')
|
||||
# response['Content-Disposition'] = 'attachment; filename="medical_bills.csv"'
|
||||
#
|
||||
# writer = csv.writer(response)
|
||||
#
|
||||
# # Write header row
|
||||
# writer.writerow([
|
||||
# 'Bill Number', 'Patient Name', 'MRN', 'Bill Type', 'Bill Date', 'Due Date',
|
||||
# 'Service Date From', 'Service Date To', 'Subtotal', 'Tax Amount', 'Total Amount',
|
||||
# 'Paid Amount', 'Balance Amount', 'Status', 'Attending Provider', 'Created Date'
|
||||
# ])
|
||||
#
|
||||
# # Write data rows
|
||||
# bills = MedicalBill.objects.filter(
|
||||
# tenant=tenant
|
||||
# ).select_related(
|
||||
# 'patient', 'attending_provider'
|
||||
# ).order_by('-bill_date')
|
||||
#
|
||||
# for bill in bills:
|
||||
# writer.writerow([
|
||||
# bill.bill_number,
|
||||
# bill.patient.get_full_name(),
|
||||
# bill.patient.mrn,
|
||||
# bill.get_bill_type_display(),
|
||||
# bill.bill_date.strftime('%Y-%m-%d'),
|
||||
# bill.due_date.strftime('%Y-%m-%d'),
|
||||
# bill.service_date_from.strftime('%Y-%m-%d'),
|
||||
# bill.service_date_to.strftime('%Y-%m-%d'),
|
||||
# str(bill.subtotal),
|
||||
# str(bill.tax_amount),
|
||||
# str(bill.total_amount),
|
||||
# str(bill.paid_amount),
|
||||
# str(bill.balance_amount),
|
||||
# bill.get_status_display(),
|
||||
# bill.attending_provider.get_full_name() if bill.attending_provider else '',
|
||||
# bill.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
# ])
|
||||
#
|
||||
# # Log audit event
|
||||
# AuditLogger.log_event(
|
||||
# request.user,
|
||||
# 'BILLS_EXPORTED',
|
||||
# 'MedicalBill',
|
||||
# None,
|
||||
# f"Exported {bills.count()} medical bills to CSV"
|
||||
# )
|
||||
#
|
||||
# return response
|
||||
|
||||
#
|
||||
#
|
||||
# # Legacy view functions for backward compatibility
|
||||
|
||||
Binary file not shown.
@ -3,7 +3,7 @@ from accounts.models import User
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from patients.models import PatientProfile
|
||||
from core.models import Department
|
||||
from hr.models import Department
|
||||
from .models import (
|
||||
BloodGroup, Donor, BloodComponent, BloodUnit, BloodTest, CrossMatch,
|
||||
BloodRequest, BloodIssue, Transfusion, AdverseReaction, InventoryLocation,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,26 +0,0 @@
|
||||
# Generated by Django 5.2.4 on 2025-09-04 15:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("blood_bank", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="donor",
|
||||
name="national_id",
|
||||
field=models.CharField(default=1129632798, max_length=10, unique=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="donor",
|
||||
name="gender",
|
||||
field=models.CharField(
|
||||
choices=[("M", "Male"), ("F", "Female"), ("O", "Other")], max_length=10
|
||||
),
|
||||
),
|
||||
]
|
||||
297
blood_bank/migrations/0002_initial.py
Normal file
297
blood_bank/migrations/0002_initial.py
Normal file
@ -0,0 +1,297 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
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")},
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
BIN
blood_bank/migrations/__pycache__/0002_initial.cpython-312.pyc
Normal file
BIN
blood_bank/migrations/__pycache__/0002_initial.cpython-312.pyc
Normal file
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,5 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from .models import Tenant, AuditLogEntry, SystemConfiguration, SystemNotification, IntegrationLog, Department
|
||||
from .models import Tenant, AuditLogEntry, SystemConfiguration, SystemNotification, IntegrationLog
|
||||
|
||||
|
||||
@admin.register(Tenant)
|
||||
@ -29,51 +29,51 @@ class TenantAdmin(admin.ModelAdmin):
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Department)
|
||||
class DepartmentAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for Department model"""
|
||||
list_display = ['name', 'code', 'department_type', 'department_head', 'is_active', 'tenant']
|
||||
list_filter = ['department_type', 'is_active', 'tenant', 'created_at']
|
||||
search_fields = ['name', 'code', 'description']
|
||||
readonly_fields = ['department_id', 'created_at', 'updated_at']
|
||||
autocomplete_fields = ['parent_department', 'department_head', 'created_by']
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('tenant', 'code', 'name', 'description', 'department_type')
|
||||
}),
|
||||
('Organizational Structure', {
|
||||
'fields': ('parent_department', 'department_head')
|
||||
}),
|
||||
('Contact Information', {
|
||||
'fields': ('phone', 'extension', 'email')
|
||||
}),
|
||||
('Location', {
|
||||
'fields': ('building', 'floor', 'wing', 'room_numbers')
|
||||
}),
|
||||
('Operations', {
|
||||
'fields': ('is_active', 'is_24_hour', 'operating_hours')
|
||||
}),
|
||||
('Financial', {
|
||||
'fields': ('cost_center_code', 'budget_code')
|
||||
}),
|
||||
('Staffing', {
|
||||
'fields': ('authorized_positions', 'current_staff_count')
|
||||
}),
|
||||
('Quality & Compliance', {
|
||||
'fields': ('accreditation_required', 'accreditation_body', 'last_inspection_date', 'next_inspection_date')
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('department_id', 'created_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
})
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Filter by tenant if user has tenant"""
|
||||
qs = super().get_queryset(request)
|
||||
if hasattr(request.user, 'tenant') and request.user.tenant:
|
||||
qs = qs.filter(tenant=request.user.tenant)
|
||||
return qs
|
||||
# @admin.register(Department)
|
||||
# class DepartmentAdmin(admin.ModelAdmin):
|
||||
# """Admin interface for Department model"""
|
||||
# list_display = ['name', 'code', 'department_type', 'department_head', 'is_active', 'tenant']
|
||||
# list_filter = ['department_type', 'is_active', 'tenant', 'created_at']
|
||||
# search_fields = ['name', 'code', 'description']
|
||||
# readonly_fields = ['department_id', 'created_at', 'updated_at']
|
||||
# autocomplete_fields = ['parent_department', 'department_head', 'created_by']
|
||||
# fieldsets = (
|
||||
# ('Basic Information', {
|
||||
# 'fields': ('tenant', 'code', 'name', 'description', 'department_type')
|
||||
# }),
|
||||
# ('Organizational Structure', {
|
||||
# 'fields': ('parent_department', 'department_head')
|
||||
# }),
|
||||
# ('Contact Information', {
|
||||
# 'fields': ('phone', 'extension', 'email')
|
||||
# }),
|
||||
# ('Location', {
|
||||
# 'fields': ('building', 'floor', 'wing', 'room_numbers')
|
||||
# }),
|
||||
# ('Operations', {
|
||||
# 'fields': ('is_active', 'is_24_hour', 'operating_hours')
|
||||
# }),
|
||||
# ('Financial', {
|
||||
# 'fields': ('cost_center_code', 'budget_code')
|
||||
# }),
|
||||
# ('Staffing', {
|
||||
# 'fields': ('authorized_positions', 'current_staff_count')
|
||||
# }),
|
||||
# ('Quality & Compliance', {
|
||||
# 'fields': ('accreditation_required', 'accreditation_body', 'last_inspection_date', 'next_inspection_date')
|
||||
# }),
|
||||
# ('Metadata', {
|
||||
# 'fields': ('department_id', 'created_by', 'created_at', 'updated_at'),
|
||||
# 'classes': ('collapse',)
|
||||
# })
|
||||
# )
|
||||
#
|
||||
# def get_queryset(self, request):
|
||||
# """Filter by tenant if user has tenant"""
|
||||
# qs = super().get_queryset(request)
|
||||
# if hasattr(request.user, 'tenant') and request.user.tenant:
|
||||
# qs = qs.filter(tenant=request.user.tenant)
|
||||
# return qs
|
||||
|
||||
|
||||
@admin.register(AuditLogEntry)
|
||||
|
||||
206
core/forms.py
206
core/forms.py
@ -6,9 +6,9 @@ notifications, departments, and search functionality.
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from .models import Tenant, SystemConfiguration, SystemNotification, Department
|
||||
|
||||
User = get_user_model()
|
||||
from .models import Tenant, SystemConfiguration, SystemNotification
|
||||
from accounts.models import User
|
||||
from hr.models import Department
|
||||
|
||||
|
||||
class TenantForm(forms.ModelForm):
|
||||
@ -227,106 +227,106 @@ class SystemNotificationForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
|
||||
class DepartmentForm(forms.ModelForm):
|
||||
"""Form for creating and managing hospital departments."""
|
||||
|
||||
class Meta:
|
||||
model = Department
|
||||
fields = [
|
||||
'name', 'code', 'description', 'department_type',
|
||||
'department_head', 'parent_department', 'building', 'floor',
|
||||
'wing', 'room_numbers', 'phone', 'extension',
|
||||
'email', 'is_active', 'is_24_hour'
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Enter department name'
|
||||
}),
|
||||
'code': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Enter department code'
|
||||
}),
|
||||
'description': forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 3,
|
||||
'placeholder': 'Enter department description'
|
||||
}),
|
||||
'department_type': forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
}),
|
||||
'department_head': forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
}),
|
||||
'parent_department': forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
}),
|
||||
'building': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Enter building name/number'
|
||||
}),
|
||||
'floor': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Enter floor'
|
||||
}),
|
||||
'wing': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Enter wing/section'
|
||||
}),
|
||||
'room_numbers': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Enter room numbers'
|
||||
}),
|
||||
'phone': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Enter phone number'
|
||||
}),
|
||||
'extension': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Enter extension'
|
||||
}),
|
||||
'email': forms.EmailInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Enter email address'
|
||||
}),
|
||||
'is_active': forms.CheckboxInput(attrs={
|
||||
'class': 'form-check-input'
|
||||
}),
|
||||
'is_24_hour': forms.CheckboxInput(attrs={
|
||||
'class': 'form-check-input'
|
||||
})
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.tenant = kwargs.pop('tenant', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.tenant:
|
||||
# Filter department head by tenant and medical staff
|
||||
self.fields['department_head'].queryset = User.objects.filter(
|
||||
tenant=self.tenant,
|
||||
is_active=True,
|
||||
role__in=['DOCTOR', 'NURSE_MANAGER', 'ADMINISTRATOR']
|
||||
)
|
||||
# Filter parent department by tenant
|
||||
self.fields['parent_department'].queryset = Department.objects.filter(
|
||||
tenant=self.tenant,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
def clean_code(self):
|
||||
"""Validate department code uniqueness within tenant."""
|
||||
code = self.cleaned_data.get('code')
|
||||
if code and self.tenant:
|
||||
queryset = Department.objects.filter(
|
||||
tenant=self.tenant,
|
||||
code=code
|
||||
)
|
||||
if self.instance.pk:
|
||||
queryset = queryset.exclude(pk=self.instance.pk)
|
||||
if queryset.exists():
|
||||
raise forms.ValidationError('A department with this code already exists.')
|
||||
return code
|
||||
# class DepartmentForm(forms.ModelForm):
|
||||
# """Form for creating and managing hospital departments."""
|
||||
#
|
||||
# class Meta:
|
||||
# model = Department
|
||||
# fields = [
|
||||
# 'name', 'code', 'description', 'department_type',
|
||||
# 'department_head', 'parent_department', 'building', 'floor',
|
||||
# 'wing', 'room_numbers', 'phone', 'extension',
|
||||
# 'email', 'is_active', 'is_24_hour'
|
||||
# ]
|
||||
# widgets = {
|
||||
# 'name': forms.TextInput(attrs={
|
||||
# 'class': 'form-control',
|
||||
# 'placeholder': 'Enter department name'
|
||||
# }),
|
||||
# 'code': forms.TextInput(attrs={
|
||||
# 'class': 'form-control',
|
||||
# 'placeholder': 'Enter department code'
|
||||
# }),
|
||||
# 'description': forms.Textarea(attrs={
|
||||
# 'class': 'form-control',
|
||||
# 'rows': 3,
|
||||
# 'placeholder': 'Enter department description'
|
||||
# }),
|
||||
# 'department_type': forms.Select(attrs={
|
||||
# 'class': 'form-select'
|
||||
# }),
|
||||
# 'department_head': forms.Select(attrs={
|
||||
# 'class': 'form-select'
|
||||
# }),
|
||||
# 'parent_department': forms.Select(attrs={
|
||||
# 'class': 'form-select'
|
||||
# }),
|
||||
# 'building': forms.TextInput(attrs={
|
||||
# 'class': 'form-control',
|
||||
# 'placeholder': 'Enter building name/number'
|
||||
# }),
|
||||
# 'floor': forms.TextInput(attrs={
|
||||
# 'class': 'form-control',
|
||||
# 'placeholder': 'Enter floor'
|
||||
# }),
|
||||
# 'wing': forms.TextInput(attrs={
|
||||
# 'class': 'form-control',
|
||||
# 'placeholder': 'Enter wing/section'
|
||||
# }),
|
||||
# 'room_numbers': forms.TextInput(attrs={
|
||||
# 'class': 'form-control',
|
||||
# 'placeholder': 'Enter room numbers'
|
||||
# }),
|
||||
# 'phone': forms.TextInput(attrs={
|
||||
# 'class': 'form-control',
|
||||
# 'placeholder': 'Enter phone number'
|
||||
# }),
|
||||
# 'extension': forms.TextInput(attrs={
|
||||
# 'class': 'form-control',
|
||||
# 'placeholder': 'Enter extension'
|
||||
# }),
|
||||
# 'email': forms.EmailInput(attrs={
|
||||
# 'class': 'form-control',
|
||||
# 'placeholder': 'Enter email address'
|
||||
# }),
|
||||
# 'is_active': forms.CheckboxInput(attrs={
|
||||
# 'class': 'form-check-input'
|
||||
# }),
|
||||
# 'is_24_hour': forms.CheckboxInput(attrs={
|
||||
# 'class': 'form-check-input'
|
||||
# })
|
||||
# }
|
||||
#
|
||||
# def __init__(self, *args, **kwargs):
|
||||
# self.tenant = kwargs.pop('tenant', None)
|
||||
# super().__init__(*args, **kwargs)
|
||||
#
|
||||
# if self.tenant:
|
||||
# # Filter department head by tenant and medical staff
|
||||
# self.fields['department_head'].queryset = User.objects.filter(
|
||||
# tenant=self.tenant,
|
||||
# is_active=True,
|
||||
# role__in=['DOCTOR', 'NURSE_MANAGER', 'ADMINISTRATOR']
|
||||
# )
|
||||
# # Filter parent department by tenant
|
||||
# self.fields['parent_department'].queryset = Department.objects.filter(
|
||||
# tenant=self.tenant,
|
||||
# is_active=True
|
||||
# )
|
||||
#
|
||||
# def clean_code(self):
|
||||
# """Validate department code uniqueness within tenant."""
|
||||
# code = self.cleaned_data.get('code')
|
||||
# if code and self.tenant:
|
||||
# queryset = Department.objects.filter(
|
||||
# tenant=self.tenant,
|
||||
# code=code
|
||||
# )
|
||||
# if self.instance.pk:
|
||||
# queryset = queryset.exclude(pk=self.instance.pk)
|
||||
# if queryset.exists():
|
||||
# raise forms.ValidationError('A department with this code already exists.')
|
||||
# return code
|
||||
|
||||
|
||||
class CoreSearchForm(forms.Form):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
@ -97,7 +97,7 @@ class Migration(migrations.Migration):
|
||||
(
|
||||
"country",
|
||||
models.CharField(
|
||||
default="United States", help_text="Country", max_length=100
|
||||
default="Saudi Arabia", help_text="Country", max_length=100
|
||||
),
|
||||
),
|
||||
(
|
||||
@ -173,7 +173,7 @@ class Migration(migrations.Migration):
|
||||
(
|
||||
"currency",
|
||||
models.CharField(
|
||||
default="USD",
|
||||
default="SAR",
|
||||
help_text="Organization currency code",
|
||||
max_length=3,
|
||||
),
|
||||
@ -676,277 +676,6 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Department",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"department_id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
help_text="Unique department identifier",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"code",
|
||||
models.CharField(
|
||||
help_text="Department code (e.g., CARD, EMER, SURG)",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
("name", models.CharField(help_text="Department name", max_length=100)),
|
||||
(
|
||||
"description",
|
||||
models.TextField(
|
||||
blank=True, help_text="Department description", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"department_type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("CLINICAL", "Clinical Department"),
|
||||
("ANCILLARY", "Ancillary Services"),
|
||||
("SUPPORT", "Support Services"),
|
||||
("ADMINISTRATIVE", "Administrative"),
|
||||
("DIAGNOSTIC", "Diagnostic Services"),
|
||||
("THERAPEUTIC", "Therapeutic Services"),
|
||||
("EMERGENCY", "Emergency Services"),
|
||||
("SURGICAL", "Surgical Services"),
|
||||
("MEDICAL", "Medical Services"),
|
||||
("NURSING", "Nursing Services"),
|
||||
("PHARMACY", "Pharmacy"),
|
||||
("LABORATORY", "Laboratory"),
|
||||
("RADIOLOGY", "Radiology"),
|
||||
("REHABILITATION", "Rehabilitation"),
|
||||
("MENTAL_HEALTH", "Mental Health"),
|
||||
("PEDIATRIC", "Pediatric"),
|
||||
("OBSTETRIC", "Obstetric"),
|
||||
("ONCOLOGY", "Oncology"),
|
||||
("CARDIOLOGY", "Cardiology"),
|
||||
("NEUROLOGY", "Neurology"),
|
||||
("ORTHOPEDIC", "Orthopedic"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
help_text="Type of department",
|
||||
max_length=30,
|
||||
),
|
||||
),
|
||||
(
|
||||
"phone",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Department phone number",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"extension",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Phone extension",
|
||||
max_length=10,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True,
|
||||
help_text="Department email",
|
||||
max_length=254,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"building",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Building name or number",
|
||||
max_length=50,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"floor",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Floor number or name",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"wing",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Wing or section",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"room_numbers",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Room numbers (e.g., 101-110, 201A-205C)",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(default=True, help_text="Department is active"),
|
||||
),
|
||||
(
|
||||
"is_24_hour",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Department operates 24 hours"
|
||||
),
|
||||
),
|
||||
(
|
||||
"operating_hours",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text="Operating hours by day of week",
|
||||
),
|
||||
),
|
||||
(
|
||||
"cost_center_code",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Cost center code for financial tracking",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"budget_code",
|
||||
models.CharField(
|
||||
blank=True, help_text="Budget code", max_length=20, null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"authorized_positions",
|
||||
models.PositiveIntegerField(
|
||||
default=0, help_text="Number of authorized positions"
|
||||
),
|
||||
),
|
||||
(
|
||||
"current_staff_count",
|
||||
models.PositiveIntegerField(
|
||||
default=0, help_text="Current number of staff members"
|
||||
),
|
||||
),
|
||||
(
|
||||
"accreditation_required",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Department requires special accreditation",
|
||||
),
|
||||
),
|
||||
(
|
||||
"accreditation_body",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Accrediting body (e.g., Joint Commission, CAP)",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_inspection_date",
|
||||
models.DateField(
|
||||
blank=True, help_text="Last inspection date", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"next_inspection_date",
|
||||
models.DateField(
|
||||
blank=True,
|
||||
help_text="Next scheduled inspection date",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="User who created the department",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="created_departments",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"department_head",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Department head/manager",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="headed_departments",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"parent_department",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Parent department (for hierarchical structure)",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="sub_departments",
|
||||
to="core.department",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
help_text="Organization tenant",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="departments",
|
||||
to="core.tenant",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Department",
|
||||
"verbose_name_plural": "Departments",
|
||||
"db_table": "core_department",
|
||||
"ordering": ["name"],
|
||||
"indexes": [
|
||||
models.Index(
|
||||
fields=["tenant", "department_type"],
|
||||
name="core_depart_tenant__ef3e04_idx",
|
||||
),
|
||||
models.Index(fields=["code"], name="core_depart_code_5a5745_idx"),
|
||||
models.Index(
|
||||
fields=["is_active"], name="core_depart_is_acti_ae42f9_idx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["parent_department"],
|
||||
name="core_depart_parent__70dda4_idx",
|
||||
),
|
||||
],
|
||||
"unique_together": {("tenant", "code")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="AuditLogEntry",
|
||||
fields=[
|
||||
|
||||
Binary file not shown.
484
core/models.py
484
core/models.py
@ -854,246 +854,246 @@ class IntegrationLog(models.Model):
|
||||
|
||||
|
||||
|
||||
class Department(models.Model):
|
||||
"""
|
||||
Hospital department model for organizational structure.
|
||||
Represents different departments within a healthcare organization.
|
||||
"""
|
||||
|
||||
DEPARTMENT_TYPE_CHOICES = [
|
||||
('CLINICAL', 'Clinical Department'),
|
||||
('ANCILLARY', 'Ancillary Services'),
|
||||
('SUPPORT', 'Support Services'),
|
||||
('ADMINISTRATIVE', 'Administrative'),
|
||||
('DIAGNOSTIC', 'Diagnostic Services'),
|
||||
('THERAPEUTIC', 'Therapeutic Services'),
|
||||
('EMERGENCY', 'Emergency Services'),
|
||||
('SURGICAL', 'Surgical Services'),
|
||||
('MEDICAL', 'Medical Services'),
|
||||
('NURSING', 'Nursing Services'),
|
||||
('PHARMACY', 'Pharmacy'),
|
||||
('LABORATORY', 'Laboratory'),
|
||||
('RADIOLOGY', 'Radiology'),
|
||||
('REHABILITATION', 'Rehabilitation'),
|
||||
('MENTAL_HEALTH', 'Mental Health'),
|
||||
('PEDIATRIC', 'Pediatric'),
|
||||
('OBSTETRIC', 'Obstetric'),
|
||||
('ONCOLOGY', 'Oncology'),
|
||||
('CARDIOLOGY', 'Cardiology'),
|
||||
('NEUROLOGY', 'Neurology'),
|
||||
('ORTHOPEDIC', 'Orthopedic'),
|
||||
('OTHER', 'Other'),
|
||||
]
|
||||
|
||||
# Tenant relationship
|
||||
tenant = models.ForeignKey(
|
||||
Tenant,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='core_departments',
|
||||
help_text='Organization tenant'
|
||||
)
|
||||
|
||||
# Department Information
|
||||
department_id = models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
unique=True,
|
||||
editable=False,
|
||||
help_text='Unique department identifier'
|
||||
)
|
||||
code = models.CharField(
|
||||
max_length=20,
|
||||
help_text='Department code (e.g., CARD, EMER, SURG)'
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
help_text='Department name'
|
||||
)
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Department description'
|
||||
)
|
||||
|
||||
# Department Classification
|
||||
department_type = models.CharField(
|
||||
max_length=30,
|
||||
choices=DEPARTMENT_TYPE_CHOICES,
|
||||
help_text='Type of department'
|
||||
)
|
||||
|
||||
# Organizational Structure
|
||||
parent_department = models.ForeignKey(
|
||||
'self',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='sub_departments',
|
||||
help_text='Parent department (for hierarchical structure)'
|
||||
)
|
||||
|
||||
# Management
|
||||
department_head = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='headed_departments',
|
||||
help_text='Department head/manager'
|
||||
)
|
||||
|
||||
# Contact Information
|
||||
phone = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Department phone number'
|
||||
)
|
||||
extension = models.CharField(
|
||||
max_length=10,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Phone extension'
|
||||
)
|
||||
email = models.EmailField(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Department email'
|
||||
)
|
||||
|
||||
# Location
|
||||
building = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Building name or number'
|
||||
)
|
||||
floor = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Floor number or name'
|
||||
)
|
||||
wing = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Wing or section'
|
||||
)
|
||||
room_numbers = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Room numbers (e.g., 101-110, 201A-205C)'
|
||||
)
|
||||
|
||||
# Operational Information
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Department is active'
|
||||
)
|
||||
is_24_hour = models.BooleanField(
|
||||
default=False,
|
||||
help_text='Department operates 24 hours'
|
||||
)
|
||||
operating_hours = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
help_text='Operating hours by day of week'
|
||||
)
|
||||
|
||||
# Budget and Cost Center
|
||||
cost_center_code = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Cost center code for financial tracking'
|
||||
)
|
||||
budget_code = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Budget code'
|
||||
)
|
||||
|
||||
# Staffing
|
||||
authorized_positions = models.PositiveIntegerField(
|
||||
default=0,
|
||||
help_text='Number of authorized positions'
|
||||
)
|
||||
current_staff_count = models.PositiveIntegerField(
|
||||
default=0,
|
||||
help_text='Current number of staff members'
|
||||
)
|
||||
|
||||
# Quality and Compliance
|
||||
accreditation_required = models.BooleanField(
|
||||
default=False,
|
||||
help_text='Department requires special accreditation'
|
||||
)
|
||||
accreditation_body = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Accrediting body (e.g., Joint Commission, CAP)'
|
||||
)
|
||||
last_inspection_date = models.DateField(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Last inspection date'
|
||||
)
|
||||
next_inspection_date = models.DateField(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Next scheduled inspection date'
|
||||
)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='created_departments',
|
||||
help_text='User who created the department'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'core_department'
|
||||
verbose_name = 'Department'
|
||||
verbose_name_plural = 'Departments'
|
||||
ordering = ['name']
|
||||
indexes = [
|
||||
models.Index(fields=['tenant', 'department_type']),
|
||||
models.Index(fields=['code']),
|
||||
models.Index(fields=['is_active']),
|
||||
models.Index(fields=['parent_department']),
|
||||
]
|
||||
unique_together = ['tenant', 'code']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.code})"
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
"""Return full department name with parent if applicable"""
|
||||
if self.parent_department:
|
||||
return f"{self.parent_department.name} - {self.name}"
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def staffing_percentage(self):
|
||||
"""Calculate current staffing percentage"""
|
||||
if self.authorized_positions > 0:
|
||||
return (self.current_staff_count / self.authorized_positions) * 100
|
||||
return 0
|
||||
|
||||
def get_all_sub_departments(self):
|
||||
"""Get all sub-departments recursively"""
|
||||
sub_departments = []
|
||||
for sub_dept in self.sub_departments.all():
|
||||
sub_departments.append(sub_dept)
|
||||
sub_departments.extend(sub_dept.get_all_sub_departments())
|
||||
return sub_departments
|
||||
# class Department(models.Model):
|
||||
# """
|
||||
# Hospital department model for organizational structure.
|
||||
# Represents different departments within a healthcare organization.
|
||||
# """
|
||||
#
|
||||
# DEPARTMENT_TYPE_CHOICES = [
|
||||
# ('CLINICAL', 'Clinical Department'),
|
||||
# ('ANCILLARY', 'Ancillary Services'),
|
||||
# ('SUPPORT', 'Support Services'),
|
||||
# ('ADMINISTRATIVE', 'Administrative'),
|
||||
# ('DIAGNOSTIC', 'Diagnostic Services'),
|
||||
# ('THERAPEUTIC', 'Therapeutic Services'),
|
||||
# ('EMERGENCY', 'Emergency Services'),
|
||||
# ('SURGICAL', 'Surgical Services'),
|
||||
# ('MEDICAL', 'Medical Services'),
|
||||
# ('NURSING', 'Nursing Services'),
|
||||
# ('PHARMACY', 'Pharmacy'),
|
||||
# ('LABORATORY', 'Laboratory'),
|
||||
# ('RADIOLOGY', 'Radiology'),
|
||||
# ('REHABILITATION', 'Rehabilitation'),
|
||||
# ('MENTAL_HEALTH', 'Mental Health'),
|
||||
# ('PEDIATRIC', 'Pediatric'),
|
||||
# ('OBSTETRIC', 'Obstetric'),
|
||||
# ('ONCOLOGY', 'Oncology'),
|
||||
# ('CARDIOLOGY', 'Cardiology'),
|
||||
# ('NEUROLOGY', 'Neurology'),
|
||||
# ('ORTHOPEDIC', 'Orthopedic'),
|
||||
# ('OTHER', 'Other'),
|
||||
# ]
|
||||
#
|
||||
# # Tenant relationship
|
||||
# tenant = models.ForeignKey(
|
||||
# Tenant,
|
||||
# on_delete=models.CASCADE,
|
||||
# related_name='core_departments',
|
||||
# help_text='Organization tenant'
|
||||
# )
|
||||
#
|
||||
# # Department Information
|
||||
# department_id = models.UUIDField(
|
||||
# default=uuid.uuid4,
|
||||
# unique=True,
|
||||
# editable=False,
|
||||
# help_text='Unique department identifier'
|
||||
# )
|
||||
# code = models.CharField(
|
||||
# max_length=20,
|
||||
# help_text='Department code (e.g., CARD, EMER, SURG)'
|
||||
# )
|
||||
# name = models.CharField(
|
||||
# max_length=100,
|
||||
# help_text='Department name'
|
||||
# )
|
||||
# description = models.TextField(
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Department description'
|
||||
# )
|
||||
#
|
||||
# # Department Classification
|
||||
# department_type = models.CharField(
|
||||
# max_length=30,
|
||||
# choices=DEPARTMENT_TYPE_CHOICES,
|
||||
# help_text='Type of department'
|
||||
# )
|
||||
#
|
||||
# # Organizational Structure
|
||||
# parent_department = models.ForeignKey(
|
||||
# 'self',
|
||||
# on_delete=models.SET_NULL,
|
||||
# null=True,
|
||||
# blank=True,
|
||||
# related_name='sub_departments',
|
||||
# help_text='Parent department (for hierarchical structure)'
|
||||
# )
|
||||
#
|
||||
# # Management
|
||||
# department_head = models.ForeignKey(
|
||||
# settings.AUTH_USER_MODEL,
|
||||
# on_delete=models.SET_NULL,
|
||||
# null=True,
|
||||
# blank=True,
|
||||
# related_name='headed_departments',
|
||||
# help_text='Department head/manager'
|
||||
# )
|
||||
#
|
||||
# # Contact Information
|
||||
# phone = models.CharField(
|
||||
# max_length=20,
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Department phone number'
|
||||
# )
|
||||
# extension = models.CharField(
|
||||
# max_length=10,
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Phone extension'
|
||||
# )
|
||||
# email = models.EmailField(
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Department email'
|
||||
# )
|
||||
#
|
||||
# # Location
|
||||
# building = models.CharField(
|
||||
# max_length=50,
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Building name or number'
|
||||
# )
|
||||
# floor = models.CharField(
|
||||
# max_length=20,
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Floor number or name'
|
||||
# )
|
||||
# wing = models.CharField(
|
||||
# max_length=20,
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Wing or section'
|
||||
# )
|
||||
# room_numbers = models.CharField(
|
||||
# max_length=100,
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Room numbers (e.g., 101-110, 201A-205C)'
|
||||
# )
|
||||
#
|
||||
# # Operational Information
|
||||
# is_active = models.BooleanField(
|
||||
# default=True,
|
||||
# help_text='Department is active'
|
||||
# )
|
||||
# is_24_hour = models.BooleanField(
|
||||
# default=False,
|
||||
# help_text='Department operates 24 hours'
|
||||
# )
|
||||
# operating_hours = models.JSONField(
|
||||
# default=dict,
|
||||
# blank=True,
|
||||
# help_text='Operating hours by day of week'
|
||||
# )
|
||||
#
|
||||
# # Budget and Cost Center
|
||||
# cost_center_code = models.CharField(
|
||||
# max_length=20,
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Cost center code for financial tracking'
|
||||
# )
|
||||
# budget_code = models.CharField(
|
||||
# max_length=20,
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Budget code'
|
||||
# )
|
||||
#
|
||||
# # Staffing
|
||||
# authorized_positions = models.PositiveIntegerField(
|
||||
# default=0,
|
||||
# help_text='Number of authorized positions'
|
||||
# )
|
||||
# current_staff_count = models.PositiveIntegerField(
|
||||
# default=0,
|
||||
# help_text='Current number of staff members'
|
||||
# )
|
||||
#
|
||||
# # Quality and Compliance
|
||||
# accreditation_required = models.BooleanField(
|
||||
# default=False,
|
||||
# help_text='Department requires special accreditation'
|
||||
# )
|
||||
# accreditation_body = models.CharField(
|
||||
# max_length=100,
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Accrediting body (e.g., Joint Commission, CAP)'
|
||||
# )
|
||||
# last_inspection_date = models.DateField(
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Last inspection date'
|
||||
# )
|
||||
# next_inspection_date = models.DateField(
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text='Next scheduled inspection date'
|
||||
# )
|
||||
#
|
||||
# # Metadata
|
||||
# created_at = models.DateTimeField(auto_now_add=True)
|
||||
# updated_at = models.DateTimeField(auto_now=True)
|
||||
# created_by = models.ForeignKey(
|
||||
# settings.AUTH_USER_MODEL,
|
||||
# on_delete=models.SET_NULL,
|
||||
# null=True,
|
||||
# blank=True,
|
||||
# related_name='created_departments',
|
||||
# help_text='User who created the department'
|
||||
# )
|
||||
#
|
||||
# class Meta:
|
||||
# db_table = 'core_department'
|
||||
# verbose_name = 'Department'
|
||||
# verbose_name_plural = 'Departments'
|
||||
# ordering = ['name']
|
||||
# indexes = [
|
||||
# models.Index(fields=['tenant', 'department_type']),
|
||||
# models.Index(fields=['code']),
|
||||
# models.Index(fields=['is_active']),
|
||||
# models.Index(fields=['parent_department']),
|
||||
# ]
|
||||
# unique_together = ['tenant', 'code']
|
||||
#
|
||||
# def __str__(self):
|
||||
# return f"{self.name} ({self.code})"
|
||||
#
|
||||
# @property
|
||||
# def full_name(self):
|
||||
# """Return full department name with parent if applicable"""
|
||||
# if self.parent_department:
|
||||
# return f"{self.parent_department.name} - {self.name}"
|
||||
# return self.name
|
||||
#
|
||||
# @property
|
||||
# def staffing_percentage(self):
|
||||
# """Calculate current staffing percentage"""
|
||||
# if self.authorized_positions > 0:
|
||||
# return (self.current_staff_count / self.authorized_positions) * 100
|
||||
# return 0
|
||||
#
|
||||
# def get_all_sub_departments(self):
|
||||
# """Get all sub-departments recursively"""
|
||||
# sub_departments = []
|
||||
# for sub_dept in self.sub_departments.all():
|
||||
# sub_departments.append(sub_dept)
|
||||
# sub_departments.extend(sub_dept.get_all_sub_departments())
|
||||
# return sub_departments
|
||||
|
||||
|
||||
23
core/urls.py
23
core/urls.py
@ -23,24 +23,19 @@ urlpatterns = [
|
||||
path('tenants/<int:pk>/deactivate/', views.deactivate_tenant, name='deactivate_tenant'),
|
||||
|
||||
# Department CRUD URLs
|
||||
path('departments/', views.DepartmentListView.as_view(), name='department_list'),
|
||||
path('departments/create/', views.DepartmentCreateView.as_view(), name='department_create'),
|
||||
path('departments/<int:pk>/', views.DepartmentDetailView.as_view(), name='department_detail'),
|
||||
path('departments/<int:pk>/edit/', views.DepartmentUpdateView.as_view(), name='department_update'),
|
||||
path('departments/<int:pk>/delete/', views.DepartmentDeleteView.as_view(), name='department_delete'),
|
||||
path('departments/<int:pk>/activate/', views.activate_department, name='activate_department'),
|
||||
path('departments/<int:pk>/deactivate/', views.deactivate_department, name='deactivate_department'),
|
||||
path('departments/bulk-activate/', views.bulk_activate_departments, name='bulk_activate_departments'),
|
||||
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('departments/', views.DepartmentListView.as_view(), name='department_list'),
|
||||
# path('departments/create/', views.DepartmentCreateView.as_view(), name='department_create'),
|
||||
# path('departments/<int:pk>/', views.DepartmentDetailView.as_view(), name='department_detail'),
|
||||
# path('departments/<int:pk>/edit/', views.DepartmentUpdateView.as_view(), name='department_update'),
|
||||
# path('departments/<int:pk>/delete/', views.DepartmentDeleteView.as_view(), name='department_delete'),
|
||||
|
||||
|
||||
# System Configuration CRUD URLs
|
||||
path('system-configuration/create/', views.SystemConfigurationCreateView.as_view(), name='system_configuration_create'),
|
||||
path('system-configuration/<int:pk>/', views.SystemConfigurationDetailView.as_view(), name='system_configuration_detail'),
|
||||
path('system-configuration/<int:pk>/edit/', views.SystemConfigurationUpdateView.as_view(), name='system_configuration_update'),
|
||||
path('system-configuration/<int:pk>/delete/', views.SystemConfigurationDeleteView.as_view(), name='system_configuration_delete'),
|
||||
path('api/department-hierarchy/', views.get_department_hierarchy, name='get_department_hierarchy'),
|
||||
path('htmx/department-tree/', views.department_tree, name='department_tree'),
|
||||
|
||||
|
||||
# System Notification CRUD URLs
|
||||
path('notifications/', views.SystemNotificationListView.as_view(), name='system_notification_list'),
|
||||
@ -79,14 +74,12 @@ urlpatterns = [
|
||||
# Search and Filter URLs
|
||||
path('search/', views.CoreSearchView.as_view(), name='search'),
|
||||
path('search/tenants/', views.tenant_search, name='tenant_search'),
|
||||
path('search/departments/', views.department_search, name='department_search'),
|
||||
path('search/audit-logs/', views.audit_log_search, name='audit_log_search'),
|
||||
|
||||
# Bulk Operations
|
||||
path('tenants/bulk-activate/', views.bulk_activate_tenants, name='bulk_activate_tenants'),
|
||||
path('tenants/bulk-deactivate/', views.bulk_deactivate_tenants, name='bulk_deactivate_tenants'),
|
||||
path('departments/bulk-activate/', views.bulk_activate_departments, name='bulk_activate_departments'),
|
||||
path('departments/bulk-deactivate/', views.bulk_deactivate_departments, name='bulk_deactivate_departments'),
|
||||
|
||||
path('audit-log/bulk-export/', views.bulk_export_audit_logs, name='bulk_export_audit_logs'),
|
||||
|
||||
# API-like endpoints for AJAX
|
||||
|
||||
617
core/views.py
617
core/views.py
@ -13,19 +13,20 @@ from django.db.models import Count, Q
|
||||
from django.utils import timezone
|
||||
from django.contrib import messages
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
from accounts.models import User
|
||||
from datetime import timedelta
|
||||
from .models import (
|
||||
Tenant, AuditLogEntry, SystemConfiguration, SystemNotification,
|
||||
IntegrationLog, Department
|
||||
IntegrationLog
|
||||
)
|
||||
from hr.models import Department
|
||||
from hr.forms import DepartmentForm
|
||||
|
||||
# Create aliases for models to match the views
|
||||
AuditLog = AuditLogEntry
|
||||
User = get_user_model()
|
||||
|
||||
from .forms import (
|
||||
TenantForm, SystemConfigurationForm, SystemNotificationForm,
|
||||
DepartmentForm, CoreSearchForm
|
||||
TenantForm, SystemConfigurationForm, SystemNotificationForm,CoreSearchForm
|
||||
)
|
||||
from .utils import AuditLogger
|
||||
|
||||
@ -66,10 +67,6 @@ class DashboardView(LoginRequiredMixin, TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TENANT VIEWS (FULL CRUD - Master Data)
|
||||
# ============================================================================
|
||||
|
||||
class TenantListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
"""
|
||||
List all tenants (Super admin only).
|
||||
@ -226,10 +223,6 @@ class TenantDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# AUDIT LOG VIEWS (READ-ONLY - System Generated)
|
||||
# ============================================================================
|
||||
|
||||
class AuditLogListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
Audit log listing view.
|
||||
@ -302,10 +295,6 @@ class AuditLogDetailView(LoginRequiredMixin, DetailView):
|
||||
return AuditLogEntry.objects.filter(tenant=tenant)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SYSTEM CONFIGURATION VIEWS (FULL CRUD - Master Data)
|
||||
# ============================================================================
|
||||
|
||||
class SystemConfigurationListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
System configuration view.
|
||||
@ -476,10 +465,6 @@ class SystemConfigurationDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SYSTEM NOTIFICATION VIEWS (FULL CRUD - Operational Data)
|
||||
# ============================================================================
|
||||
|
||||
class SystemNotificationListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List system notifications.
|
||||
@ -637,10 +622,6 @@ class SystemNotificationDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# INTEGRATION LOG VIEWS (READ-ONLY - System Generated)
|
||||
# ============================================================================
|
||||
|
||||
class IntegrationLogListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List integration logs.
|
||||
@ -708,189 +689,6 @@ class IntegrationLogDetailView(LoginRequiredMixin, DetailView):
|
||||
return IntegrationLog.objects.filter(tenant=tenant)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DEPARTMENT VIEWS (FULL CRUD - Master Data)
|
||||
# ============================================================================
|
||||
|
||||
class DepartmentListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List departments.
|
||||
"""
|
||||
model = Department
|
||||
template_name = 'core/department_list.html'
|
||||
context_object_name = 'departments'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
tenant = getattr(self.request, 'tenant', None)
|
||||
if not tenant:
|
||||
return Department.objects.none()
|
||||
|
||||
queryset = Department.objects.filter(tenant=tenant).order_by('name')
|
||||
|
||||
# Apply search filter
|
||||
search = self.request.GET.get('search')
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=search) |
|
||||
Q(description__icontains=search) |
|
||||
Q(location__icontains=search)
|
||||
)
|
||||
|
||||
# Apply status filter
|
||||
status = self.request.GET.get('status')
|
||||
if status:
|
||||
queryset = queryset.filter(is_active=(status == 'active'))
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class DepartmentDetailView(LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
Display department details.
|
||||
"""
|
||||
model = Department
|
||||
template_name = 'core/department_detail.html'
|
||||
context_object_name = 'department'
|
||||
|
||||
def get_queryset(self):
|
||||
tenant = getattr(self.request, 'tenant', None)
|
||||
if not tenant:
|
||||
return Department.objects.none()
|
||||
return Department.objects.filter(tenant=tenant)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
department = self.object
|
||||
|
||||
# Get department statistics
|
||||
context.update({
|
||||
'employee_count': department.current_staff_count,
|
||||
'recent_activity': AuditLogEntry.objects.filter(
|
||||
tenant=department.tenant,
|
||||
object_id=str(department.pk),
|
||||
content_type__model='department'
|
||||
).order_by('-timestamp')[:10],
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class DepartmentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
"""
|
||||
Create new department.
|
||||
"""
|
||||
model = Department
|
||||
form_class = DepartmentForm
|
||||
template_name = 'core/department_form.html'
|
||||
permission_required = 'core.add_department'
|
||||
success_url = reverse_lazy('core:department_list')
|
||||
|
||||
def form_valid(self, form):
|
||||
# Set tenant
|
||||
form.instance.tenant = getattr(self.request, 'tenant', None)
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log department creation
|
||||
AuditLogger.log_event(
|
||||
tenant=form.instance.tenant,
|
||||
event_type='CREATE',
|
||||
event_category='SYSTEM_ADMINISTRATION',
|
||||
action='Create Department',
|
||||
description=f'Created department: {self.object.name}',
|
||||
user=self.request.user,
|
||||
content_object=self.object,
|
||||
request=self.request
|
||||
)
|
||||
|
||||
messages.success(self.request, f'Department "{self.object.name}" created successfully.')
|
||||
return response
|
||||
|
||||
|
||||
class DepartmentUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||||
"""
|
||||
Update department.
|
||||
"""
|
||||
model = Department
|
||||
form_class = DepartmentForm
|
||||
template_name = 'core/department_form.html'
|
||||
permission_required = 'core.change_department'
|
||||
|
||||
def get_queryset(self):
|
||||
tenant = getattr(self.request, 'tenant', None)
|
||||
if not tenant:
|
||||
return Department.objects.none()
|
||||
return Department.objects.filter(tenant=tenant)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('core:department_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log department update
|
||||
AuditLogger.log_event(
|
||||
tenant=self.object.tenant,
|
||||
event_type='UPDATE',
|
||||
event_category='SYSTEM_ADMINISTRATION',
|
||||
action='Update Department',
|
||||
description=f'Updated department: {self.object.name}',
|
||||
user=self.request.user,
|
||||
content_object=self.object,
|
||||
request=self.request
|
||||
)
|
||||
|
||||
messages.success(self.request, f'Department "{self.object.name}" updated successfully.')
|
||||
return response
|
||||
|
||||
|
||||
class DepartmentDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||||
"""
|
||||
Delete department (soft delete to inactive).
|
||||
"""
|
||||
model = Department
|
||||
template_name = 'core/department_confirm_delete.html'
|
||||
permission_required = 'core.delete_department'
|
||||
success_url = reverse_lazy('core:department_list')
|
||||
|
||||
def get_queryset(self):
|
||||
tenant = getattr(self.request, 'tenant', None)
|
||||
if not tenant:
|
||||
return Department.objects.none()
|
||||
return Department.objects.filter(tenant=tenant)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
# Check if department has employees
|
||||
if self.object.get_employee_count() > 0:
|
||||
messages.error(request, 'Cannot delete department with active employees.')
|
||||
return redirect('core:department_detail', pk=self.object.pk)
|
||||
|
||||
# Soft delete - set to inactive
|
||||
self.object.is_active = False
|
||||
self.object.save()
|
||||
|
||||
# Log department deletion
|
||||
AuditLogger.log_event(
|
||||
tenant=self.object.tenant,
|
||||
event_type='DELETE',
|
||||
event_category='SYSTEM_ADMINISTRATION',
|
||||
action='Deactivate Department',
|
||||
description=f'Deactivated department: {self.object.name}',
|
||||
user=request.user,
|
||||
content_object=self.object,
|
||||
request=request
|
||||
)
|
||||
|
||||
messages.success(request, f'Department "{self.object.name}" deactivated successfully.')
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HTMX VIEWS FOR REAL-TIME UPDATES
|
||||
# ============================================================================
|
||||
|
||||
@login_required
|
||||
def dashboard_stats(request):
|
||||
"""
|
||||
@ -1064,10 +862,6 @@ def system_health(request):
|
||||
})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ACTION VIEWS FOR WORKFLOW OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
@login_required
|
||||
def activate_notification(request, pk):
|
||||
"""
|
||||
@ -1175,9 +969,6 @@ def reset_configuration(request, pk):
|
||||
return redirect('core:system_configuration_detail', pk=pk)
|
||||
|
||||
|
||||
|
||||
|
||||
# Missing HTMX Views
|
||||
def tenant_stats(request):
|
||||
"""
|
||||
HTMX view for tenant statistics.
|
||||
@ -1194,20 +985,6 @@ def tenant_stats(request):
|
||||
return render(request, 'core/partials/tenant_stats.html', {'stats': stats})
|
||||
|
||||
|
||||
def department_tree(request):
|
||||
"""
|
||||
HTMX view for department tree structure.
|
||||
"""
|
||||
departments = Department.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
parent=None
|
||||
).prefetch_related('children')
|
||||
|
||||
return render(request, 'core/partials/department_tree.html', {
|
||||
'departments': departments
|
||||
})
|
||||
|
||||
|
||||
def configuration_search(request):
|
||||
"""
|
||||
HTMX view for configuration search.
|
||||
@ -1240,7 +1017,6 @@ def audit_log_list_htmx(request):
|
||||
})
|
||||
|
||||
|
||||
# Missing Action Views
|
||||
def activate_tenant(request, pk):
|
||||
"""
|
||||
Activate a tenant.
|
||||
@ -1265,30 +1041,6 @@ def deactivate_tenant(request, pk):
|
||||
return redirect('core:tenant_detail', pk=pk)
|
||||
|
||||
|
||||
def activate_department(request, pk):
|
||||
"""
|
||||
Activate a department.
|
||||
"""
|
||||
department = get_object_or_404(Department, department_id=pk)
|
||||
department.is_active = True
|
||||
department.save()
|
||||
|
||||
messages.success(request, f'Department "{department.name}" has been activated.')
|
||||
return redirect('core:department_detail', pk=pk)
|
||||
|
||||
|
||||
def deactivate_department(request, pk):
|
||||
"""
|
||||
Deactivate a department.
|
||||
"""
|
||||
department = get_object_or_404(Department, department_id=pk)
|
||||
department.is_active = False
|
||||
department.save()
|
||||
|
||||
messages.success(request, f'Department "{department.name}" has been deactivated.')
|
||||
return redirect('core:department_detail', pk=pk)
|
||||
|
||||
|
||||
def reset_system_configuration(request):
|
||||
"""
|
||||
Reset system configuration to defaults.
|
||||
@ -1302,7 +1054,7 @@ def reset_system_configuration(request):
|
||||
messages.success(request, 'System configuration has been reset to defaults.')
|
||||
return redirect('core:system_configuration_list')
|
||||
|
||||
return render(request, 'core/reset_configuration_confirm.html')
|
||||
return render(request, 'core/configurations/reset_configuration_confirm.html')
|
||||
|
||||
|
||||
def export_audit_log(request):
|
||||
@ -1335,7 +1087,6 @@ def export_audit_log(request):
|
||||
return response
|
||||
|
||||
|
||||
# Missing Search Views
|
||||
class CoreSearchView(ListView):
|
||||
"""
|
||||
Generic search view for core models.
|
||||
@ -1396,23 +1147,6 @@ def tenant_search(request):
|
||||
return JsonResponse({'tenants': list(tenants)})
|
||||
|
||||
|
||||
def department_search(request):
|
||||
"""
|
||||
AJAX search for departments.
|
||||
"""
|
||||
query = request.GET.get('q', '')
|
||||
departments = []
|
||||
|
||||
if query:
|
||||
departments = Department.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
name__icontains=query
|
||||
).values('department_id', 'name', 'department_type')[:10]
|
||||
|
||||
return JsonResponse({'departments': list(departments)})
|
||||
|
||||
|
||||
# Missing Bulk Operation Views
|
||||
def bulk_activate_tenants(request):
|
||||
"""
|
||||
Bulk activate tenants.
|
||||
@ -1443,38 +1177,6 @@ def bulk_deactivate_tenants(request):
|
||||
return redirect('core:tenant_list')
|
||||
|
||||
|
||||
def bulk_activate_departments(request):
|
||||
"""
|
||||
Bulk activate departments.
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
department_ids = request.POST.getlist('department_ids')
|
||||
count = Department.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
department_id__in=department_ids
|
||||
).update(is_active=True)
|
||||
|
||||
messages.success(request, f'{count} departments have been activated.')
|
||||
|
||||
return redirect('core:department_list')
|
||||
|
||||
|
||||
def bulk_deactivate_departments(request):
|
||||
"""
|
||||
Bulk deactivate departments.
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
department_ids = request.POST.getlist('department_ids')
|
||||
count = Department.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
department_id__in=department_ids
|
||||
).update(is_active=False)
|
||||
|
||||
messages.success(request, f'{count} departments have been deactivated.')
|
||||
|
||||
return redirect('core:department_list')
|
||||
|
||||
|
||||
def bulk_export_audit_logs(request):
|
||||
"""
|
||||
Bulk export audit logs.
|
||||
@ -1511,7 +1213,6 @@ def bulk_export_audit_logs(request):
|
||||
return redirect('core:audit_log_list')
|
||||
|
||||
|
||||
# Missing API Views
|
||||
def validate_tenant_data(request):
|
||||
"""
|
||||
AJAX validation for tenant data.
|
||||
@ -1528,31 +1229,6 @@ def validate_tenant_data(request):
|
||||
return JsonResponse({'valid': len(errors) == 0, 'errors': errors})
|
||||
|
||||
|
||||
def get_department_hierarchy(request):
|
||||
"""
|
||||
Get department hierarchy as JSON.
|
||||
"""
|
||||
departments = Department.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
is_active=True
|
||||
).select_related('parent')
|
||||
|
||||
def build_tree(parent=None):
|
||||
children = []
|
||||
for dept in departments:
|
||||
if dept.parent == parent:
|
||||
children.append({
|
||||
'id': str(dept.department_id),
|
||||
'name': dept.name,
|
||||
'type': dept.department_type,
|
||||
'children': build_tree(dept)
|
||||
})
|
||||
return children
|
||||
|
||||
hierarchy = build_tree()
|
||||
return JsonResponse({'hierarchy': hierarchy})
|
||||
|
||||
|
||||
def get_system_status(request):
|
||||
"""
|
||||
Get system status information.
|
||||
@ -1604,8 +1280,6 @@ def backup_configuration(request):
|
||||
return response
|
||||
|
||||
|
||||
|
||||
|
||||
def restore_configuration(request):
|
||||
"""
|
||||
Restore system configuration from backup.
|
||||
@ -1644,111 +1318,184 @@ def restore_configuration(request):
|
||||
return render(request, 'core/restore_configuration.html')
|
||||
|
||||
|
||||
@login_required
|
||||
def assign_department_head(request, pk):
|
||||
"""
|
||||
Assign a department head to a department.
|
||||
"""
|
||||
department = get_object_or_404(Department, pk=pk, tenant=request.user.tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
user_id = request.POST.get('user_id')
|
||||
|
||||
if user_id:
|
||||
try:
|
||||
user = User.objects.get(id=user_id, tenant=request.user.tenant)
|
||||
|
||||
# Remove current department head if exists
|
||||
if department.department_head:
|
||||
old_head = department.department_head
|
||||
AuditLogger.log_event(
|
||||
request=request,
|
||||
event_type='UPDATE',
|
||||
event_category='SYSTEM_ADMINISTRATION',
|
||||
action=f'Removed department head from {department.name}',
|
||||
description=f'Removed {old_head.get_full_name()} as head of {department.name}',
|
||||
content_object=department,
|
||||
additional_data={
|
||||
'old_department_head_id': old_head.id,
|
||||
'old_department_head_name': old_head.get_full_name()
|
||||
}
|
||||
)
|
||||
|
||||
# Assign new department head
|
||||
department.department_head = user
|
||||
department.save()
|
||||
|
||||
# Log the assignment
|
||||
AuditLogger.log_event(
|
||||
request=request,
|
||||
event_type='UPDATE',
|
||||
event_category='SYSTEM_ADMINISTRATION',
|
||||
action=f'Assigned department head to {department.name}',
|
||||
description=f'Assigned {user.get_full_name()} as head of {department.name}',
|
||||
content_object=department,
|
||||
additional_data={
|
||||
'new_department_head_id': user.id,
|
||||
'new_department_head_name': user.get_full_name()
|
||||
}
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f'{user.get_full_name()} has been assigned as head of {department.name}.'
|
||||
)
|
||||
return redirect('core:department_detail', pk=department.pk)
|
||||
|
||||
except User.DoesNotExist:
|
||||
messages.error(request, 'Selected user not found.')
|
||||
else:
|
||||
# Remove department head
|
||||
if department.department_head:
|
||||
old_head = department.department_head
|
||||
department.department_head = None
|
||||
department.save()
|
||||
|
||||
# Log the removal
|
||||
AuditLogger.log_event(
|
||||
request=request,
|
||||
event_type='UPDATE',
|
||||
event_category='SYSTEM_ADMINISTRATION',
|
||||
action=f'Removed department head from {department.name}',
|
||||
description=f'Removed {old_head.get_full_name()} as head of {department.name}',
|
||||
content_object=department,
|
||||
additional_data={
|
||||
'removed_department_head_id': old_head.id,
|
||||
'removed_department_head_name': old_head.get_full_name()
|
||||
}
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f'Department head has been removed from {department.name}.'
|
||||
)
|
||||
else:
|
||||
messages.info(request, 'No department head was assigned.')
|
||||
|
||||
return redirect('core:department_detail', pk=department.pk)
|
||||
|
||||
# Get eligible users (staff members who can be department heads)
|
||||
eligible_users = User.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
is_active=True,
|
||||
is_staff=True
|
||||
).exclude(
|
||||
id=department.department_head.id if department.department_head else None
|
||||
).order_by('first_name', 'last_name')
|
||||
|
||||
context = {
|
||||
'department': department,
|
||||
'eligible_users': eligible_users,
|
||||
'current_head': department.department_head,
|
||||
}
|
||||
|
||||
return render(request, 'core/assign_department_head.html', context)
|
||||
|
||||
|
||||
|
||||
# Department Views
|
||||
|
||||
# class DepartmentListView(LoginRequiredMixin, ListView):
|
||||
# """
|
||||
# List departments.
|
||||
# """
|
||||
# model = Department
|
||||
# template_name = 'core/department_list.html'
|
||||
# context_object_name = 'departments'
|
||||
# paginate_by = 20
|
||||
#
|
||||
# def get_queryset(self):
|
||||
# tenant = getattr(self.request, 'tenant', None)
|
||||
# if not tenant:
|
||||
# return Department.objects.none()
|
||||
#
|
||||
# queryset = Department.objects.filter(tenant=tenant).order_by('name')
|
||||
#
|
||||
# # Apply search filter
|
||||
# search = self.request.GET.get('search')
|
||||
# if search:
|
||||
# queryset = queryset.filter(
|
||||
# Q(name__icontains=search) |
|
||||
# Q(description__icontains=search) |
|
||||
# Q(location__icontains=search)
|
||||
# )
|
||||
#
|
||||
# # Apply status filter
|
||||
# status = self.request.GET.get('status')
|
||||
# if status:
|
||||
# queryset = queryset.filter(is_active=(status == 'active'))
|
||||
#
|
||||
# return queryset
|
||||
#
|
||||
#
|
||||
# class DepartmentDetailView(LoginRequiredMixin, DetailView):
|
||||
# """
|
||||
# Display department details.
|
||||
# """
|
||||
# model = Department
|
||||
# template_name = 'core/department_detail.html'
|
||||
# context_object_name = 'department'
|
||||
#
|
||||
# def get_queryset(self):
|
||||
# tenant = getattr(self.request, 'tenant', None)
|
||||
# if not tenant:
|
||||
# return Department.objects.none()
|
||||
# return Department.objects.filter(tenant=tenant)
|
||||
#
|
||||
# def get_context_data(self, **kwargs):
|
||||
# context = super().get_context_data(**kwargs)
|
||||
# department = self.object
|
||||
#
|
||||
# # Get department statistics
|
||||
# context.update({
|
||||
# 'employee_count': department.current_staff_count,
|
||||
# 'recent_activity': AuditLogEntry.objects.filter(
|
||||
# tenant=department.tenant,
|
||||
# object_id=str(department.pk),
|
||||
# content_type__model='department'
|
||||
# ).order_by('-timestamp')[:10],
|
||||
# })
|
||||
#
|
||||
# return context
|
||||
#
|
||||
#
|
||||
# class DepartmentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
# """
|
||||
# Create new department.
|
||||
# """
|
||||
# model = Department
|
||||
# form_class = DepartmentForm
|
||||
# template_name = 'core/department_form.html'
|
||||
# permission_required = 'core.add_department'
|
||||
# success_url = reverse_lazy('core:department_list')
|
||||
#
|
||||
# def form_valid(self, form):
|
||||
# # Set tenant
|
||||
# form.instance.tenant = getattr(self.request, 'tenant', None)
|
||||
# response = super().form_valid(form)
|
||||
#
|
||||
# # Log department creation
|
||||
# AuditLogger.log_event(
|
||||
# tenant=form.instance.tenant,
|
||||
# event_type='CREATE',
|
||||
# event_category='SYSTEM_ADMINISTRATION',
|
||||
# action='Create Department',
|
||||
# description=f'Created department: {self.object.name}',
|
||||
# user=self.request.user,
|
||||
# content_object=self.object,
|
||||
# request=self.request
|
||||
# )
|
||||
#
|
||||
# messages.success(self.request, f'Department "{self.object.name}" created successfully.')
|
||||
# return response
|
||||
#
|
||||
#
|
||||
# class DepartmentUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||||
# """
|
||||
# Update department.
|
||||
# """
|
||||
# model = Department
|
||||
# form_class = DepartmentForm
|
||||
# template_name = 'core/department_form.html'
|
||||
# permission_required = 'core.change_department'
|
||||
#
|
||||
# def get_queryset(self):
|
||||
# tenant = getattr(self.request, 'tenant', None)
|
||||
# if not tenant:
|
||||
# return Department.objects.none()
|
||||
# return Department.objects.filter(tenant=tenant)
|
||||
#
|
||||
# def get_success_url(self):
|
||||
# return reverse('core:department_detail', kwargs={'pk': self.object.pk})
|
||||
#
|
||||
# def form_valid(self, form):
|
||||
# response = super().form_valid(form)
|
||||
#
|
||||
# # Log department update
|
||||
# AuditLogger.log_event(
|
||||
# tenant=self.object.tenant,
|
||||
# event_type='UPDATE',
|
||||
# event_category='SYSTEM_ADMINISTRATION',
|
||||
# action='Update Department',
|
||||
# description=f'Updated department: {self.object.name}',
|
||||
# user=self.request.user,
|
||||
# content_object=self.object,
|
||||
# request=self.request
|
||||
# )
|
||||
#
|
||||
# messages.success(self.request, f'Department "{self.object.name}" updated successfully.')
|
||||
# return response
|
||||
#
|
||||
#
|
||||
# class DepartmentDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||||
# """
|
||||
# Delete department (soft delete to inactive).
|
||||
# """
|
||||
# model = Department
|
||||
# template_name = 'core/department_confirm_delete.html'
|
||||
# permission_required = 'core.delete_department'
|
||||
# success_url = reverse_lazy('core:department_list')
|
||||
#
|
||||
# def get_queryset(self):
|
||||
# tenant = getattr(self.request, 'tenant', None)
|
||||
# if not tenant:
|
||||
# return Department.objects.none()
|
||||
# return Department.objects.filter(tenant=tenant)
|
||||
#
|
||||
# def delete(self, request, *args, **kwargs):
|
||||
# self.object = self.get_object()
|
||||
#
|
||||
# # Check if department has employees
|
||||
# if self.object.get_employee_count() > 0:
|
||||
# messages.error(request, 'Cannot delete department with active employees.')
|
||||
# return redirect('core:department_detail', pk=self.object.pk)
|
||||
#
|
||||
# # Soft delete - set to inactive
|
||||
# self.object.is_active = False
|
||||
# self.object.save()
|
||||
#
|
||||
# # Log department deletion
|
||||
# AuditLogger.log_event(
|
||||
# tenant=self.object.tenant,
|
||||
# event_type='DELETE',
|
||||
# event_category='SYSTEM_ADMINISTRATION',
|
||||
# action='Deactivate Department',
|
||||
# description=f'Deactivated department: {self.object.name}',
|
||||
# user=request.user,
|
||||
# content_object=self.object,
|
||||
# request=request
|
||||
# )
|
||||
#
|
||||
# messages.success(request, f'Department "{self.object.name}" deactivated successfully.')
|
||||
# return redirect(self.success_url)
|
||||
#
|
||||
# import json
|
||||
#
|
||||
|
||||
129
core_data.py
129
core_data.py
@ -7,13 +7,12 @@ django.setup()
|
||||
|
||||
import random
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from django.contrib.auth import get_user_model
|
||||
from accounts.models import User
|
||||
from django.utils import timezone as django_timezone
|
||||
from core.models import Tenant, AuditLogEntry, SystemConfiguration, SystemNotification, IntegrationLog, Department
|
||||
from core.models import Tenant, AuditLogEntry, SystemConfiguration, SystemNotification, IntegrationLog
|
||||
import uuid
|
||||
import json
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
# Saudi-specific data constants
|
||||
SAUDI_CITIES = [
|
||||
@ -119,64 +118,64 @@ def create_super_user():
|
||||
tenant=tenant1 # assumes your User model has a ForeignKey to Tenant named `tenant`
|
||||
)
|
||||
|
||||
def create_saudi_departments(tenants, departments_per_tenant=15):
|
||||
"""Create Saudi healthcare departments"""
|
||||
departments = []
|
||||
|
||||
department_types = [
|
||||
('clinical', 'Clinical Department'),
|
||||
('support', 'Support Department'),
|
||||
('administrative', 'Administrative Department'),
|
||||
('diagnostic', 'Diagnostic Department')
|
||||
]
|
||||
|
||||
for tenant in tenants:
|
||||
# Create main departments
|
||||
for specialty in SAUDI_MEDICAL_SPECIALTIES[:departments_per_tenant]:
|
||||
dept_type = random.choice(department_types)
|
||||
|
||||
department = Department.objects.create(
|
||||
tenant=tenant,
|
||||
department_id=uuid.uuid4(),
|
||||
code=specialty.replace(' ', '').upper()[:10],
|
||||
name=f"Department of {specialty}",
|
||||
description=f"Specialized {specialty.lower()} department providing comprehensive medical care",
|
||||
department_type=dept_type[0],
|
||||
parent_department=None, # Main departments
|
||||
phone=f"+966-{random.randint(1, 9)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}",
|
||||
extension=f"{random.randint(1000, 9999)}",
|
||||
email=f"{specialty.lower().replace(' ', '').replace('and', '')}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa",
|
||||
building=f"Building {random.choice(['A', 'B', 'C', 'D', 'Medical Tower'])}",
|
||||
floor=f"Floor {random.randint(1, 10)}",
|
||||
wing=random.choice(['North Wing', 'South Wing', 'East Wing', 'West Wing', 'Central Wing']),
|
||||
room_numbers=f"{random.randint(100, 999)}-{random.randint(100, 999)}",
|
||||
is_active=True,
|
||||
is_24_hour=specialty in ['Emergency Medicine', 'Internal Medicine', 'Cardiology'],
|
||||
operating_hours={
|
||||
"sunday": {"open": "07:00", "close": "20:00"},
|
||||
"monday": {"open": "07:00", "close": "20:00"},
|
||||
"tuesday": {"open": "07:00", "close": "20:00"},
|
||||
"wednesday": {"open": "07:00", "close": "20:00"},
|
||||
"thursday": {"open": "07:00", "close": "20:00"},
|
||||
"friday": {"open": "14:00", "close": "20:00"}, # Friday afternoon
|
||||
"saturday": {"open": "07:00", "close": "20:00"}
|
||||
},
|
||||
cost_center_code=f"CC-{random.randint(1000, 9999)}",
|
||||
budget_code=f"BG-{specialty.replace(' ', '').upper()[:6]}",
|
||||
authorized_positions=random.randint(5, 50),
|
||||
current_staff_count=random.randint(3, 45),
|
||||
accreditation_required=True,
|
||||
accreditation_body="CBAHI",
|
||||
last_inspection_date=django_timezone.now() - timedelta(days=random.randint(30, 365)),
|
||||
next_inspection_date=django_timezone.now() + timedelta(days=random.randint(30, 365)),
|
||||
created_at=django_timezone.now() - timedelta(days=random.randint(1, 180)),
|
||||
updated_at=django_timezone.now()
|
||||
)
|
||||
departments.append(department)
|
||||
|
||||
print(f"Created {departments_per_tenant} departments for {tenant.name}")
|
||||
|
||||
return departments
|
||||
# def create_saudi_departments(tenants, departments_per_tenant=15):
|
||||
# """Create Saudi healthcare departments"""
|
||||
# departments = []
|
||||
#
|
||||
# department_types = [
|
||||
# ('clinical', 'Clinical Department'),
|
||||
# ('support', 'Support Department'),
|
||||
# ('administrative', 'Administrative Department'),
|
||||
# ('diagnostic', 'Diagnostic Department')
|
||||
# ]
|
||||
#
|
||||
# for tenant in tenants:
|
||||
# # Create main departments
|
||||
# for specialty in SAUDI_MEDICAL_SPECIALTIES[:departments_per_tenant]:
|
||||
# dept_type = random.choice(department_types)
|
||||
#
|
||||
# department = Department.objects.create(
|
||||
# tenant=tenant,
|
||||
# department_id=uuid.uuid4(),
|
||||
# code=specialty.replace(' ', '').upper()[:10],
|
||||
# name=f"Department of {specialty}",
|
||||
# description=f"Specialized {specialty.lower()} department providing comprehensive medical care",
|
||||
# department_type=dept_type[0],
|
||||
# parent_department=None, # Main departments
|
||||
# phone=f"+966-{random.randint(1, 9)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}",
|
||||
# extension=f"{random.randint(1000, 9999)}",
|
||||
# email=f"{specialty.lower().replace(' ', '').replace('and', '')}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa",
|
||||
# building=f"Building {random.choice(['A', 'B', 'C', 'D', 'Medical Tower'])}",
|
||||
# floor=f"Floor {random.randint(1, 10)}",
|
||||
# wing=random.choice(['North Wing', 'South Wing', 'East Wing', 'West Wing', 'Central Wing']),
|
||||
# room_numbers=f"{random.randint(100, 999)}-{random.randint(100, 999)}",
|
||||
# is_active=True,
|
||||
# is_24_hour=specialty in ['Emergency Medicine', 'Internal Medicine', 'Cardiology'],
|
||||
# operating_hours={
|
||||
# "sunday": {"open": "07:00", "close": "20:00"},
|
||||
# "monday": {"open": "07:00", "close": "20:00"},
|
||||
# "tuesday": {"open": "07:00", "close": "20:00"},
|
||||
# "wednesday": {"open": "07:00", "close": "20:00"},
|
||||
# "thursday": {"open": "07:00", "close": "20:00"},
|
||||
# "friday": {"open": "14:00", "close": "20:00"}, # Friday afternoon
|
||||
# "saturday": {"open": "07:00", "close": "20:00"}
|
||||
# },
|
||||
# cost_center_code=f"CC-{random.randint(1000, 9999)}",
|
||||
# budget_code=f"BG-{specialty.replace(' ', '').upper()[:6]}",
|
||||
# authorized_positions=random.randint(5, 50),
|
||||
# current_staff_count=random.randint(3, 45),
|
||||
# accreditation_required=True,
|
||||
# accreditation_body="CBAHI",
|
||||
# last_inspection_date=django_timezone.now() - timedelta(days=random.randint(30, 365)),
|
||||
# next_inspection_date=django_timezone.now() + timedelta(days=random.randint(30, 365)),
|
||||
# created_at=django_timezone.now() - timedelta(days=random.randint(1, 180)),
|
||||
# updated_at=django_timezone.now()
|
||||
# )
|
||||
# departments.append(department)
|
||||
#
|
||||
# print(f"Created {departments_per_tenant} departments for {tenant.name}")
|
||||
#
|
||||
# return departments
|
||||
|
||||
|
||||
def create_saudi_system_configurations(tenants):
|
||||
@ -439,8 +438,8 @@ def main():
|
||||
create_super_user()
|
||||
|
||||
# Create departments
|
||||
print("\n2. Creating Saudi Medical Departments...")
|
||||
departments = create_saudi_departments(tenants, 12)
|
||||
# print("\n2. Creating Saudi Medical Departments...")
|
||||
# departments = create_saudi_departments(tenants, 12)
|
||||
|
||||
# Create system configurations
|
||||
print("\n3. Creating Saudi System Configurations...")
|
||||
@ -461,7 +460,7 @@ def main():
|
||||
print(f"\n✅ Saudi Healthcare Data Generation Complete!")
|
||||
print(f"📊 Summary:")
|
||||
print(f" - Tenants: {len(tenants)}")
|
||||
print(f" - Departments: {len(departments)}")
|
||||
# print(f" - Departments: {len(departments)}")
|
||||
print(f" - System Configurations: {len(configurations)}")
|
||||
print(f" - Notifications: {len(notifications)}")
|
||||
print(f" - Audit Logs: {len(audit_logs)}")
|
||||
@ -469,7 +468,7 @@ def main():
|
||||
|
||||
return {
|
||||
'tenants': tenants,
|
||||
'departments': departments,
|
||||
# 'departments': departments,
|
||||
'configurations': configurations,
|
||||
'notifications': notifications,
|
||||
'audit_logs': audit_logs,
|
||||
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
|
||||
Binary file not shown.
@ -537,7 +537,7 @@ def create_encounters(tenants, days_back=30):
|
||||
suitable_admissions = [
|
||||
adm for adm in admissions
|
||||
if adm.patient == patient and
|
||||
adm.admission_date <= encounter_date
|
||||
adm.admission_datetime <= start_datetime
|
||||
]
|
||||
if suitable_admissions:
|
||||
linked_admission = random.choice(suitable_admissions)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -213,7 +213,7 @@ class DepartmentAdmin(admin.ModelAdmin):
|
||||
Admin interface for departments.
|
||||
"""
|
||||
list_display = [
|
||||
'department_code', 'name', 'department_type',
|
||||
'code', 'name', 'department_type',
|
||||
'department_head', 'employee_count_display',
|
||||
'total_fte_display', 'is_active'
|
||||
]
|
||||
@ -221,7 +221,7 @@ class DepartmentAdmin(admin.ModelAdmin):
|
||||
'tenant', 'department_type', 'is_active'
|
||||
]
|
||||
search_fields = [
|
||||
'department_code', 'name', 'description'
|
||||
'code', 'name', 'description'
|
||||
]
|
||||
readonly_fields = [
|
||||
'department_id', 'employee_count', 'total_fte',
|
||||
@ -230,7 +230,7 @@ class DepartmentAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
('Department Information', {
|
||||
'fields': [
|
||||
'department_id', 'tenant', 'department_code', 'name', 'description'
|
||||
'department_id', 'tenant', 'code', 'name', 'description'
|
||||
]
|
||||
}),
|
||||
('Department Type', {
|
||||
|
||||
@ -274,7 +274,7 @@ class DepartmentForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Department
|
||||
fields = [
|
||||
'name', 'department_code', 'description', 'department_type',
|
||||
'name', 'code', 'description', 'department_type',
|
||||
'parent_department', 'department_head', 'annual_budget',
|
||||
'cost_center', 'location', 'is_active', 'notes'
|
||||
]
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
@ -40,8 +40,11 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
(
|
||||
"department_code",
|
||||
models.CharField(help_text="Department code", max_length=20),
|
||||
"code",
|
||||
models.CharField(
|
||||
help_text="Department code (e.g., CARD, EMER, SURG)",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
("name", models.CharField(help_text="Department name", max_length=100)),
|
||||
(
|
||||
@ -64,6 +67,33 @@ class Migration(migrations.Migration):
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"phone",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Department phone number",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"extension",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Phone extension",
|
||||
max_length=10,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True,
|
||||
help_text="Department email",
|
||||
max_length=254,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"annual_budget",
|
||||
models.DecimalField(
|
||||
@ -83,6 +113,12 @@ class Migration(migrations.Migration):
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"authorized_positions",
|
||||
models.PositiveIntegerField(
|
||||
default=0, help_text="Number of authorized positions"
|
||||
),
|
||||
),
|
||||
(
|
||||
"location",
|
||||
models.CharField(
|
||||
@ -96,6 +132,50 @@ class Migration(migrations.Migration):
|
||||
"is_active",
|
||||
models.BooleanField(default=True, help_text="Department is active"),
|
||||
),
|
||||
(
|
||||
"is_24_hour",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Department operates 24 hours"
|
||||
),
|
||||
),
|
||||
(
|
||||
"operating_hours",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text="Operating hours by day of week",
|
||||
),
|
||||
),
|
||||
(
|
||||
"accreditation_required",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Department requires special accreditation",
|
||||
),
|
||||
),
|
||||
(
|
||||
"accreditation_body",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Accrediting body (e.g., Joint Commission, CAP)",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_inspection_date",
|
||||
models.DateField(
|
||||
blank=True, help_text="Last inspection date", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"next_inspection_date",
|
||||
models.DateField(
|
||||
blank=True,
|
||||
help_text="Next scheduled inspection date",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"notes",
|
||||
models.TextField(
|
||||
@ -122,7 +202,7 @@ class Migration(migrations.Migration):
|
||||
help_text="Parent department",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="child_departments",
|
||||
related_name="sub_departments",
|
||||
to="hr.department",
|
||||
),
|
||||
),
|
||||
@ -131,7 +211,7 @@ class Migration(migrations.Migration):
|
||||
models.ForeignKey(
|
||||
help_text="Organization tenant",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="hr_departments",
|
||||
related_name="departments",
|
||||
to="core.tenant",
|
||||
),
|
||||
),
|
||||
@ -248,6 +328,16 @@ class Migration(migrations.Migration):
|
||||
blank=True, help_text="Country", max_length=50, null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"national_id",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="National ID",
|
||||
max_length=10,
|
||||
null=True,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"date_of_birth",
|
||||
models.DateField(blank=True, help_text="Date of birth", null=True),
|
||||
@ -1193,6 +1283,12 @@ class Migration(migrations.Migration):
|
||||
"passed",
|
||||
models.BooleanField(default=False, help_text="Training passed"),
|
||||
),
|
||||
(
|
||||
"is_certified",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Training is certified"
|
||||
),
|
||||
),
|
||||
(
|
||||
"certificate_number",
|
||||
models.CharField(
|
||||
@ -1299,9 +1395,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="department",
|
||||
index=models.Index(
|
||||
fields=["department_code"], name="hr_departme_departm_078f94_idx"
|
||||
),
|
||||
index=models.Index(fields=["code"], name="hr_departme_code_d27daf_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="department",
|
||||
@ -1315,7 +1409,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="department",
|
||||
unique_together={("tenant", "department_code")},
|
||||
unique_together={("tenant", "code")},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="performancereview",
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.2.4 on 2025-09-07 14:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("hr", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="trainingrecord",
|
||||
name="is_certified",
|
||||
field=models.BooleanField(default=False, help_text="Training is certified"),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@ -422,7 +422,7 @@ class Department(models.Model):
|
||||
editable=False,
|
||||
help_text='Unique department identifier'
|
||||
)
|
||||
department_code = models.CharField(
|
||||
code = models.CharField(
|
||||
max_length=20,
|
||||
help_text='Department code (e.g., CARD, EMER, SURG)'
|
||||
)
|
||||
@ -570,14 +570,14 @@ class Department(models.Model):
|
||||
ordering = ['name']
|
||||
indexes = [
|
||||
models.Index(fields=['tenant', 'department_type']),
|
||||
models.Index(fields=['department_code']),
|
||||
models.Index(fields=['code']),
|
||||
models.Index(fields=['name']),
|
||||
models.Index(fields=['is_active']),
|
||||
]
|
||||
unique_together = ['tenant', 'department_code']
|
||||
unique_together = ['tenant', 'code']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.department_code} - {self.name}"
|
||||
return f"{self.code} - {self.name}"
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
|
||||
@ -30,6 +30,14 @@ urlpatterns = [
|
||||
path('departments/<int:pk>/', views.DepartmentDetailView.as_view(), name='department_detail'),
|
||||
path('departments/<int:pk>/update/', views.DepartmentUpdateView.as_view(), name='department_update'),
|
||||
path('departments/<int:pk>/delete/', views.DepartmentDeleteView.as_view(), name='department_delete'),
|
||||
path('departments/<int:pk>/activate/', views.activate_department, name='activate_department'),
|
||||
path('departments/<int:pk>/deactivate/', views.deactivate_department, name='deactivate_department'),
|
||||
path('departments/bulk-activate/', views.bulk_activate_departments, name='bulk_activate_departments'),
|
||||
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('search/departments/', views.department_search, name='department_search'),
|
||||
|
||||
# ============================================================================
|
||||
# SCHEDULE URLS (LIMITED CRUD - Operational Data)
|
||||
|
||||
219
hr/views.py
219
hr/views.py
@ -17,7 +17,7 @@ from django.core.paginator import Paginator
|
||||
from django.db import transaction
|
||||
from datetime import datetime, timedelta, date
|
||||
import json
|
||||
|
||||
from accounts.models import User
|
||||
from .models import (
|
||||
Employee, Department, Schedule, ScheduleAssignment,
|
||||
TimeEntry, PerformanceReview, TrainingRecord
|
||||
@ -26,6 +26,7 @@ from .forms import (
|
||||
EmployeeForm, DepartmentForm, ScheduleForm, ScheduleAssignmentForm,
|
||||
TimeEntryForm, PerformanceReviewForm, TrainingRecordForm
|
||||
)
|
||||
from core.utils import AuditLogger
|
||||
|
||||
|
||||
class HRDashboardView(LoginRequiredMixin, TemplateView):
|
||||
@ -1284,6 +1285,222 @@ def api_department_list(request):
|
||||
return JsonResponse({'departments': list(departments)})
|
||||
|
||||
|
||||
def department_tree(request):
|
||||
"""
|
||||
HTMX view for department tree structure.
|
||||
"""
|
||||
departments = Department.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
parent=None
|
||||
).prefetch_related('children')
|
||||
|
||||
return render(request, 'core/partials/department_tree.html', {
|
||||
'departments': departments
|
||||
})
|
||||
|
||||
|
||||
def activate_department(request, pk):
|
||||
"""
|
||||
Activate a department.
|
||||
"""
|
||||
department = get_object_or_404(Department, department_id=pk)
|
||||
department.is_active = True
|
||||
department.save()
|
||||
|
||||
messages.success(request, f'Department "{department.name}" has been activated.')
|
||||
return redirect('hr:department_detail', pk=pk)
|
||||
|
||||
|
||||
def deactivate_department(request, pk):
|
||||
"""
|
||||
Deactivate a department.
|
||||
"""
|
||||
department = get_object_or_404(Department, department_id=pk)
|
||||
department.is_active = False
|
||||
department.save()
|
||||
|
||||
messages.success(request, f'Department "{department.name}" has been deactivated.')
|
||||
return redirect('hr:department_detail', pk=pk)
|
||||
|
||||
|
||||
def department_search(request):
|
||||
"""
|
||||
AJAX search for departments.
|
||||
"""
|
||||
query = request.GET.get('q', '')
|
||||
departments = []
|
||||
|
||||
if query:
|
||||
departments = Department.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
name__icontains=query
|
||||
).values('department_id', 'name', 'department_type')[:10]
|
||||
|
||||
return JsonResponse({'departments': list(departments)})
|
||||
|
||||
|
||||
def bulk_activate_departments(request):
|
||||
"""
|
||||
Bulk activate departments.
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
department_ids = request.POST.getlist('department_ids')
|
||||
count = Department.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
department_id__in=department_ids
|
||||
).update(is_active=True)
|
||||
|
||||
messages.success(request, f'{count} departments have been activated.')
|
||||
|
||||
return redirect('hr:department_list')
|
||||
|
||||
|
||||
def bulk_deactivate_departments(request):
|
||||
"""
|
||||
Bulk deactivate departments.
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
department_ids = request.POST.getlist('department_ids')
|
||||
count = Department.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
department_id__in=department_ids
|
||||
).update(is_active=False)
|
||||
|
||||
messages.success(request, f'{count} departments have been deactivated.')
|
||||
|
||||
return redirect('hr:department_list')
|
||||
|
||||
|
||||
def get_department_hierarchy(request):
|
||||
"""
|
||||
Get department hierarchy as JSON.
|
||||
"""
|
||||
departments = Department.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
is_active=True
|
||||
).select_related('parent')
|
||||
|
||||
def build_tree(parent=None):
|
||||
children = []
|
||||
for dept in departments:
|
||||
if dept.parent == parent:
|
||||
children.append({
|
||||
'id': str(dept.department_id),
|
||||
'name': dept.name,
|
||||
'type': dept.department_type,
|
||||
'children': build_tree(dept)
|
||||
})
|
||||
return children
|
||||
|
||||
hierarchy = build_tree()
|
||||
return JsonResponse({'hierarchy': hierarchy})
|
||||
|
||||
|
||||
@login_required
|
||||
def assign_department_head(request, pk):
|
||||
"""
|
||||
Assign a department head to a department.
|
||||
"""
|
||||
department = get_object_or_404(Department, pk=pk, tenant=request.user.tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
user_id = request.POST.get('user_id')
|
||||
|
||||
if user_id:
|
||||
try:
|
||||
user = User.objects.get(id=user_id, tenant=request.user.tenant)
|
||||
|
||||
# Remove current department head if exists
|
||||
if department.department_head:
|
||||
old_head = department.department_head
|
||||
AuditLogger.log_event(
|
||||
request=request,
|
||||
event_type='UPDATE',
|
||||
event_category='SYSTEM_ADMINISTRATION',
|
||||
action=f'Removed department head from {department.name}',
|
||||
description=f'Removed {old_head.get_full_name()} as head of {department.name}',
|
||||
content_object=department,
|
||||
additional_data={
|
||||
'old_department_head_id': old_head.id,
|
||||
'old_department_head_name': old_head.get_full_name()
|
||||
}
|
||||
)
|
||||
|
||||
# Assign new department head
|
||||
department.department_head = user
|
||||
department.save()
|
||||
|
||||
# Log the assignment
|
||||
AuditLogger.log_event(
|
||||
request=request,
|
||||
event_type='UPDATE',
|
||||
event_category='SYSTEM_ADMINISTRATION',
|
||||
action=f'Assigned department head to {department.name}',
|
||||
description=f'Assigned {user.get_full_name()} as head of {department.name}',
|
||||
content_object=department,
|
||||
additional_data={
|
||||
'new_department_head_id': user.id,
|
||||
'new_department_head_name': user.get_full_name()
|
||||
}
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f'{user.get_full_name()} has been assigned as head of {department.name}.'
|
||||
)
|
||||
return redirect('core:department_detail', pk=department.pk)
|
||||
|
||||
except User.DoesNotExist:
|
||||
messages.error(request, 'Selected user not found.')
|
||||
else:
|
||||
# Remove department head
|
||||
if department.department_head:
|
||||
old_head = department.department_head
|
||||
department.department_head = None
|
||||
department.save()
|
||||
|
||||
# Log the removal
|
||||
AuditLogger.log_event(
|
||||
request=request,
|
||||
event_type='UPDATE',
|
||||
event_category='SYSTEM_ADMINISTRATION',
|
||||
action=f'Removed department head from {department.name}',
|
||||
description=f'Removed {old_head.get_full_name()} as head of {department.name}',
|
||||
content_object=department,
|
||||
additional_data={
|
||||
'removed_department_head_id': old_head.id,
|
||||
'removed_department_head_name': old_head.get_full_name()
|
||||
}
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f'Department head has been removed from {department.name}.'
|
||||
)
|
||||
else:
|
||||
messages.info(request, 'No department head was assigned.')
|
||||
|
||||
return redirect('core:department_detail', pk=department.pk)
|
||||
|
||||
# Get eligible users (staff members who can be department heads)
|
||||
eligible_users = User.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
is_active=True,
|
||||
is_staff=True
|
||||
).exclude(
|
||||
id=department.department_head.id if department.department_head else None
|
||||
).order_by('first_name', 'last_name')
|
||||
|
||||
context = {
|
||||
'department': department,
|
||||
'eligible_users': eligible_users,
|
||||
'current_head': department.department_head,
|
||||
}
|
||||
|
||||
return render(request, 'hr/departments/assign_department_head.html', context)
|
||||
|
||||
|
||||
|
||||
# Query patterns to use if needed
|
||||
# # All upcoming sessions for a tenant (next 30 days)
|
||||
# TrainingSession.objects.filter(
|
||||
|
||||
@ -106,7 +106,7 @@ def create_saudi_departments(tenants):
|
||||
for tenant in tenants:
|
||||
# Check for existing departments to avoid duplicates
|
||||
existing_dept_codes = set(
|
||||
Department.objects.filter(tenant=tenant).values_list('department_code', flat=True)
|
||||
Department.objects.filter(tenant=tenant).values_list('code', flat=True)
|
||||
)
|
||||
|
||||
for dept_code, dept_name, dept_desc in SAUDI_DEPARTMENTS:
|
||||
@ -129,7 +129,7 @@ def create_saudi_departments(tenants):
|
||||
try:
|
||||
department = Department.objects.create(
|
||||
tenant=tenant,
|
||||
department_code=dept_code,
|
||||
code=dept_code,
|
||||
name=dept_name,
|
||||
description=dept_desc,
|
||||
department_type=dept_type,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
@ -403,6 +403,15 @@ class Migration(migrations.Migration):
|
||||
max_length=30,
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_operational",
|
||||
models.BooleanField(default=True, help_text="Operational status"),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(default=True, help_text="Active status"),
|
||||
),
|
||||
("is_active_out_of_service", models.BooleanField(default=True)),
|
||||
(
|
||||
"room_type",
|
||||
models.CharField(
|
||||
@ -518,10 +527,12 @@ class Migration(migrations.Migration):
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("WINDOW", "Window Side"),
|
||||
("DOOR", "Door Side"),
|
||||
("CENTER", "Center"),
|
||||
("CORNER", "Corner"),
|
||||
("A", "A"),
|
||||
("B", "B"),
|
||||
("C", "C"),
|
||||
("D", "D"),
|
||||
("E", "E"),
|
||||
("F", "F"),
|
||||
],
|
||||
help_text="Position within room",
|
||||
max_length=20,
|
||||
@ -619,7 +630,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"ward_id",
|
||||
models.CharField(help_text="Unique ward identifier", max_length=20),
|
||||
models.CharField(help_text="Unique ward identifier", max_length=50),
|
||||
),
|
||||
("name", models.CharField(help_text="Ward name", max_length=200)),
|
||||
(
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-19 15:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("inpatients", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="ward",
|
||||
name="ward_id",
|
||||
field=models.CharField(help_text="Unique ward identifier", max_length=50),
|
||||
),
|
||||
]
|
||||
@ -1,31 +0,0 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-19 19:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("inpatients", "0002_alter_ward_ward_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="bed",
|
||||
name="bed_position",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("A", "A"),
|
||||
("B", "B"),
|
||||
("C", "C"),
|
||||
("D", "D"),
|
||||
("E", "E"),
|
||||
("F", "F"),
|
||||
],
|
||||
help_text="Position within room",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,28 +0,0 @@
|
||||
# Generated by Django 5.2.4 on 2025-09-03 15:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("inpatients", "0003_alter_bed_bed_position"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="bed",
|
||||
name="is_active",
|
||||
field=models.BooleanField(default=True, help_text="Active status"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="bed",
|
||||
name="is_active_out_of_service",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="bed",
|
||||
name="is_operational",
|
||||
field=models.BooleanField(default=True, help_text="Operational status"),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
|
||||
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
@ -325,6 +325,12 @@ class Migration(migrations.Migration):
|
||||
default=0, help_text="Reorder quantity"
|
||||
),
|
||||
),
|
||||
(
|
||||
"min_stock_level",
|
||||
models.PositiveIntegerField(
|
||||
blank=True, help_text="Minimum stock level", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"max_stock_level",
|
||||
models.PositiveIntegerField(
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-05 13:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("inventory", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="inventoryitem",
|
||||
name="min_stock_level",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Minimum stock level", null=True
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
@ -19,6 +19,177 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LabOrder",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"order_id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
help_text="Unique order identifier",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"order_number",
|
||||
models.CharField(
|
||||
help_text="Lab order number", max_length=20, unique=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"order_datetime",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now,
|
||||
help_text="Date and time order was placed",
|
||||
),
|
||||
),
|
||||
(
|
||||
"priority",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("ROUTINE", "Routine"),
|
||||
("URGENT", "Urgent"),
|
||||
("STAT", "STAT"),
|
||||
("ASAP", "ASAP"),
|
||||
("TIMED", "Timed"),
|
||||
],
|
||||
default="ROUTINE",
|
||||
help_text="Order priority",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"clinical_indication",
|
||||
models.TextField(
|
||||
blank=True, help_text="Clinical indication for tests", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"diagnosis_code",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="ICD-10 diagnosis code",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"clinical_notes",
|
||||
models.TextField(blank=True, help_text="Clinical notes", null=True),
|
||||
),
|
||||
(
|
||||
"collection_datetime",
|
||||
models.DateTimeField(
|
||||
blank=True,
|
||||
help_text="Requested collection date and time",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"collection_location",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Collection location",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"fasting_status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("FASTING", "Fasting"),
|
||||
("NON_FASTING", "Non-Fasting"),
|
||||
("UNKNOWN", "Unknown"),
|
||||
],
|
||||
default="UNKNOWN",
|
||||
help_text="Patient fasting status",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("PENDING", "Pending"),
|
||||
("SCHEDULED", "Scheduled"),
|
||||
("COLLECTED", "Collected"),
|
||||
("IN_PROGRESS", "In Progress"),
|
||||
("COMPLETED", "Completed"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
("ON_HOLD", "On Hold"),
|
||||
],
|
||||
default="PENDING",
|
||||
help_text="Order status",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"special_instructions",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="Special instructions for collection or processing",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"encounter",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Related encounter",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="lab_orders",
|
||||
to="emr.encounter",
|
||||
),
|
||||
),
|
||||
(
|
||||
"ordering_provider",
|
||||
models.ForeignKey(
|
||||
help_text="Ordering provider",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="ordered_lab_tests",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"patient",
|
||||
models.ForeignKey(
|
||||
help_text="Patient",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="lab_orders",
|
||||
to="patients.patientprofile",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
help_text="Organization tenant",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="lab_orders",
|
||||
to="core.tenant",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Lab Order",
|
||||
"verbose_name_plural": "Lab Orders",
|
||||
"db_table": "laboratory_lab_order",
|
||||
"ordering": ["-order_datetime"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="LabTest",
|
||||
fields=[
|
||||
@ -352,7 +523,7 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="LabOrder",
|
||||
name="LabResult",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
@ -364,89 +535,123 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
(
|
||||
"order_id",
|
||||
"result_id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
help_text="Unique order identifier",
|
||||
help_text="Unique result identifier",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"order_number",
|
||||
"result_value",
|
||||
models.TextField(
|
||||
blank=True, help_text="Test result value", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"result_unit",
|
||||
models.CharField(
|
||||
help_text="Lab order number", max_length=20, unique=True
|
||||
blank=True,
|
||||
help_text="Result unit of measure",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"order_datetime",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now,
|
||||
help_text="Date and time order was placed",
|
||||
),
|
||||
),
|
||||
(
|
||||
"priority",
|
||||
"result_type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("ROUTINE", "Routine"),
|
||||
("URGENT", "Urgent"),
|
||||
("STAT", "STAT"),
|
||||
("ASAP", "ASAP"),
|
||||
("TIMED", "Timed"),
|
||||
("NUMERIC", "Numeric"),
|
||||
("TEXT", "Text"),
|
||||
("CODED", "Coded"),
|
||||
("NARRATIVE", "Narrative"),
|
||||
],
|
||||
default="ROUTINE",
|
||||
help_text="Order priority",
|
||||
default="NUMERIC",
|
||||
help_text="Type of result",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"clinical_indication",
|
||||
models.TextField(
|
||||
blank=True, help_text="Clinical indication for tests", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"diagnosis_code",
|
||||
"reference_range",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="ICD-10 diagnosis code",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"clinical_notes",
|
||||
models.TextField(blank=True, help_text="Clinical notes", null=True),
|
||||
),
|
||||
(
|
||||
"collection_datetime",
|
||||
models.DateTimeField(
|
||||
blank=True,
|
||||
help_text="Requested collection date and time",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"collection_location",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Collection location",
|
||||
help_text="Reference range",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"fasting_status",
|
||||
"abnormal_flag",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("FASTING", "Fasting"),
|
||||
("NON_FASTING", "Non-Fasting"),
|
||||
("UNKNOWN", "Unknown"),
|
||||
("N", "Normal"),
|
||||
("H", "High"),
|
||||
("L", "Low"),
|
||||
("HH", "Critical High"),
|
||||
("LL", "Critical Low"),
|
||||
("A", "Abnormal"),
|
||||
],
|
||||
default="UNKNOWN",
|
||||
help_text="Patient fasting status",
|
||||
max_length=20,
|
||||
help_text="Abnormal flag",
|
||||
max_length=10,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_critical",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Result is critical value"
|
||||
),
|
||||
),
|
||||
(
|
||||
"critical_called",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Critical value was called to provider"
|
||||
),
|
||||
),
|
||||
(
|
||||
"critical_called_datetime",
|
||||
models.DateTimeField(
|
||||
blank=True,
|
||||
help_text="Date and time critical value was called",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"critical_called_to",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Person critical value was called to",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"analyzed_datetime",
|
||||
models.DateTimeField(
|
||||
blank=True, help_text="Date and time analyzed", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"analyzer",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Analyzer/instrument used",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"verified",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Result has been verified"
|
||||
),
|
||||
),
|
||||
(
|
||||
"verified_datetime",
|
||||
models.DateTimeField(
|
||||
blank=True, help_text="Date and time of verification", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
@ -454,82 +659,108 @@ class Migration(migrations.Migration):
|
||||
models.CharField(
|
||||
choices=[
|
||||
("PENDING", "Pending"),
|
||||
("SCHEDULED", "Scheduled"),
|
||||
("COLLECTED", "Collected"),
|
||||
("IN_PROGRESS", "In Progress"),
|
||||
("COMPLETED", "Completed"),
|
||||
("VERIFIED", "Verified"),
|
||||
("AMENDED", "Amended"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
("ON_HOLD", "On Hold"),
|
||||
],
|
||||
default="PENDING",
|
||||
help_text="Order status",
|
||||
help_text="Result status",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"special_instructions",
|
||||
"technician_comments",
|
||||
models.TextField(
|
||||
blank=True, help_text="Technician comments", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"pathologist_comments",
|
||||
models.TextField(
|
||||
blank=True, help_text="Pathologist comments", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"qc_passed",
|
||||
models.BooleanField(
|
||||
default=True, help_text="Quality control passed"
|
||||
),
|
||||
),
|
||||
(
|
||||
"qc_notes",
|
||||
models.TextField(
|
||||
blank=True, help_text="Quality control notes", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"reported_datetime",
|
||||
models.DateTimeField(
|
||||
blank=True,
|
||||
help_text="Special instructions for collection or processing",
|
||||
help_text="Date and time result was reported",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"encounter",
|
||||
"analyzed_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Related encounter",
|
||||
help_text="Lab technician who analyzed",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="lab_orders",
|
||||
to="emr.encounter",
|
||||
),
|
||||
),
|
||||
(
|
||||
"ordering_provider",
|
||||
models.ForeignKey(
|
||||
help_text="Ordering provider",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="ordered_lab_tests",
|
||||
related_name="analyzed_results",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"patient",
|
||||
"order",
|
||||
models.ForeignKey(
|
||||
help_text="Patient",
|
||||
help_text="Related lab order",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="lab_orders",
|
||||
to="patients.patientprofile",
|
||||
related_name="results",
|
||||
to="laboratory.laborder",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tenant",
|
||||
"verified_by",
|
||||
models.ForeignKey(
|
||||
help_text="Organization tenant",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="lab_orders",
|
||||
to="core.tenant",
|
||||
blank=True,
|
||||
help_text="Lab professional who verified result",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="verified_results",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"tests",
|
||||
models.ManyToManyField(
|
||||
help_text="Ordered tests",
|
||||
related_name="orders",
|
||||
"test",
|
||||
models.ForeignKey(
|
||||
help_text="Lab test",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="results",
|
||||
to="laboratory.labtest",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Lab Order",
|
||||
"verbose_name_plural": "Lab Orders",
|
||||
"db_table": "laboratory_lab_order",
|
||||
"ordering": ["-order_datetime"],
|
||||
"verbose_name": "Lab Result",
|
||||
"verbose_name_plural": "Lab Results",
|
||||
"db_table": "laboratory_lab_result",
|
||||
"ordering": ["-analyzed_datetime"],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="laborder",
|
||||
name="tests",
|
||||
field=models.ManyToManyField(
|
||||
help_text="Ordered tests",
|
||||
related_name="orders",
|
||||
to="laboratory.labtest",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="QualityControl",
|
||||
fields=[
|
||||
@ -647,6 +878,14 @@ class Migration(migrations.Migration):
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"result",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="quality_controls",
|
||||
to="laboratory.labresult",
|
||||
),
|
||||
),
|
||||
(
|
||||
"reviewed_by",
|
||||
models.ForeignKey(
|
||||
@ -1033,244 +1272,15 @@ class Migration(migrations.Migration):
|
||||
"ordering": ["-collected_datetime"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="LabResult",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"result_id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
help_text="Unique result identifier",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"result_value",
|
||||
models.TextField(
|
||||
blank=True, help_text="Test result value", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"result_unit",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Result unit of measure",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"result_type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("NUMERIC", "Numeric"),
|
||||
("TEXT", "Text"),
|
||||
("CODED", "Coded"),
|
||||
("NARRATIVE", "Narrative"),
|
||||
],
|
||||
default="NUMERIC",
|
||||
help_text="Type of result",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"reference_range",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Reference range",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"abnormal_flag",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("N", "Normal"),
|
||||
("H", "High"),
|
||||
("L", "Low"),
|
||||
("HH", "Critical High"),
|
||||
("LL", "Critical Low"),
|
||||
("A", "Abnormal"),
|
||||
],
|
||||
help_text="Abnormal flag",
|
||||
max_length=10,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_critical",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Result is critical value"
|
||||
),
|
||||
),
|
||||
(
|
||||
"critical_called",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Critical value was called to provider"
|
||||
),
|
||||
),
|
||||
(
|
||||
"critical_called_datetime",
|
||||
models.DateTimeField(
|
||||
blank=True,
|
||||
help_text="Date and time critical value was called",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"critical_called_to",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Person critical value was called to",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"analyzed_datetime",
|
||||
models.DateTimeField(
|
||||
blank=True, help_text="Date and time analyzed", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"analyzer",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Analyzer/instrument used",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"verified",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Result has been verified"
|
||||
),
|
||||
),
|
||||
(
|
||||
"verified_datetime",
|
||||
models.DateTimeField(
|
||||
blank=True, help_text="Date and time of verification", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("PENDING", "Pending"),
|
||||
("IN_PROGRESS", "In Progress"),
|
||||
("COMPLETED", "Completed"),
|
||||
("VERIFIED", "Verified"),
|
||||
("AMENDED", "Amended"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
],
|
||||
default="PENDING",
|
||||
help_text="Result status",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"technician_comments",
|
||||
models.TextField(
|
||||
blank=True, help_text="Technician comments", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"pathologist_comments",
|
||||
models.TextField(
|
||||
blank=True, help_text="Pathologist comments", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"qc_passed",
|
||||
models.BooleanField(
|
||||
default=True, help_text="Quality control passed"
|
||||
),
|
||||
),
|
||||
(
|
||||
"qc_notes",
|
||||
models.TextField(
|
||||
blank=True, help_text="Quality control notes", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"reported_datetime",
|
||||
models.DateTimeField(
|
||||
blank=True,
|
||||
help_text="Date and time result was reported",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"analyzed_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Lab technician who analyzed",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="analyzed_results",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"order",
|
||||
models.ForeignKey(
|
||||
help_text="Related lab order",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="results",
|
||||
to="laboratory.laborder",
|
||||
),
|
||||
),
|
||||
(
|
||||
"verified_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Lab professional who verified result",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="verified_results",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"test",
|
||||
models.ForeignKey(
|
||||
help_text="Lab test",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="results",
|
||||
to="laboratory.labtest",
|
||||
),
|
||||
),
|
||||
(
|
||||
"specimen",
|
||||
models.ForeignKey(
|
||||
help_text="Specimen used for test",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="results",
|
||||
to="laboratory.specimen",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Lab Result",
|
||||
"verbose_name_plural": "Lab Results",
|
||||
"db_table": "laboratory_lab_result",
|
||||
"ordering": ["-analyzed_datetime"],
|
||||
},
|
||||
migrations.AddField(
|
||||
model_name="labresult",
|
||||
name="specimen",
|
||||
field=models.ForeignKey(
|
||||
help_text="Specimen used for test",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="results",
|
||||
to="laboratory.specimen",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="labtest",
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-28 19:46
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("laboratory", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="qualitycontrol",
|
||||
name="result",
|
||||
field=models.ForeignKey(
|
||||
default=1,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="quality_controls",
|
||||
to="laboratory.labresult",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 04:41
|
||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
@ -1283,7 +1283,7 @@ class Migration(migrations.Migration):
|
||||
models.ForeignKey(
|
||||
help_text="Operating surgeon",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="surgical_notes",
|
||||
related_name="surgeon_surgical_notes",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
@ -1292,7 +1292,7 @@ class Migration(migrations.Migration):
|
||||
models.OneToOneField(
|
||||
help_text="Related surgical case",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="surgical_note",
|
||||
related_name="surgical_notes",
|
||||
to="operating_theatre.surgicalcase",
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
# Generated by Django 5.2.4 on 2025-09-04 15:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("operating_theatre", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="surgicalnote",
|
||||
name="surgeon",
|
||||
field=models.ForeignKey(
|
||||
help_text="Operating surgeon",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="surgeon_surgical_notes",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="surgicalnote",
|
||||
name="surgical_case",
|
||||
field=models.OneToOneField(
|
||||
help_text="Related surgical case",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="surgical_notes",
|
||||
to="operating_theatre.surgicalcase",
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -76,13 +76,14 @@ class EmergencyContactForm(forms.ModelForm):
|
||||
"""
|
||||
class Meta:
|
||||
model = EmergencyContact
|
||||
exclude = ["patient"]
|
||||
fields = [
|
||||
'patient', 'first_name', 'last_name', 'relationship', 'phone_number',
|
||||
'first_name', 'last_name', 'relationship', 'phone_number',
|
||||
'mobile_number', 'email', 'address_line_1', 'address_line_2', 'city',
|
||||
'state', 'zip_code', 'priority', 'is_authorized_for_medical_decisions'
|
||||
]
|
||||
widgets = {
|
||||
'patient': forms.Select(attrs={'class': 'form-select'}),
|
||||
# 'patient': forms.Select(attrs={'class': 'form-select'}),
|
||||
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'relationship': forms.Select(attrs={'class': 'form-select'}),
|
||||
@ -102,11 +103,11 @@ class EmergencyContactForm(forms.ModelForm):
|
||||
user = kwargs.pop('user', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if user and hasattr(user, 'tenant'):
|
||||
self.fields['patient'].queryset = PatientProfile.objects.filter(
|
||||
tenant=user.tenant,
|
||||
is_active=True
|
||||
).order_by('last_name', 'first_name')
|
||||
# if user and hasattr(user, 'tenant'):
|
||||
# self.fields['patient'].queryset = PatientProfile.objects.filter(
|
||||
# tenant=user.tenant,
|
||||
# is_active=True
|
||||
# ).order_by('last_name', 'first_name')
|
||||
|
||||
|
||||
class InsuranceInfoForm(forms.ModelForm):
|
||||
@ -115,15 +116,15 @@ class InsuranceInfoForm(forms.ModelForm):
|
||||
"""
|
||||
class Meta:
|
||||
model = InsuranceInfo
|
||||
exclude = ["patient"]
|
||||
fields = [
|
||||
'patient', 'insurance_type', 'insurance_company', 'plan_name', 'plan_type',
|
||||
'insurance_type', 'insurance_company', 'plan_name', 'plan_type',
|
||||
'policy_number', 'group_number', 'subscriber_name', 'subscriber_relationship',
|
||||
'subscriber_dob', 'subscriber_ssn', 'effective_date', 'termination_date',
|
||||
'copay_amount', 'deductible_amount', 'out_of_pocket_max', 'is_verified',
|
||||
'requires_authorization'
|
||||
]
|
||||
widgets = {
|
||||
'patient': forms.Select(attrs={'class': 'form-select'}),
|
||||
'insurance_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'insurance_company': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'plan_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
@ -147,11 +148,11 @@ class InsuranceInfoForm(forms.ModelForm):
|
||||
user = kwargs.pop('user', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if user and hasattr(user, 'tenant'):
|
||||
self.fields['patient'].queryset = PatientProfile.objects.filter(
|
||||
tenant=user.tenant,
|
||||
is_active=True
|
||||
).order_by('last_name', 'first_name')
|
||||
# if user and hasattr(user, 'tenant'):
|
||||
# self.fields['patient'].queryset = PatientProfile.objects.filter(
|
||||
# tenant=user.tenant,
|
||||
# is_active=True
|
||||
# ).order_by('last_name', 'first_name')
|
||||
|
||||
|
||||
class ConsentTemplateForm(forms.ModelForm):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user