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.models
|
||||||
import django.contrib.auth.validators
|
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
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
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.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -207,6 +207,26 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
("is_active", models.BooleanField(default=True)),
|
("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)),
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
("updated_at", models.DateTimeField(auto_now=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
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
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.core.validators
|
||||||
import django.db.models.deletion
|
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
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -10,12 +10,20 @@ app_name = 'appointments'
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Main views
|
# Main views
|
||||||
path('', views.AppointmentDashboardView.as_view(), name='dashboard'),
|
path('', views.AppointmentDashboardView.as_view(), name='dashboard'),
|
||||||
path('list/', views.AppointmentListView.as_view(), name='appointment_list'),
|
path('requests/', views.AppointmentListView.as_view(), name='appointment_list'),
|
||||||
path('create/', views.AppointmentRequestCreateView.as_view(), name='appointment_create'),
|
path('requests/create/', views.AppointmentRequestCreateView.as_view(), name='appointment_create'),
|
||||||
path('detail/<int:pk>/', views.AppointmentDetailView.as_view(), name='appointment_detail'),
|
path('requests/<int:pk>/detail/', views.AppointmentDetailView.as_view(), name='appointment_detail'),
|
||||||
path('calendar/', views.SchedulingCalendarView.as_view(), name='scheduling_calendar'),
|
path('calendar/', views.SchedulingCalendarView.as_view(), name='scheduling_calendar'),
|
||||||
path('queue/', views.QueueManagementView.as_view(), name='queue_management'),
|
path('queue/', views.QueueManagementView.as_view(), name='queue_management'),
|
||||||
|
|
||||||
|
# Telemedicine
|
||||||
path('telemedicine/', views.TelemedicineView.as_view(), name='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
|
# HTMX endpoints
|
||||||
path('search/', views.appointment_search, name='appointment_search'),
|
path('search/', views.appointment_search, name='appointment_search'),
|
||||||
@ -27,9 +35,10 @@ urlpatterns = [
|
|||||||
# Actions
|
# Actions
|
||||||
path('check-in/<int:appointment_id>/', views.check_in_patient, name='check_in_patient'),
|
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('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('complete/<int:appointment_id>/', views.complete_appointment, name='complete_appointment'),
|
||||||
path('reschedule/<int:appointment_id>/', views.reschedule_appointment, name='reschedule_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
|
# API endpoints
|
||||||
# path('api/', include('appointments.api.urls')),
|
# path('api/', include('appointments.api.urls')),
|
||||||
|
|||||||
@ -31,10 +31,6 @@ from core.utils import AuditLogger
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# DASHBOARD VIEW
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class AppointmentDashboardView(LoginRequiredMixin, TemplateView):
|
class AppointmentDashboardView(LoginRequiredMixin, TemplateView):
|
||||||
"""
|
"""
|
||||||
Appointment dashboard view.
|
Appointment dashboard view.
|
||||||
@ -85,10 +81,6 @@ class AppointmentDashboardView(LoginRequiredMixin, TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# APPOINTMENT REQUEST VIEWS (RESTRICTED CRUD - Clinical Data)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class AppointmentRequestListView(LoginRequiredMixin, ListView):
|
class AppointmentRequestListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List appointment requests.
|
List appointment requests.
|
||||||
@ -199,7 +191,7 @@ class AppointmentRequestCreateView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
"""
|
"""
|
||||||
model = AppointmentRequest
|
model = AppointmentRequest
|
||||||
form_class = AppointmentRequestForm
|
form_class = AppointmentRequestForm
|
||||||
template_name = 'appointments/appointment_request_form.html'
|
template_name = 'appointments/requests/appointment_form.html'
|
||||||
permission_required = 'appointments.add_appointmentrequest'
|
permission_required = 'appointments.add_appointmentrequest'
|
||||||
success_url = reverse_lazy('appointments:appointment_request_list')
|
success_url = reverse_lazy('appointments:appointment_request_list')
|
||||||
|
|
||||||
@ -235,7 +227,7 @@ class AppointmentRequestUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
"""
|
"""
|
||||||
model = AppointmentRequest
|
model = AppointmentRequest
|
||||||
form_class = AppointmentRequestForm
|
form_class = AppointmentRequestForm
|
||||||
template_name = 'appointments/appointment_request_form.html'
|
template_name = 'appointments/requests/appointment_form.html'
|
||||||
permission_required = 'appointments.change_appointmentrequest'
|
permission_required = 'appointments.change_appointmentrequest'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -276,7 +268,7 @@ class AppointmentRequestDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
Cancel appointment request.
|
Cancel appointment request.
|
||||||
"""
|
"""
|
||||||
model = AppointmentRequest
|
model = AppointmentRequest
|
||||||
template_name = 'appointments/appointment_request_confirm_delete.html'
|
template_name = 'appointments/requests/appointment_confirm_delete.html'
|
||||||
permission_required = 'appointments.delete_appointmentrequest'
|
permission_required = 'appointments.delete_appointmentrequest'
|
||||||
success_url = reverse_lazy('appointments:appointment_request_list')
|
success_url = reverse_lazy('appointments:appointment_request_list')
|
||||||
|
|
||||||
@ -309,10 +301,6 @@ class AppointmentRequestDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
return redirect(self.success_url)
|
return redirect(self.success_url)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# SLOT AVAILABILITY VIEWS (LIMITED CRUD - Operational Data)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class SlotAvailabilityListView(LoginRequiredMixin, ListView):
|
class SlotAvailabilityListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List slot availability.
|
List slot availability.
|
||||||
@ -514,10 +502,6 @@ class SlotAvailabilityDeleteView(LoginRequiredMixin, PermissionRequiredMixin, De
|
|||||||
return super().delete(request, *args, **kwargs)
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# WAITING QUEUE VIEWS (FULL CRUD - Operational Data)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class WaitingQueueListView(LoginRequiredMixin, ListView):
|
class WaitingQueueListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List waiting queues.
|
List waiting queues.
|
||||||
@ -733,10 +717,6 @@ class WaitingQueueDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Delete
|
|||||||
return super().delete(request, *args, **kwargs)
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# QUEUE ENTRY VIEWS (LIMITED CRUD - Operational Data)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class QueueEntryListView(LoginRequiredMixin, ListView):
|
class QueueEntryListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List queue entries.
|
List queue entries.
|
||||||
@ -878,10 +858,6 @@ class QueueEntryUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVi
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# TELEMEDICINE SESSION VIEWS (RESTRICTED CRUD - Clinical Data)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class TelemedicineSessionListView(LoginRequiredMixin, ListView):
|
class TelemedicineSessionListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List telemedicine sessions.
|
List telemedicine sessions.
|
||||||
@ -935,7 +911,7 @@ class TelemedicineSessionDetailView(LoginRequiredMixin, DetailView):
|
|||||||
Display telemedicine session details.
|
Display telemedicine session details.
|
||||||
"""
|
"""
|
||||||
model = TelemedicineSession
|
model = TelemedicineSession
|
||||||
template_name = 'appointments/telemedicine_session_detail.html'
|
template_name = 'appointments/telemedicine/telemedicine_session_detail.html'
|
||||||
context_object_name = 'session'
|
context_object_name = 'session'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -1021,10 +997,6 @@ class TelemedicineSessionUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# APPOINTMENT TEMPLATE VIEWS (FULL CRUD - Master Data)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class AppointmentTemplateListView(LoginRequiredMixin, ListView):
|
class AppointmentTemplateListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List appointment templates.
|
List appointment templates.
|
||||||
@ -1217,10 +1189,6 @@ class AppointmentTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
return super().delete(request, *args, **kwargs)
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# HTMX VIEWS FOR REAL-TIME UPDATES
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def appointment_search(request):
|
def appointment_search(request):
|
||||||
"""
|
"""
|
||||||
@ -1370,7 +1338,7 @@ def queue_status(request, queue_id):
|
|||||||
default=Value(None), output_field=IntegerField()
|
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')
|
.order_by('queue_position', 'updated_at')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1434,20 +1402,19 @@ def calendar_appointments(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if provider_id:
|
if provider_id:
|
||||||
queryset = queryset.filter(provider_id=provider_id)
|
queryset = queryset.filter(provider__id=provider_id)
|
||||||
|
|
||||||
appointments = queryset.order_by('scheduled_datetime')
|
appointments = queryset.order_by('scheduled_datetime')
|
||||||
|
# providers = queryset.order_by('provider__first_name')
|
||||||
|
|
||||||
|
|
||||||
return render(request, 'appointments/partials/calendar_appointments.html', {
|
return render(request, 'appointments/partials/calendar_appointments.html', {
|
||||||
'appointments': appointments,
|
'appointments': appointments,
|
||||||
'selected_date': selected_date
|
'selected_date': selected_date,
|
||||||
|
# 'providers': providers
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# ACTION VIEWS FOR WORKFLOW OPERATIONS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def confirm_appointment(request, pk):
|
def confirm_appointment(request, pk):
|
||||||
"""
|
"""
|
||||||
@ -1543,6 +1510,38 @@ def complete_appointment(request, pk):
|
|||||||
return redirect('appointments:appointment_request_detail', pk=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
|
@login_required
|
||||||
def next_in_queue(request, queue_id):
|
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.')
|
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
|
@login_required
|
||||||
@ -1702,18 +1701,17 @@ def end_telemedicine_session(request, pk):
|
|||||||
request=request
|
request=request
|
||||||
)
|
)
|
||||||
|
|
||||||
messages.success(request, f'Telemedicine session ended successfully.')
|
messages.success(request, 'Telemedicine session ended successfully.')
|
||||||
return redirect('appointments:telemedicine_session_detail', pk=pk)
|
return redirect('appointments:telemedicine_session_detail', pk=session.pk)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Missing Views - Placeholder implementations
|
|
||||||
class AppointmentListView(LoginRequiredMixin, ListView):
|
class AppointmentListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List view for appointments.
|
List view for appointments.
|
||||||
"""
|
"""
|
||||||
model = AppointmentRequest
|
model = AppointmentRequest
|
||||||
template_name = 'appointments/appointment_list.html'
|
template_name = 'appointments/requests/appointment_list.html'
|
||||||
context_object_name = 'appointments'
|
context_object_name = 'appointments'
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
|
|
||||||
@ -1728,7 +1726,7 @@ class AppointmentDetailView(LoginRequiredMixin, DetailView):
|
|||||||
Detail view for appointments.
|
Detail view for appointments.
|
||||||
"""
|
"""
|
||||||
model = AppointmentRequest
|
model = AppointmentRequest
|
||||||
template_name = 'appointments/appointment_detail.html'
|
template_name = 'appointments/requests/appointment_request_detail.html'
|
||||||
context_object_name = 'appointment'
|
context_object_name = 'appointment'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -1775,11 +1773,19 @@ class QueueManagementView(LoginRequiredMixin, ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class TelemedicineView(LoginRequiredMixin, TemplateView):
|
class TelemedicineView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
Telemedicine appointments view.
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
@ -1790,6 +1796,38 @@ class TelemedicineView(LoginRequiredMixin, TemplateView):
|
|||||||
return context
|
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):
|
def check_in_patient(request, appointment_id):
|
||||||
"""
|
"""
|
||||||
Check in a patient for their appointment.
|
Check in a patient for their appointment.
|
||||||
@ -1815,13 +1853,13 @@ def call_next_patient(request, queue_id):
|
|||||||
return redirect('appointments:queue_management')
|
return redirect('appointments:queue_management')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
def reschedule_appointment(request, appointment_id):
|
def reschedule_appointment(request, appointment_id):
|
||||||
"""
|
"""
|
||||||
Reschedule an appointment.
|
Reschedule an appointment.
|
||||||
"""
|
"""
|
||||||
appointment = get_object_or_404(AppointmentRequest,
|
appointment = get_object_or_404(AppointmentRequest,
|
||||||
request_id=appointment_id,
|
pk=appointment_id,
|
||||||
tenant=request.user.tenant
|
tenant=request.user.tenant
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1835,8 +1873,8 @@ def reschedule_appointment(request, appointment_id):
|
|||||||
appointment.status = 'RESCHEDULED'
|
appointment.status = 'RESCHEDULED'
|
||||||
appointment.save()
|
appointment.save()
|
||||||
|
|
||||||
messages.success(request, f'Appointment has been rescheduled to {new_date} at {new_time}.')
|
messages.success(request, 'Appointment has been rescheduled')
|
||||||
return redirect('appointments:appointment_detail', pk=appointment_id)
|
return redirect('appointments:appointment_detail', pk=appointment.pk)
|
||||||
|
|
||||||
return render(request, 'appointments/reschedule_appointment.html', {
|
return render(request, 'appointments/reschedule_appointment.html', {
|
||||||
'appointment': appointment
|
'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 billing.utils
|
||||||
import django.db.models.deletion
|
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
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
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/', views.InsuranceClaimListView.as_view(), name='claim_list'),
|
||||||
path('claims/<uuid:claim_id>/', views.InsuranceClaimDetailView.as_view(), name='claim_detail'),
|
path('claims/<uuid:claim_id>/', views.InsuranceClaimDetailView.as_view(), name='claim_detail'),
|
||||||
path('claims/create/', views.InsuranceClaimCreateView.as_view(), name='claim_create'),
|
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('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>/receipt/', views.payment_receipt, name='payment_receipt'),
|
||||||
path('payments/<uuid:payment_id>/email/', views.payment_email, name='payment_email'),
|
path('payments/<uuid:payment_id>/email/', views.payment_email, name='payment_email'),
|
||||||
|
|||||||
641
billing/views.py
641
billing/views.py
@ -33,12 +33,9 @@ from patients.models import PatientProfile, InsuranceInfo
|
|||||||
from accounts.models import User
|
from accounts.models import User
|
||||||
from emr.models import Encounter
|
from emr.models import Encounter
|
||||||
from inpatients.models import Admission
|
from inpatients.models import Admission
|
||||||
|
from core.utils import AuditLogger
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# DASHBOARD VIEW
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class BillingDashboardView(LoginRequiredMixin, TemplateView):
|
class BillingDashboardView(LoginRequiredMixin, TemplateView):
|
||||||
"""
|
"""
|
||||||
Billing dashboard view with comprehensive statistics and recent activity.
|
Billing dashboard view with comprehensive statistics and recent activity.
|
||||||
@ -103,10 +100,6 @@ class BillingDashboardView(LoginRequiredMixin, TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# MEDICAL BILL VIEWS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class MedicalBillListView(LoginRequiredMixin, ListView):
|
class MedicalBillListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List view for medical bills with filtering and search.
|
List view for medical bills with filtering and search.
|
||||||
@ -311,10 +304,6 @@ class MedicalBillDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteV
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# INSURANCE CLAIM VIEWS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class InsuranceClaimListView(LoginRequiredMixin, ListView):
|
class InsuranceClaimListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List view for insurance claims with filtering and search.
|
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})
|
return reverse('billing:claim_detail', kwargs={'claim_id': self.object.claim_id})
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
class InsuranceClaimUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||||||
# PAYMENT VIEWS
|
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):
|
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})
|
return reverse('billing:payment_detail', kwargs={'payment_id': self.object.payment_id})
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# HTMX VIEWS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# @login_required
|
@login_required
|
||||||
# def htmx_billing_stats(request):
|
def htmx_billing_stats(request):
|
||||||
# """
|
"""
|
||||||
# HTMX endpoint for billing statistics.
|
HTMX endpoint for billing statistics.
|
||||||
# """
|
"""
|
||||||
# tenant = getattr(request, 'tenant', None)
|
tenant = getattr(request, 'tenant', None)
|
||||||
# if not tenant:
|
if not tenant:
|
||||||
# return JsonResponse({'error': 'No tenant found'})
|
return JsonResponse({'error': 'No tenant found'})
|
||||||
#
|
|
||||||
# today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
#
|
|
||||||
# # Calculate statistics
|
# Calculate statistics
|
||||||
# bills = MedicalBill.objects.filter(tenant=tenant)
|
bills = MedicalBill.objects.filter(tenant=tenant)
|
||||||
# stats = {
|
stats = {
|
||||||
# 'total_bills': bills.count(),
|
'total_bills': bills.count(),
|
||||||
# 'total_revenue': float(bills.aggregate(
|
'total_revenue': float(bills.aggregate(
|
||||||
# total=Sum('total_amount')
|
total=Sum('total_amount')
|
||||||
# )['total'] or 0),
|
)['total'] or 0),
|
||||||
# 'total_paid': float(bills.aggregate(
|
'total_paid': float(bills.aggregate(
|
||||||
# total=Sum('paid_amount')
|
total=Sum('paid_amount')
|
||||||
# )['total'] or 0),
|
)['total'] or 0),
|
||||||
# 'overdue_bills': bills.filter(
|
'overdue_bills': bills.filter(
|
||||||
# due_date__lt=today,
|
due_date__lt=today,
|
||||||
# status__in=['draft', 'sent', 'partial_payment']
|
status__in=['draft', 'sent', 'partial_payment']
|
||||||
# ).count()
|
).count()
|
||||||
# }
|
}
|
||||||
#
|
|
||||||
# stats['total_outstanding'] = stats['total_revenue'] - stats['total_paid']
|
stats['total_outstanding'] = stats['total_revenue'] - stats['total_paid']
|
||||||
#
|
|
||||||
# return JsonResponse(stats)
|
return JsonResponse(stats)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def billing_stats(request):
|
def billing_stats(request):
|
||||||
@ -715,6 +757,7 @@ def bill_details_api(request, bill_id):
|
|||||||
}
|
}
|
||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def bill_search(request):
|
def bill_search(request):
|
||||||
"""
|
"""
|
||||||
@ -738,10 +781,6 @@ def bill_search(request):
|
|||||||
return render(request, 'billing/partials/bill_list.html', {'bills': bills})
|
return render(request, 'billing/partials/bill_list.html', {'bills': bills})
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# ACTION VIEWS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
def submit_bill(request, bill_id):
|
def submit_bill(request, bill_id):
|
||||||
@ -768,50 +807,65 @@ def submit_bill(request, bill_id):
|
|||||||
return JsonResponse({'success': False, 'error': 'Bill not found'})
|
return JsonResponse({'success': False, 'error': 'Bill not found'})
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# EXPORT VIEWS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def export_bills(request):
|
def export_bills(request):
|
||||||
"""
|
"""
|
||||||
Export bills to CSV.
|
Export medical bills to CSV.
|
||||||
"""
|
"""
|
||||||
tenant = getattr(request, 'tenant', None)
|
tenant = request.user.tenant
|
||||||
if not tenant:
|
|
||||||
return HttpResponse('No tenant found', status=400)
|
# Create HTTP response with CSV content type
|
||||||
|
|
||||||
# Create CSV response
|
|
||||||
response = HttpResponse(content_type='text/csv')
|
response = HttpResponse(content_type='text/csv')
|
||||||
response['Content-Disposition'] = 'attachment; filename="medical_bills.csv"'
|
response['Content-Disposition'] = 'attachment; filename="medical_bills.csv"'
|
||||||
|
|
||||||
writer = csv.writer(response)
|
writer = csv.writer(response)
|
||||||
|
|
||||||
|
# Write header row
|
||||||
writer.writerow([
|
writer.writerow([
|
||||||
'Bill Number', 'Patient Name', 'Bill Date', 'Due Date',
|
'Bill Number', 'Patient Name', 'MRN', 'Bill Type', 'Bill Date', 'Due Date',
|
||||||
'Total Amount', 'Paid Amount', 'Balance', 'Status'
|
'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:
|
for bill in bills:
|
||||||
writer.writerow([
|
writer.writerow([
|
||||||
bill.bill_number,
|
bill.bill_number,
|
||||||
bill.patient.get_full_name(),
|
bill.patient.get_full_name(),
|
||||||
|
bill.patient.mrn,
|
||||||
|
bill.get_bill_type_display(),
|
||||||
bill.bill_date.strftime('%Y-%m-%d'),
|
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.total_amount),
|
||||||
str(bill.paid_amount),
|
str(bill.paid_amount),
|
||||||
str(bill.balance_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
|
return response
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# PRINT VIEWS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def print_bills(request):
|
def print_bills(request):
|
||||||
"""
|
"""
|
||||||
@ -1127,7 +1181,7 @@ def payment_receipt(request, payment_id):
|
|||||||
# Get payment with related objects
|
# Get payment with related objects
|
||||||
payment = Payment.objects.select_related(
|
payment = Payment.objects.select_related(
|
||||||
'medical_bill', 'medical_bill__patient', 'processed_by'
|
'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
|
# Calculate payment details
|
||||||
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.
|
# 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})
|
# return render(request, 'billing/partials/billing_stats.html', {'stats': stats})
|
||||||
#
|
|
||||||
#
|
|
||||||
# @login_required
|
@login_required
|
||||||
# def htmx_bill_search(request):
|
def htmx_bill_search(request):
|
||||||
# """
|
"""
|
||||||
# HTMX view for medical bill search.
|
HTMX view for medical bill search.
|
||||||
# """
|
"""
|
||||||
# tenant = request.user.tenant
|
tenant = request.user.tenant
|
||||||
# search = request.GET.get('search', '')
|
search = request.GET.get('search', '')
|
||||||
#
|
|
||||||
# bills = MedicalBill.objects.filter(tenant=tenant)
|
bills = MedicalBill.objects.filter(tenant=tenant)
|
||||||
#
|
|
||||||
# if search:
|
if search:
|
||||||
# bills = bills.filter(
|
bills = bills.filter(
|
||||||
# Q(bill_number__icontains=search) |
|
Q(bill_number__icontains=search) |
|
||||||
# Q(patient__first_name__icontains=search) |
|
Q(patient__first_name__icontains=search) |
|
||||||
# Q(patient__last_name__icontains=search) |
|
Q(patient__last_name__icontains=search) |
|
||||||
# Q(patient__mrn__icontains=search)
|
Q(patient__mrn__icontains=search)
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# bills = bills.select_related(
|
bills = bills.select_related(
|
||||||
# 'patient', 'encounter', 'attending_provider'
|
'patient', 'encounter', 'attending_provider'
|
||||||
# ).order_by('-bill_date')[:10]
|
).order_by('-bill_date')[:10]
|
||||||
#
|
|
||||||
# return render(request, 'billing/partials/bill_list.html', {'bills': bills})
|
return render(request, 'billing/partials/bill_list.html', {'bills': bills})
|
||||||
#
|
|
||||||
#
|
|
||||||
# @login_required
|
@login_required
|
||||||
# def htmx_payment_search(request):
|
def htmx_payment_search(request):
|
||||||
# """
|
"""
|
||||||
# HTMX view for payment search.
|
HTMX view for payment search.
|
||||||
# """
|
"""
|
||||||
# tenant = request.user.tenant
|
tenant = request.user.tenant
|
||||||
# search = request.GET.get('search', '')
|
search = request.GET.get('search', '')
|
||||||
#
|
|
||||||
# payments = Payment.objects.filter(medical_bill__tenant=tenant)
|
payments = Payment.objects.filter(medical_bill__tenant=tenant)
|
||||||
#
|
|
||||||
# if search:
|
if search:
|
||||||
# payments = payments.filter(
|
payments = payments.filter(
|
||||||
# Q(payment_number__icontains=search) |
|
Q(payment_number__icontains=search) |
|
||||||
# Q(medical_bill__bill_number__icontains=search) |
|
Q(medical_bill__bill_number__icontains=search) |
|
||||||
# Q(medical_bill__patient__first_name__icontains=search) |
|
Q(medical_bill__patient__first_name__icontains=search) |
|
||||||
# Q(medical_bill__patient__last_name__icontains=search)
|
Q(medical_bill__patient__last_name__icontains=search)
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# payments = payments.select_related(
|
payments = payments.select_related(
|
||||||
# 'medical_bill', 'medical_bill__patient'
|
'medical_bill', 'medical_bill__patient'
|
||||||
# ).order_by('-payment_date')[:10]
|
).order_by('-payment_date')[:10]
|
||||||
#
|
|
||||||
# return render(request, 'billing/partials/payment_list.html', {'payments': payments})
|
return render(request, 'billing/partials/payment_list.html', {'payments': payments})
|
||||||
#
|
|
||||||
#
|
|
||||||
# @login_required
|
@login_required
|
||||||
# def htmx_claim_search(request):
|
def htmx_claim_search(request):
|
||||||
# """
|
"""
|
||||||
# HTMX view for insurance claim search.
|
HTMX view for insurance claim search.
|
||||||
# """
|
"""
|
||||||
# tenant = request.user.tenant
|
tenant = request.user.tenant
|
||||||
# search = request.GET.get('search', '')
|
search = request.GET.get('search', '')
|
||||||
#
|
|
||||||
# claims = InsuranceClaim.objects.filter(medical_bill__tenant=tenant)
|
claims = InsuranceClaim.objects.filter(medical_bill__tenant=tenant)
|
||||||
#
|
|
||||||
# if search:
|
if search:
|
||||||
# claims = claims.filter(
|
claims = claims.filter(
|
||||||
# Q(claim_number__icontains=search) |
|
Q(claim_number__icontains=search) |
|
||||||
# Q(medical_bill__bill_number__icontains=search) |
|
Q(medical_bill__bill_number__icontains=search) |
|
||||||
# Q(medical_bill__patient__first_name__icontains=search) |
|
Q(medical_bill__patient__first_name__icontains=search) |
|
||||||
# Q(medical_bill__patient__last_name__icontains=search)
|
Q(medical_bill__patient__last_name__icontains=search)
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# claims = claims.select_related(
|
claims = claims.select_related(
|
||||||
# 'medical_bill', 'medical_bill__patient', 'insurance_info'
|
'medical_bill', 'medical_bill__patient', 'insurance_info'
|
||||||
# ).order_by('-submission_date')[:10]
|
).order_by('-submission_date')[:10]
|
||||||
#
|
|
||||||
# return render(request, 'billing/partials/claim_list.html', {'claims': claims})
|
return render(request, 'billing/partials/claim_list.html', {'claims': claims})
|
||||||
#
|
|
||||||
#
|
|
||||||
# # Action Views
|
# # Action Views
|
||||||
# @login_required
|
# @login_required
|
||||||
# @require_http_methods(["POST"])
|
# @require_http_methods(["POST"])
|
||||||
@ -2146,171 +2215,115 @@ def bill_line_items_api(request, bill_id=None):
|
|||||||
# messages.success(request, 'Medical bill submitted successfully')
|
# messages.success(request, 'Medical bill submitted successfully')
|
||||||
#
|
#
|
||||||
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
# 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
|
@login_required
|
||||||
# @require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
# def process_payment(request, bill_id):
|
def submit_insurance_claim(request, bill_id):
|
||||||
# """
|
"""
|
||||||
# Process payment for medical bill.
|
Submit insurance claim for medical bill.
|
||||||
# """
|
"""
|
||||||
# bill = get_object_or_404(
|
bill = get_object_or_404(
|
||||||
# MedicalBill,
|
MedicalBill,
|
||||||
# bill_id=bill_id,
|
bill_id=bill_id,
|
||||||
# tenant=request.user.tenant
|
tenant=request.user.tenant
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# payment_amount = Decimal(request.POST.get('payment_amount', '0.00'))
|
insurance_type = request.POST.get('insurance_type', 'PRIMARY')
|
||||||
# payment_method = request.POST.get('payment_method', 'CASH')
|
|
||||||
# payment_source = request.POST.get('payment_source', 'PATIENT')
|
# Determine which insurance to use
|
||||||
#
|
if insurance_type == 'PRIMARY' and bill.primary_insurance:
|
||||||
# if payment_amount > 0:
|
insurance_info = bill.primary_insurance
|
||||||
# # Create payment record
|
claim_type = 'PRIMARY'
|
||||||
# payment = Payment.objects.create(
|
elif insurance_type == 'SECONDARY' and bill.secondary_insurance:
|
||||||
# medical_bill=bill,
|
insurance_info = bill.secondary_insurance
|
||||||
# payment_amount=payment_amount,
|
claim_type = 'SECONDARY'
|
||||||
# payment_method=payment_method,
|
else:
|
||||||
# payment_source=payment_source,
|
messages.error(request, 'No insurance information available for claim submission')
|
||||||
# payment_date=timezone.now().date(),
|
return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
||||||
# received_by=request.user,
|
|
||||||
# processed_by=request.user,
|
# Create insurance claim
|
||||||
# status='PROCESSED'
|
claim = InsuranceClaim.objects.create(
|
||||||
# )
|
medical_bill=bill,
|
||||||
#
|
insurance_info=insurance_info,
|
||||||
# # Update bill paid amount and status
|
claim_type=claim_type,
|
||||||
# bill.paid_amount += payment_amount
|
submission_date=timezone.now().date(),
|
||||||
# bill.balance_amount = bill.total_amount - bill.paid_amount
|
service_date_from=bill.service_date_from,
|
||||||
#
|
service_date_to=bill.service_date_to,
|
||||||
# if bill.balance_amount <= 0:
|
billed_amount=bill.total_amount,
|
||||||
# bill.status = 'PAID'
|
status='SUBMITTED',
|
||||||
# elif bill.paid_amount > 0:
|
created_by=request.user
|
||||||
# bill.status = 'PARTIAL_PAID'
|
)
|
||||||
#
|
|
||||||
# bill.save()
|
# Log audit event
|
||||||
#
|
AuditLogger.log_event(
|
||||||
# # Log audit event
|
request.user,
|
||||||
# AuditLogger.log_event(
|
'INSURANCE_CLAIM_SUBMITTED',
|
||||||
# request.user,
|
'InsuranceClaim',
|
||||||
# 'PAYMENT_PROCESSED',
|
str(claim.claim_id),
|
||||||
# 'Payment',
|
f"Submitted {claim_type.lower()} insurance claim {claim.claim_number} for bill {bill.bill_number}"
|
||||||
# str(payment.payment_id),
|
)
|
||||||
# f"Processed payment {payment.payment_number} for ${payment_amount} on 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)
|
||||||
# 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)
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# # Export Views
|
# # 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
|
# # 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 django.utils import timezone
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from patients.models import PatientProfile
|
from patients.models import PatientProfile
|
||||||
from core.models import Department
|
from hr.models import Department
|
||||||
from .models import (
|
from .models import (
|
||||||
BloodGroup, Donor, BloodComponent, BloodUnit, BloodTest, CrossMatch,
|
BloodGroup, Donor, BloodComponent, BloodUnit, BloodTest, CrossMatch,
|
||||||
BloodRequest, BloodIssue, Transfusion, AdverseReaction, InventoryLocation,
|
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 django.db.models.deletion
|
||||||
import uuid
|
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
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
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 django.contrib import admin
|
||||||
from .models import Tenant, AuditLogEntry, SystemConfiguration, SystemNotification, IntegrationLog, Department
|
from .models import Tenant, AuditLogEntry, SystemConfiguration, SystemNotification, IntegrationLog
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Tenant)
|
@admin.register(Tenant)
|
||||||
@ -29,51 +29,51 @@ class TenantAdmin(admin.ModelAdmin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Department)
|
# @admin.register(Department)
|
||||||
class DepartmentAdmin(admin.ModelAdmin):
|
# class DepartmentAdmin(admin.ModelAdmin):
|
||||||
"""Admin interface for Department model"""
|
# """Admin interface for Department model"""
|
||||||
list_display = ['name', 'code', 'department_type', 'department_head', 'is_active', 'tenant']
|
# list_display = ['name', 'code', 'department_type', 'department_head', 'is_active', 'tenant']
|
||||||
list_filter = ['department_type', 'is_active', 'tenant', 'created_at']
|
# list_filter = ['department_type', 'is_active', 'tenant', 'created_at']
|
||||||
search_fields = ['name', 'code', 'description']
|
# search_fields = ['name', 'code', 'description']
|
||||||
readonly_fields = ['department_id', 'created_at', 'updated_at']
|
# readonly_fields = ['department_id', 'created_at', 'updated_at']
|
||||||
autocomplete_fields = ['parent_department', 'department_head', 'created_by']
|
# autocomplete_fields = ['parent_department', 'department_head', 'created_by']
|
||||||
fieldsets = (
|
# fieldsets = (
|
||||||
('Basic Information', {
|
# ('Basic Information', {
|
||||||
'fields': ('tenant', 'code', 'name', 'description', 'department_type')
|
# 'fields': ('tenant', 'code', 'name', 'description', 'department_type')
|
||||||
}),
|
# }),
|
||||||
('Organizational Structure', {
|
# ('Organizational Structure', {
|
||||||
'fields': ('parent_department', 'department_head')
|
# 'fields': ('parent_department', 'department_head')
|
||||||
}),
|
# }),
|
||||||
('Contact Information', {
|
# ('Contact Information', {
|
||||||
'fields': ('phone', 'extension', 'email')
|
# 'fields': ('phone', 'extension', 'email')
|
||||||
}),
|
# }),
|
||||||
('Location', {
|
# ('Location', {
|
||||||
'fields': ('building', 'floor', 'wing', 'room_numbers')
|
# 'fields': ('building', 'floor', 'wing', 'room_numbers')
|
||||||
}),
|
# }),
|
||||||
('Operations', {
|
# ('Operations', {
|
||||||
'fields': ('is_active', 'is_24_hour', 'operating_hours')
|
# 'fields': ('is_active', 'is_24_hour', 'operating_hours')
|
||||||
}),
|
# }),
|
||||||
('Financial', {
|
# ('Financial', {
|
||||||
'fields': ('cost_center_code', 'budget_code')
|
# 'fields': ('cost_center_code', 'budget_code')
|
||||||
}),
|
# }),
|
||||||
('Staffing', {
|
# ('Staffing', {
|
||||||
'fields': ('authorized_positions', 'current_staff_count')
|
# 'fields': ('authorized_positions', 'current_staff_count')
|
||||||
}),
|
# }),
|
||||||
('Quality & Compliance', {
|
# ('Quality & Compliance', {
|
||||||
'fields': ('accreditation_required', 'accreditation_body', 'last_inspection_date', 'next_inspection_date')
|
# 'fields': ('accreditation_required', 'accreditation_body', 'last_inspection_date', 'next_inspection_date')
|
||||||
}),
|
# }),
|
||||||
('Metadata', {
|
# ('Metadata', {
|
||||||
'fields': ('department_id', 'created_by', 'created_at', 'updated_at'),
|
# 'fields': ('department_id', 'created_by', 'created_at', 'updated_at'),
|
||||||
'classes': ('collapse',)
|
# 'classes': ('collapse',)
|
||||||
})
|
# })
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
def get_queryset(self, request):
|
# def get_queryset(self, request):
|
||||||
"""Filter by tenant if user has tenant"""
|
# """Filter by tenant if user has tenant"""
|
||||||
qs = super().get_queryset(request)
|
# qs = super().get_queryset(request)
|
||||||
if hasattr(request.user, 'tenant') and request.user.tenant:
|
# if hasattr(request.user, 'tenant') and request.user.tenant:
|
||||||
qs = qs.filter(tenant=request.user.tenant)
|
# qs = qs.filter(tenant=request.user.tenant)
|
||||||
return qs
|
# return qs
|
||||||
|
|
||||||
|
|
||||||
@admin.register(AuditLogEntry)
|
@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 import forms
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from .models import Tenant, SystemConfiguration, SystemNotification, Department
|
from .models import Tenant, SystemConfiguration, SystemNotification
|
||||||
|
from accounts.models import User
|
||||||
User = get_user_model()
|
from hr.models import Department
|
||||||
|
|
||||||
|
|
||||||
class TenantForm(forms.ModelForm):
|
class TenantForm(forms.ModelForm):
|
||||||
@ -227,106 +227,106 @@ class SystemNotificationForm(forms.ModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DepartmentForm(forms.ModelForm):
|
# class DepartmentForm(forms.ModelForm):
|
||||||
"""Form for creating and managing hospital departments."""
|
# """Form for creating and managing hospital departments."""
|
||||||
|
#
|
||||||
class Meta:
|
# class Meta:
|
||||||
model = Department
|
# model = Department
|
||||||
fields = [
|
# fields = [
|
||||||
'name', 'code', 'description', 'department_type',
|
# 'name', 'code', 'description', 'department_type',
|
||||||
'department_head', 'parent_department', 'building', 'floor',
|
# 'department_head', 'parent_department', 'building', 'floor',
|
||||||
'wing', 'room_numbers', 'phone', 'extension',
|
# 'wing', 'room_numbers', 'phone', 'extension',
|
||||||
'email', 'is_active', 'is_24_hour'
|
# 'email', 'is_active', 'is_24_hour'
|
||||||
]
|
# ]
|
||||||
widgets = {
|
# widgets = {
|
||||||
'name': forms.TextInput(attrs={
|
# 'name': forms.TextInput(attrs={
|
||||||
'class': 'form-control',
|
# 'class': 'form-control',
|
||||||
'placeholder': 'Enter department name'
|
# 'placeholder': 'Enter department name'
|
||||||
}),
|
# }),
|
||||||
'code': forms.TextInput(attrs={
|
# 'code': forms.TextInput(attrs={
|
||||||
'class': 'form-control',
|
# 'class': 'form-control',
|
||||||
'placeholder': 'Enter department code'
|
# 'placeholder': 'Enter department code'
|
||||||
}),
|
# }),
|
||||||
'description': forms.Textarea(attrs={
|
# 'description': forms.Textarea(attrs={
|
||||||
'class': 'form-control',
|
# 'class': 'form-control',
|
||||||
'rows': 3,
|
# 'rows': 3,
|
||||||
'placeholder': 'Enter department description'
|
# 'placeholder': 'Enter department description'
|
||||||
}),
|
# }),
|
||||||
'department_type': forms.Select(attrs={
|
# 'department_type': forms.Select(attrs={
|
||||||
'class': 'form-select'
|
# 'class': 'form-select'
|
||||||
}),
|
# }),
|
||||||
'department_head': forms.Select(attrs={
|
# 'department_head': forms.Select(attrs={
|
||||||
'class': 'form-select'
|
# 'class': 'form-select'
|
||||||
}),
|
# }),
|
||||||
'parent_department': forms.Select(attrs={
|
# 'parent_department': forms.Select(attrs={
|
||||||
'class': 'form-select'
|
# 'class': 'form-select'
|
||||||
}),
|
# }),
|
||||||
'building': forms.TextInput(attrs={
|
# 'building': forms.TextInput(attrs={
|
||||||
'class': 'form-control',
|
# 'class': 'form-control',
|
||||||
'placeholder': 'Enter building name/number'
|
# 'placeholder': 'Enter building name/number'
|
||||||
}),
|
# }),
|
||||||
'floor': forms.TextInput(attrs={
|
# 'floor': forms.TextInput(attrs={
|
||||||
'class': 'form-control',
|
# 'class': 'form-control',
|
||||||
'placeholder': 'Enter floor'
|
# 'placeholder': 'Enter floor'
|
||||||
}),
|
# }),
|
||||||
'wing': forms.TextInput(attrs={
|
# 'wing': forms.TextInput(attrs={
|
||||||
'class': 'form-control',
|
# 'class': 'form-control',
|
||||||
'placeholder': 'Enter wing/section'
|
# 'placeholder': 'Enter wing/section'
|
||||||
}),
|
# }),
|
||||||
'room_numbers': forms.TextInput(attrs={
|
# 'room_numbers': forms.TextInput(attrs={
|
||||||
'class': 'form-control',
|
# 'class': 'form-control',
|
||||||
'placeholder': 'Enter room numbers'
|
# 'placeholder': 'Enter room numbers'
|
||||||
}),
|
# }),
|
||||||
'phone': forms.TextInput(attrs={
|
# 'phone': forms.TextInput(attrs={
|
||||||
'class': 'form-control',
|
# 'class': 'form-control',
|
||||||
'placeholder': 'Enter phone number'
|
# 'placeholder': 'Enter phone number'
|
||||||
}),
|
# }),
|
||||||
'extension': forms.TextInput(attrs={
|
# 'extension': forms.TextInput(attrs={
|
||||||
'class': 'form-control',
|
# 'class': 'form-control',
|
||||||
'placeholder': 'Enter extension'
|
# 'placeholder': 'Enter extension'
|
||||||
}),
|
# }),
|
||||||
'email': forms.EmailInput(attrs={
|
# 'email': forms.EmailInput(attrs={
|
||||||
'class': 'form-control',
|
# 'class': 'form-control',
|
||||||
'placeholder': 'Enter email address'
|
# 'placeholder': 'Enter email address'
|
||||||
}),
|
# }),
|
||||||
'is_active': forms.CheckboxInput(attrs={
|
# 'is_active': forms.CheckboxInput(attrs={
|
||||||
'class': 'form-check-input'
|
# 'class': 'form-check-input'
|
||||||
}),
|
# }),
|
||||||
'is_24_hour': forms.CheckboxInput(attrs={
|
# 'is_24_hour': forms.CheckboxInput(attrs={
|
||||||
'class': 'form-check-input'
|
# 'class': 'form-check-input'
|
||||||
})
|
# })
|
||||||
}
|
# }
|
||||||
|
#
|
||||||
def __init__(self, *args, **kwargs):
|
# def __init__(self, *args, **kwargs):
|
||||||
self.tenant = kwargs.pop('tenant', None)
|
# self.tenant = kwargs.pop('tenant', None)
|
||||||
super().__init__(*args, **kwargs)
|
# super().__init__(*args, **kwargs)
|
||||||
|
#
|
||||||
if self.tenant:
|
# if self.tenant:
|
||||||
# Filter department head by tenant and medical staff
|
# # Filter department head by tenant and medical staff
|
||||||
self.fields['department_head'].queryset = User.objects.filter(
|
# self.fields['department_head'].queryset = User.objects.filter(
|
||||||
tenant=self.tenant,
|
# tenant=self.tenant,
|
||||||
is_active=True,
|
# is_active=True,
|
||||||
role__in=['DOCTOR', 'NURSE_MANAGER', 'ADMINISTRATOR']
|
# role__in=['DOCTOR', 'NURSE_MANAGER', 'ADMINISTRATOR']
|
||||||
)
|
# )
|
||||||
# Filter parent department by tenant
|
# # Filter parent department by tenant
|
||||||
self.fields['parent_department'].queryset = Department.objects.filter(
|
# self.fields['parent_department'].queryset = Department.objects.filter(
|
||||||
tenant=self.tenant,
|
# tenant=self.tenant,
|
||||||
is_active=True
|
# is_active=True
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
def clean_code(self):
|
# def clean_code(self):
|
||||||
"""Validate department code uniqueness within tenant."""
|
# """Validate department code uniqueness within tenant."""
|
||||||
code = self.cleaned_data.get('code')
|
# code = self.cleaned_data.get('code')
|
||||||
if code and self.tenant:
|
# if code and self.tenant:
|
||||||
queryset = Department.objects.filter(
|
# queryset = Department.objects.filter(
|
||||||
tenant=self.tenant,
|
# tenant=self.tenant,
|
||||||
code=code
|
# code=code
|
||||||
)
|
# )
|
||||||
if self.instance.pk:
|
# if self.instance.pk:
|
||||||
queryset = queryset.exclude(pk=self.instance.pk)
|
# queryset = queryset.exclude(pk=self.instance.pk)
|
||||||
if queryset.exists():
|
# if queryset.exists():
|
||||||
raise forms.ValidationError('A department with this code already exists.')
|
# raise forms.ValidationError('A department with this code already exists.')
|
||||||
return code
|
# return code
|
||||||
|
|
||||||
|
|
||||||
class CoreSearchForm(forms.Form):
|
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.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -97,7 +97,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"country",
|
"country",
|
||||||
models.CharField(
|
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",
|
"currency",
|
||||||
models.CharField(
|
models.CharField(
|
||||||
default="USD",
|
default="SAR",
|
||||||
help_text="Organization currency code",
|
help_text="Organization currency code",
|
||||||
max_length=3,
|
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(
|
migrations.CreateModel(
|
||||||
name="AuditLogEntry",
|
name="AuditLogEntry",
|
||||||
fields=[
|
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):
|
# class Department(models.Model):
|
||||||
"""
|
# """
|
||||||
Hospital department model for organizational structure.
|
# Hospital department model for organizational structure.
|
||||||
Represents different departments within a healthcare organization.
|
# Represents different departments within a healthcare organization.
|
||||||
"""
|
# """
|
||||||
|
#
|
||||||
DEPARTMENT_TYPE_CHOICES = [
|
# DEPARTMENT_TYPE_CHOICES = [
|
||||||
('CLINICAL', 'Clinical Department'),
|
# ('CLINICAL', 'Clinical Department'),
|
||||||
('ANCILLARY', 'Ancillary Services'),
|
# ('ANCILLARY', 'Ancillary Services'),
|
||||||
('SUPPORT', 'Support Services'),
|
# ('SUPPORT', 'Support Services'),
|
||||||
('ADMINISTRATIVE', 'Administrative'),
|
# ('ADMINISTRATIVE', 'Administrative'),
|
||||||
('DIAGNOSTIC', 'Diagnostic Services'),
|
# ('DIAGNOSTIC', 'Diagnostic Services'),
|
||||||
('THERAPEUTIC', 'Therapeutic Services'),
|
# ('THERAPEUTIC', 'Therapeutic Services'),
|
||||||
('EMERGENCY', 'Emergency Services'),
|
# ('EMERGENCY', 'Emergency Services'),
|
||||||
('SURGICAL', 'Surgical Services'),
|
# ('SURGICAL', 'Surgical Services'),
|
||||||
('MEDICAL', 'Medical Services'),
|
# ('MEDICAL', 'Medical Services'),
|
||||||
('NURSING', 'Nursing Services'),
|
# ('NURSING', 'Nursing Services'),
|
||||||
('PHARMACY', 'Pharmacy'),
|
# ('PHARMACY', 'Pharmacy'),
|
||||||
('LABORATORY', 'Laboratory'),
|
# ('LABORATORY', 'Laboratory'),
|
||||||
('RADIOLOGY', 'Radiology'),
|
# ('RADIOLOGY', 'Radiology'),
|
||||||
('REHABILITATION', 'Rehabilitation'),
|
# ('REHABILITATION', 'Rehabilitation'),
|
||||||
('MENTAL_HEALTH', 'Mental Health'),
|
# ('MENTAL_HEALTH', 'Mental Health'),
|
||||||
('PEDIATRIC', 'Pediatric'),
|
# ('PEDIATRIC', 'Pediatric'),
|
||||||
('OBSTETRIC', 'Obstetric'),
|
# ('OBSTETRIC', 'Obstetric'),
|
||||||
('ONCOLOGY', 'Oncology'),
|
# ('ONCOLOGY', 'Oncology'),
|
||||||
('CARDIOLOGY', 'Cardiology'),
|
# ('CARDIOLOGY', 'Cardiology'),
|
||||||
('NEUROLOGY', 'Neurology'),
|
# ('NEUROLOGY', 'Neurology'),
|
||||||
('ORTHOPEDIC', 'Orthopedic'),
|
# ('ORTHOPEDIC', 'Orthopedic'),
|
||||||
('OTHER', 'Other'),
|
# ('OTHER', 'Other'),
|
||||||
]
|
# ]
|
||||||
|
#
|
||||||
# Tenant relationship
|
# # Tenant relationship
|
||||||
tenant = models.ForeignKey(
|
# tenant = models.ForeignKey(
|
||||||
Tenant,
|
# Tenant,
|
||||||
on_delete=models.CASCADE,
|
# on_delete=models.CASCADE,
|
||||||
related_name='core_departments',
|
# related_name='core_departments',
|
||||||
help_text='Organization tenant'
|
# help_text='Organization tenant'
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
# Department Information
|
# # Department Information
|
||||||
department_id = models.UUIDField(
|
# department_id = models.UUIDField(
|
||||||
default=uuid.uuid4,
|
# default=uuid.uuid4,
|
||||||
unique=True,
|
# unique=True,
|
||||||
editable=False,
|
# editable=False,
|
||||||
help_text='Unique department identifier'
|
# help_text='Unique department identifier'
|
||||||
)
|
# )
|
||||||
code = models.CharField(
|
# code = models.CharField(
|
||||||
max_length=20,
|
# max_length=20,
|
||||||
help_text='Department code (e.g., CARD, EMER, SURG)'
|
# help_text='Department code (e.g., CARD, EMER, SURG)'
|
||||||
)
|
# )
|
||||||
name = models.CharField(
|
# name = models.CharField(
|
||||||
max_length=100,
|
# max_length=100,
|
||||||
help_text='Department name'
|
# help_text='Department name'
|
||||||
)
|
# )
|
||||||
description = models.TextField(
|
# description = models.TextField(
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Department description'
|
# help_text='Department description'
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
# Department Classification
|
# # Department Classification
|
||||||
department_type = models.CharField(
|
# department_type = models.CharField(
|
||||||
max_length=30,
|
# max_length=30,
|
||||||
choices=DEPARTMENT_TYPE_CHOICES,
|
# choices=DEPARTMENT_TYPE_CHOICES,
|
||||||
help_text='Type of department'
|
# help_text='Type of department'
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
# Organizational Structure
|
# # Organizational Structure
|
||||||
parent_department = models.ForeignKey(
|
# parent_department = models.ForeignKey(
|
||||||
'self',
|
# 'self',
|
||||||
on_delete=models.SET_NULL,
|
# on_delete=models.SET_NULL,
|
||||||
null=True,
|
# null=True,
|
||||||
blank=True,
|
# blank=True,
|
||||||
related_name='sub_departments',
|
# related_name='sub_departments',
|
||||||
help_text='Parent department (for hierarchical structure)'
|
# help_text='Parent department (for hierarchical structure)'
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
# Management
|
# # Management
|
||||||
department_head = models.ForeignKey(
|
# department_head = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
# settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.SET_NULL,
|
# on_delete=models.SET_NULL,
|
||||||
null=True,
|
# null=True,
|
||||||
blank=True,
|
# blank=True,
|
||||||
related_name='headed_departments',
|
# related_name='headed_departments',
|
||||||
help_text='Department head/manager'
|
# help_text='Department head/manager'
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
# Contact Information
|
# # Contact Information
|
||||||
phone = models.CharField(
|
# phone = models.CharField(
|
||||||
max_length=20,
|
# max_length=20,
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Department phone number'
|
# help_text='Department phone number'
|
||||||
)
|
# )
|
||||||
extension = models.CharField(
|
# extension = models.CharField(
|
||||||
max_length=10,
|
# max_length=10,
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Phone extension'
|
# help_text='Phone extension'
|
||||||
)
|
# )
|
||||||
email = models.EmailField(
|
# email = models.EmailField(
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Department email'
|
# help_text='Department email'
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
# Location
|
# # Location
|
||||||
building = models.CharField(
|
# building = models.CharField(
|
||||||
max_length=50,
|
# max_length=50,
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Building name or number'
|
# help_text='Building name or number'
|
||||||
)
|
# )
|
||||||
floor = models.CharField(
|
# floor = models.CharField(
|
||||||
max_length=20,
|
# max_length=20,
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Floor number or name'
|
# help_text='Floor number or name'
|
||||||
)
|
# )
|
||||||
wing = models.CharField(
|
# wing = models.CharField(
|
||||||
max_length=20,
|
# max_length=20,
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Wing or section'
|
# help_text='Wing or section'
|
||||||
)
|
# )
|
||||||
room_numbers = models.CharField(
|
# room_numbers = models.CharField(
|
||||||
max_length=100,
|
# max_length=100,
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Room numbers (e.g., 101-110, 201A-205C)'
|
# help_text='Room numbers (e.g., 101-110, 201A-205C)'
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
# Operational Information
|
# # Operational Information
|
||||||
is_active = models.BooleanField(
|
# is_active = models.BooleanField(
|
||||||
default=True,
|
# default=True,
|
||||||
help_text='Department is active'
|
# help_text='Department is active'
|
||||||
)
|
# )
|
||||||
is_24_hour = models.BooleanField(
|
# is_24_hour = models.BooleanField(
|
||||||
default=False,
|
# default=False,
|
||||||
help_text='Department operates 24 hours'
|
# help_text='Department operates 24 hours'
|
||||||
)
|
# )
|
||||||
operating_hours = models.JSONField(
|
# operating_hours = models.JSONField(
|
||||||
default=dict,
|
# default=dict,
|
||||||
blank=True,
|
# blank=True,
|
||||||
help_text='Operating hours by day of week'
|
# help_text='Operating hours by day of week'
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
# Budget and Cost Center
|
# # Budget and Cost Center
|
||||||
cost_center_code = models.CharField(
|
# cost_center_code = models.CharField(
|
||||||
max_length=20,
|
# max_length=20,
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Cost center code for financial tracking'
|
# help_text='Cost center code for financial tracking'
|
||||||
)
|
# )
|
||||||
budget_code = models.CharField(
|
# budget_code = models.CharField(
|
||||||
max_length=20,
|
# max_length=20,
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Budget code'
|
# help_text='Budget code'
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
# Staffing
|
# # Staffing
|
||||||
authorized_positions = models.PositiveIntegerField(
|
# authorized_positions = models.PositiveIntegerField(
|
||||||
default=0,
|
# default=0,
|
||||||
help_text='Number of authorized positions'
|
# help_text='Number of authorized positions'
|
||||||
)
|
# )
|
||||||
current_staff_count = models.PositiveIntegerField(
|
# current_staff_count = models.PositiveIntegerField(
|
||||||
default=0,
|
# default=0,
|
||||||
help_text='Current number of staff members'
|
# help_text='Current number of staff members'
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
# Quality and Compliance
|
# # Quality and Compliance
|
||||||
accreditation_required = models.BooleanField(
|
# accreditation_required = models.BooleanField(
|
||||||
default=False,
|
# default=False,
|
||||||
help_text='Department requires special accreditation'
|
# help_text='Department requires special accreditation'
|
||||||
)
|
# )
|
||||||
accreditation_body = models.CharField(
|
# accreditation_body = models.CharField(
|
||||||
max_length=100,
|
# max_length=100,
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Accrediting body (e.g., Joint Commission, CAP)'
|
# help_text='Accrediting body (e.g., Joint Commission, CAP)'
|
||||||
)
|
# )
|
||||||
last_inspection_date = models.DateField(
|
# last_inspection_date = models.DateField(
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Last inspection date'
|
# help_text='Last inspection date'
|
||||||
)
|
# )
|
||||||
next_inspection_date = models.DateField(
|
# next_inspection_date = models.DateField(
|
||||||
blank=True,
|
# blank=True,
|
||||||
null=True,
|
# null=True,
|
||||||
help_text='Next scheduled inspection date'
|
# help_text='Next scheduled inspection date'
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
# Metadata
|
# # Metadata
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
# created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
# updated_at = models.DateTimeField(auto_now=True)
|
||||||
created_by = models.ForeignKey(
|
# created_by = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
# settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.SET_NULL,
|
# on_delete=models.SET_NULL,
|
||||||
null=True,
|
# null=True,
|
||||||
blank=True,
|
# blank=True,
|
||||||
related_name='created_departments',
|
# related_name='created_departments',
|
||||||
help_text='User who created the department'
|
# help_text='User who created the department'
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
class Meta:
|
# class Meta:
|
||||||
db_table = 'core_department'
|
# db_table = 'core_department'
|
||||||
verbose_name = 'Department'
|
# verbose_name = 'Department'
|
||||||
verbose_name_plural = 'Departments'
|
# verbose_name_plural = 'Departments'
|
||||||
ordering = ['name']
|
# ordering = ['name']
|
||||||
indexes = [
|
# indexes = [
|
||||||
models.Index(fields=['tenant', 'department_type']),
|
# models.Index(fields=['tenant', 'department_type']),
|
||||||
models.Index(fields=['code']),
|
# models.Index(fields=['code']),
|
||||||
models.Index(fields=['is_active']),
|
# models.Index(fields=['is_active']),
|
||||||
models.Index(fields=['parent_department']),
|
# models.Index(fields=['parent_department']),
|
||||||
]
|
# ]
|
||||||
unique_together = ['tenant', 'code']
|
# unique_together = ['tenant', 'code']
|
||||||
|
#
|
||||||
def __str__(self):
|
# def __str__(self):
|
||||||
return f"{self.name} ({self.code})"
|
# return f"{self.name} ({self.code})"
|
||||||
|
#
|
||||||
@property
|
# @property
|
||||||
def full_name(self):
|
# def full_name(self):
|
||||||
"""Return full department name with parent if applicable"""
|
# """Return full department name with parent if applicable"""
|
||||||
if self.parent_department:
|
# if self.parent_department:
|
||||||
return f"{self.parent_department.name} - {self.name}"
|
# return f"{self.parent_department.name} - {self.name}"
|
||||||
return self.name
|
# return self.name
|
||||||
|
#
|
||||||
@property
|
# @property
|
||||||
def staffing_percentage(self):
|
# def staffing_percentage(self):
|
||||||
"""Calculate current staffing percentage"""
|
# """Calculate current staffing percentage"""
|
||||||
if self.authorized_positions > 0:
|
# if self.authorized_positions > 0:
|
||||||
return (self.current_staff_count / self.authorized_positions) * 100
|
# return (self.current_staff_count / self.authorized_positions) * 100
|
||||||
return 0
|
# return 0
|
||||||
|
#
|
||||||
def get_all_sub_departments(self):
|
# def get_all_sub_departments(self):
|
||||||
"""Get all sub-departments recursively"""
|
# """Get all sub-departments recursively"""
|
||||||
sub_departments = []
|
# sub_departments = []
|
||||||
for sub_dept in self.sub_departments.all():
|
# for sub_dept in self.sub_departments.all():
|
||||||
sub_departments.append(sub_dept)
|
# sub_departments.append(sub_dept)
|
||||||
sub_departments.extend(sub_dept.get_all_sub_departments())
|
# sub_departments.extend(sub_dept.get_all_sub_departments())
|
||||||
return 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'),
|
path('tenants/<int:pk>/deactivate/', views.deactivate_tenant, name='deactivate_tenant'),
|
||||||
|
|
||||||
# Department CRUD URLs
|
# Department CRUD URLs
|
||||||
path('departments/', views.DepartmentListView.as_view(), name='department_list'),
|
# path('departments/', views.DepartmentListView.as_view(), name='department_list'),
|
||||||
path('departments/create/', views.DepartmentCreateView.as_view(), name='department_create'),
|
# 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>/', views.DepartmentDetailView.as_view(), name='department_detail'),
|
||||||
path('departments/<int:pk>/edit/', views.DepartmentUpdateView.as_view(), name='department_update'),
|
# 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>/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'),
|
|
||||||
|
|
||||||
# System Configuration CRUD URLs
|
# System Configuration CRUD URLs
|
||||||
path('system-configuration/create/', views.SystemConfigurationCreateView.as_view(), name='system_configuration_create'),
|
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>/', 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>/edit/', views.SystemConfigurationUpdateView.as_view(), name='system_configuration_update'),
|
||||||
path('system-configuration/<int:pk>/delete/', views.SystemConfigurationDeleteView.as_view(), name='system_configuration_delete'),
|
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
|
# System Notification CRUD URLs
|
||||||
path('notifications/', views.SystemNotificationListView.as_view(), name='system_notification_list'),
|
path('notifications/', views.SystemNotificationListView.as_view(), name='system_notification_list'),
|
||||||
@ -79,14 +74,12 @@ urlpatterns = [
|
|||||||
# Search and Filter URLs
|
# Search and Filter URLs
|
||||||
path('search/', views.CoreSearchView.as_view(), name='search'),
|
path('search/', views.CoreSearchView.as_view(), name='search'),
|
||||||
path('search/tenants/', views.tenant_search, name='tenant_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'),
|
path('search/audit-logs/', views.audit_log_search, name='audit_log_search'),
|
||||||
|
|
||||||
# Bulk Operations
|
# Bulk Operations
|
||||||
path('tenants/bulk-activate/', views.bulk_activate_tenants, name='bulk_activate_tenants'),
|
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('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'),
|
path('audit-log/bulk-export/', views.bulk_export_audit_logs, name='bulk_export_audit_logs'),
|
||||||
|
|
||||||
# API-like endpoints for AJAX
|
# 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.utils import timezone
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.urls import reverse_lazy, reverse
|
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 datetime import timedelta
|
||||||
from .models import (
|
from .models import (
|
||||||
Tenant, AuditLogEntry, SystemConfiguration, SystemNotification,
|
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
|
# Create aliases for models to match the views
|
||||||
AuditLog = AuditLogEntry
|
AuditLog = AuditLogEntry
|
||||||
User = get_user_model()
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
TenantForm, SystemConfigurationForm, SystemNotificationForm,
|
TenantForm, SystemConfigurationForm, SystemNotificationForm,CoreSearchForm
|
||||||
DepartmentForm, CoreSearchForm
|
|
||||||
)
|
)
|
||||||
from .utils import AuditLogger
|
from .utils import AuditLogger
|
||||||
|
|
||||||
@ -66,10 +67,6 @@ class DashboardView(LoginRequiredMixin, TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# TENANT VIEWS (FULL CRUD - Master Data)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class TenantListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
class TenantListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List all tenants (Super admin only).
|
List all tenants (Super admin only).
|
||||||
@ -226,10 +223,6 @@ class TenantDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
|||||||
return redirect(self.success_url)
|
return redirect(self.success_url)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# AUDIT LOG VIEWS (READ-ONLY - System Generated)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class AuditLogListView(LoginRequiredMixin, ListView):
|
class AuditLogListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
Audit log listing view.
|
Audit log listing view.
|
||||||
@ -302,10 +295,6 @@ class AuditLogDetailView(LoginRequiredMixin, DetailView):
|
|||||||
return AuditLogEntry.objects.filter(tenant=tenant)
|
return AuditLogEntry.objects.filter(tenant=tenant)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# SYSTEM CONFIGURATION VIEWS (FULL CRUD - Master Data)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class SystemConfigurationListView(LoginRequiredMixin, ListView):
|
class SystemConfigurationListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
System configuration view.
|
System configuration view.
|
||||||
@ -476,10 +465,6 @@ class SystemConfigurationDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
return super().delete(request, *args, **kwargs)
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# SYSTEM NOTIFICATION VIEWS (FULL CRUD - Operational Data)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class SystemNotificationListView(LoginRequiredMixin, ListView):
|
class SystemNotificationListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List system notifications.
|
List system notifications.
|
||||||
@ -637,10 +622,6 @@ class SystemNotificationDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
return super().delete(request, *args, **kwargs)
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# INTEGRATION LOG VIEWS (READ-ONLY - System Generated)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class IntegrationLogListView(LoginRequiredMixin, ListView):
|
class IntegrationLogListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List integration logs.
|
List integration logs.
|
||||||
@ -708,189 +689,6 @@ class IntegrationLogDetailView(LoginRequiredMixin, DetailView):
|
|||||||
return IntegrationLog.objects.filter(tenant=tenant)
|
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
|
@login_required
|
||||||
def dashboard_stats(request):
|
def dashboard_stats(request):
|
||||||
"""
|
"""
|
||||||
@ -1064,10 +862,6 @@ def system_health(request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# ACTION VIEWS FOR WORKFLOW OPERATIONS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def activate_notification(request, pk):
|
def activate_notification(request, pk):
|
||||||
"""
|
"""
|
||||||
@ -1175,9 +969,6 @@ def reset_configuration(request, pk):
|
|||||||
return redirect('core:system_configuration_detail', pk=pk)
|
return redirect('core:system_configuration_detail', pk=pk)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Missing HTMX Views
|
|
||||||
def tenant_stats(request):
|
def tenant_stats(request):
|
||||||
"""
|
"""
|
||||||
HTMX view for tenant statistics.
|
HTMX view for tenant statistics.
|
||||||
@ -1194,20 +985,6 @@ def tenant_stats(request):
|
|||||||
return render(request, 'core/partials/tenant_stats.html', {'stats': stats})
|
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):
|
def configuration_search(request):
|
||||||
"""
|
"""
|
||||||
HTMX view for configuration search.
|
HTMX view for configuration search.
|
||||||
@ -1240,7 +1017,6 @@ def audit_log_list_htmx(request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# Missing Action Views
|
|
||||||
def activate_tenant(request, pk):
|
def activate_tenant(request, pk):
|
||||||
"""
|
"""
|
||||||
Activate a tenant.
|
Activate a tenant.
|
||||||
@ -1265,30 +1041,6 @@ def deactivate_tenant(request, pk):
|
|||||||
return redirect('core:tenant_detail', pk=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):
|
def reset_system_configuration(request):
|
||||||
"""
|
"""
|
||||||
Reset system configuration to defaults.
|
Reset system configuration to defaults.
|
||||||
@ -1302,7 +1054,7 @@ def reset_system_configuration(request):
|
|||||||
messages.success(request, 'System configuration has been reset to defaults.')
|
messages.success(request, 'System configuration has been reset to defaults.')
|
||||||
return redirect('core:system_configuration_list')
|
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):
|
def export_audit_log(request):
|
||||||
@ -1335,7 +1087,6 @@ def export_audit_log(request):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
# Missing Search Views
|
|
||||||
class CoreSearchView(ListView):
|
class CoreSearchView(ListView):
|
||||||
"""
|
"""
|
||||||
Generic search view for core models.
|
Generic search view for core models.
|
||||||
@ -1396,23 +1147,6 @@ def tenant_search(request):
|
|||||||
return JsonResponse({'tenants': list(tenants)})
|
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):
|
def bulk_activate_tenants(request):
|
||||||
"""
|
"""
|
||||||
Bulk activate tenants.
|
Bulk activate tenants.
|
||||||
@ -1443,38 +1177,6 @@ def bulk_deactivate_tenants(request):
|
|||||||
return redirect('core:tenant_list')
|
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):
|
def bulk_export_audit_logs(request):
|
||||||
"""
|
"""
|
||||||
Bulk export audit logs.
|
Bulk export audit logs.
|
||||||
@ -1511,7 +1213,6 @@ def bulk_export_audit_logs(request):
|
|||||||
return redirect('core:audit_log_list')
|
return redirect('core:audit_log_list')
|
||||||
|
|
||||||
|
|
||||||
# Missing API Views
|
|
||||||
def validate_tenant_data(request):
|
def validate_tenant_data(request):
|
||||||
"""
|
"""
|
||||||
AJAX validation for tenant data.
|
AJAX validation for tenant data.
|
||||||
@ -1528,31 +1229,6 @@ def validate_tenant_data(request):
|
|||||||
return JsonResponse({'valid': len(errors) == 0, 'errors': errors})
|
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):
|
def get_system_status(request):
|
||||||
"""
|
"""
|
||||||
Get system status information.
|
Get system status information.
|
||||||
@ -1604,8 +1280,6 @@ def backup_configuration(request):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def restore_configuration(request):
|
def restore_configuration(request):
|
||||||
"""
|
"""
|
||||||
Restore system configuration from backup.
|
Restore system configuration from backup.
|
||||||
@ -1644,111 +1318,184 @@ def restore_configuration(request):
|
|||||||
return render(request, 'core/restore_configuration.html')
|
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
|
# import json
|
||||||
#
|
#
|
||||||
|
|||||||
129
core_data.py
129
core_data.py
@ -7,13 +7,12 @@ django.setup()
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
from datetime import datetime, timedelta, timezone
|
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 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 uuid
|
||||||
import json
|
import json
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
# Saudi-specific data constants
|
# Saudi-specific data constants
|
||||||
SAUDI_CITIES = [
|
SAUDI_CITIES = [
|
||||||
@ -119,64 +118,64 @@ def create_super_user():
|
|||||||
tenant=tenant1 # assumes your User model has a ForeignKey to Tenant named `tenant`
|
tenant=tenant1 # assumes your User model has a ForeignKey to Tenant named `tenant`
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_saudi_departments(tenants, departments_per_tenant=15):
|
# def create_saudi_departments(tenants, departments_per_tenant=15):
|
||||||
"""Create Saudi healthcare departments"""
|
# """Create Saudi healthcare departments"""
|
||||||
departments = []
|
# departments = []
|
||||||
|
#
|
||||||
department_types = [
|
# department_types = [
|
||||||
('clinical', 'Clinical Department'),
|
# ('clinical', 'Clinical Department'),
|
||||||
('support', 'Support Department'),
|
# ('support', 'Support Department'),
|
||||||
('administrative', 'Administrative Department'),
|
# ('administrative', 'Administrative Department'),
|
||||||
('diagnostic', 'Diagnostic Department')
|
# ('diagnostic', 'Diagnostic Department')
|
||||||
]
|
# ]
|
||||||
|
#
|
||||||
for tenant in tenants:
|
# for tenant in tenants:
|
||||||
# Create main departments
|
# # Create main departments
|
||||||
for specialty in SAUDI_MEDICAL_SPECIALTIES[:departments_per_tenant]:
|
# for specialty in SAUDI_MEDICAL_SPECIALTIES[:departments_per_tenant]:
|
||||||
dept_type = random.choice(department_types)
|
# dept_type = random.choice(department_types)
|
||||||
|
#
|
||||||
department = Department.objects.create(
|
# department = Department.objects.create(
|
||||||
tenant=tenant,
|
# tenant=tenant,
|
||||||
department_id=uuid.uuid4(),
|
# department_id=uuid.uuid4(),
|
||||||
code=specialty.replace(' ', '').upper()[:10],
|
# code=specialty.replace(' ', '').upper()[:10],
|
||||||
name=f"Department of {specialty}",
|
# name=f"Department of {specialty}",
|
||||||
description=f"Specialized {specialty.lower()} department providing comprehensive medical care",
|
# description=f"Specialized {specialty.lower()} department providing comprehensive medical care",
|
||||||
department_type=dept_type[0],
|
# department_type=dept_type[0],
|
||||||
parent_department=None, # Main departments
|
# parent_department=None, # Main departments
|
||||||
phone=f"+966-{random.randint(1, 9)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}",
|
# phone=f"+966-{random.randint(1, 9)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}",
|
||||||
extension=f"{random.randint(1000, 9999)}",
|
# extension=f"{random.randint(1000, 9999)}",
|
||||||
email=f"{specialty.lower().replace(' ', '').replace('and', '')}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa",
|
# email=f"{specialty.lower().replace(' ', '').replace('and', '')}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa",
|
||||||
building=f"Building {random.choice(['A', 'B', 'C', 'D', 'Medical Tower'])}",
|
# building=f"Building {random.choice(['A', 'B', 'C', 'D', 'Medical Tower'])}",
|
||||||
floor=f"Floor {random.randint(1, 10)}",
|
# floor=f"Floor {random.randint(1, 10)}",
|
||||||
wing=random.choice(['North Wing', 'South Wing', 'East Wing', 'West Wing', 'Central Wing']),
|
# wing=random.choice(['North Wing', 'South Wing', 'East Wing', 'West Wing', 'Central Wing']),
|
||||||
room_numbers=f"{random.randint(100, 999)}-{random.randint(100, 999)}",
|
# room_numbers=f"{random.randint(100, 999)}-{random.randint(100, 999)}",
|
||||||
is_active=True,
|
# is_active=True,
|
||||||
is_24_hour=specialty in ['Emergency Medicine', 'Internal Medicine', 'Cardiology'],
|
# is_24_hour=specialty in ['Emergency Medicine', 'Internal Medicine', 'Cardiology'],
|
||||||
operating_hours={
|
# operating_hours={
|
||||||
"sunday": {"open": "07:00", "close": "20:00"},
|
# "sunday": {"open": "07:00", "close": "20:00"},
|
||||||
"monday": {"open": "07:00", "close": "20:00"},
|
# "monday": {"open": "07:00", "close": "20:00"},
|
||||||
"tuesday": {"open": "07:00", "close": "20:00"},
|
# "tuesday": {"open": "07:00", "close": "20:00"},
|
||||||
"wednesday": {"open": "07:00", "close": "20:00"},
|
# "wednesday": {"open": "07:00", "close": "20:00"},
|
||||||
"thursday": {"open": "07:00", "close": "20:00"},
|
# "thursday": {"open": "07:00", "close": "20:00"},
|
||||||
"friday": {"open": "14:00", "close": "20:00"}, # Friday afternoon
|
# "friday": {"open": "14:00", "close": "20:00"}, # Friday afternoon
|
||||||
"saturday": {"open": "07:00", "close": "20:00"}
|
# "saturday": {"open": "07:00", "close": "20:00"}
|
||||||
},
|
# },
|
||||||
cost_center_code=f"CC-{random.randint(1000, 9999)}",
|
# cost_center_code=f"CC-{random.randint(1000, 9999)}",
|
||||||
budget_code=f"BG-{specialty.replace(' ', '').upper()[:6]}",
|
# budget_code=f"BG-{specialty.replace(' ', '').upper()[:6]}",
|
||||||
authorized_positions=random.randint(5, 50),
|
# authorized_positions=random.randint(5, 50),
|
||||||
current_staff_count=random.randint(3, 45),
|
# current_staff_count=random.randint(3, 45),
|
||||||
accreditation_required=True,
|
# accreditation_required=True,
|
||||||
accreditation_body="CBAHI",
|
# accreditation_body="CBAHI",
|
||||||
last_inspection_date=django_timezone.now() - timedelta(days=random.randint(30, 365)),
|
# last_inspection_date=django_timezone.now() - timedelta(days=random.randint(30, 365)),
|
||||||
next_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)),
|
# created_at=django_timezone.now() - timedelta(days=random.randint(1, 180)),
|
||||||
updated_at=django_timezone.now()
|
# updated_at=django_timezone.now()
|
||||||
)
|
# )
|
||||||
departments.append(department)
|
# departments.append(department)
|
||||||
|
#
|
||||||
print(f"Created {departments_per_tenant} departments for {tenant.name}")
|
# print(f"Created {departments_per_tenant} departments for {tenant.name}")
|
||||||
|
#
|
||||||
return departments
|
# return departments
|
||||||
|
|
||||||
|
|
||||||
def create_saudi_system_configurations(tenants):
|
def create_saudi_system_configurations(tenants):
|
||||||
@ -439,8 +438,8 @@ def main():
|
|||||||
create_super_user()
|
create_super_user()
|
||||||
|
|
||||||
# Create departments
|
# Create departments
|
||||||
print("\n2. Creating Saudi Medical Departments...")
|
# print("\n2. Creating Saudi Medical Departments...")
|
||||||
departments = create_saudi_departments(tenants, 12)
|
# departments = create_saudi_departments(tenants, 12)
|
||||||
|
|
||||||
# Create system configurations
|
# Create system configurations
|
||||||
print("\n3. Creating Saudi System Configurations...")
|
print("\n3. Creating Saudi System Configurations...")
|
||||||
@ -461,7 +460,7 @@ def main():
|
|||||||
print(f"\n✅ Saudi Healthcare Data Generation Complete!")
|
print(f"\n✅ Saudi Healthcare Data Generation Complete!")
|
||||||
print(f"📊 Summary:")
|
print(f"📊 Summary:")
|
||||||
print(f" - Tenants: {len(tenants)}")
|
print(f" - Tenants: {len(tenants)}")
|
||||||
print(f" - Departments: {len(departments)}")
|
# print(f" - Departments: {len(departments)}")
|
||||||
print(f" - System Configurations: {len(configurations)}")
|
print(f" - System Configurations: {len(configurations)}")
|
||||||
print(f" - Notifications: {len(notifications)}")
|
print(f" - Notifications: {len(notifications)}")
|
||||||
print(f" - Audit Logs: {len(audit_logs)}")
|
print(f" - Audit Logs: {len(audit_logs)}")
|
||||||
@ -469,7 +468,7 @@ def main():
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'tenants': tenants,
|
'tenants': tenants,
|
||||||
'departments': departments,
|
# 'departments': departments,
|
||||||
'configurations': configurations,
|
'configurations': configurations,
|
||||||
'notifications': notifications,
|
'notifications': notifications,
|
||||||
'audit_logs': audit_logs,
|
'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.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|||||||
Binary file not shown.
@ -537,7 +537,7 @@ def create_encounters(tenants, days_back=30):
|
|||||||
suitable_admissions = [
|
suitable_admissions = [
|
||||||
adm for adm in admissions
|
adm for adm in admissions
|
||||||
if adm.patient == patient and
|
if adm.patient == patient and
|
||||||
adm.admission_date <= encounter_date
|
adm.admission_datetime <= start_datetime
|
||||||
]
|
]
|
||||||
if suitable_admissions:
|
if suitable_admissions:
|
||||||
linked_admission = random.choice(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.
|
Admin interface for departments.
|
||||||
"""
|
"""
|
||||||
list_display = [
|
list_display = [
|
||||||
'department_code', 'name', 'department_type',
|
'code', 'name', 'department_type',
|
||||||
'department_head', 'employee_count_display',
|
'department_head', 'employee_count_display',
|
||||||
'total_fte_display', 'is_active'
|
'total_fte_display', 'is_active'
|
||||||
]
|
]
|
||||||
@ -221,7 +221,7 @@ class DepartmentAdmin(admin.ModelAdmin):
|
|||||||
'tenant', 'department_type', 'is_active'
|
'tenant', 'department_type', 'is_active'
|
||||||
]
|
]
|
||||||
search_fields = [
|
search_fields = [
|
||||||
'department_code', 'name', 'description'
|
'code', 'name', 'description'
|
||||||
]
|
]
|
||||||
readonly_fields = [
|
readonly_fields = [
|
||||||
'department_id', 'employee_count', 'total_fte',
|
'department_id', 'employee_count', 'total_fte',
|
||||||
@ -230,7 +230,7 @@ class DepartmentAdmin(admin.ModelAdmin):
|
|||||||
fieldsets = [
|
fieldsets = [
|
||||||
('Department Information', {
|
('Department Information', {
|
||||||
'fields': [
|
'fields': [
|
||||||
'department_id', 'tenant', 'department_code', 'name', 'description'
|
'department_id', 'tenant', 'code', 'name', 'description'
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
('Department Type', {
|
('Department Type', {
|
||||||
|
|||||||
@ -274,7 +274,7 @@ class DepartmentForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Department
|
model = Department
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'department_code', 'description', 'department_type',
|
'name', 'code', 'description', 'department_type',
|
||||||
'parent_department', 'department_head', 'annual_budget',
|
'parent_department', 'department_head', 'annual_budget',
|
||||||
'cost_center', 'location', 'is_active', 'notes'
|
'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.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -40,8 +40,11 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"department_code",
|
"code",
|
||||||
models.CharField(help_text="Department code", max_length=20),
|
models.CharField(
|
||||||
|
help_text="Department code (e.g., CARD, EMER, SURG)",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
("name", models.CharField(help_text="Department name", max_length=100)),
|
("name", models.CharField(help_text="Department name", max_length=100)),
|
||||||
(
|
(
|
||||||
@ -64,6 +67,33 @@ class Migration(migrations.Migration):
|
|||||||
max_length=20,
|
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",
|
"annual_budget",
|
||||||
models.DecimalField(
|
models.DecimalField(
|
||||||
@ -83,6 +113,12 @@ class Migration(migrations.Migration):
|
|||||||
null=True,
|
null=True,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"authorized_positions",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
default=0, help_text="Number of authorized positions"
|
||||||
|
),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"location",
|
"location",
|
||||||
models.CharField(
|
models.CharField(
|
||||||
@ -96,6 +132,50 @@ class Migration(migrations.Migration):
|
|||||||
"is_active",
|
"is_active",
|
||||||
models.BooleanField(default=True, help_text="Department 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",
|
"notes",
|
||||||
models.TextField(
|
models.TextField(
|
||||||
@ -122,7 +202,7 @@ class Migration(migrations.Migration):
|
|||||||
help_text="Parent department",
|
help_text="Parent department",
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="child_departments",
|
related_name="sub_departments",
|
||||||
to="hr.department",
|
to="hr.department",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -131,7 +211,7 @@ class Migration(migrations.Migration):
|
|||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
help_text="Organization tenant",
|
help_text="Organization tenant",
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="hr_departments",
|
related_name="departments",
|
||||||
to="core.tenant",
|
to="core.tenant",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -248,6 +328,16 @@ class Migration(migrations.Migration):
|
|||||||
blank=True, help_text="Country", max_length=50, null=True
|
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",
|
"date_of_birth",
|
||||||
models.DateField(blank=True, help_text="Date of birth", null=True),
|
models.DateField(blank=True, help_text="Date of birth", null=True),
|
||||||
@ -1193,6 +1283,12 @@ class Migration(migrations.Migration):
|
|||||||
"passed",
|
"passed",
|
||||||
models.BooleanField(default=False, help_text="Training passed"),
|
models.BooleanField(default=False, help_text="Training passed"),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"is_certified",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, help_text="Training is certified"
|
||||||
|
),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"certificate_number",
|
"certificate_number",
|
||||||
models.CharField(
|
models.CharField(
|
||||||
@ -1299,9 +1395,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="department",
|
model_name="department",
|
||||||
index=models.Index(
|
index=models.Index(fields=["code"], name="hr_departme_code_d27daf_idx"),
|
||||||
fields=["department_code"], name="hr_departme_departm_078f94_idx"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="department",
|
model_name="department",
|
||||||
@ -1315,7 +1409,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name="department",
|
name="department",
|
||||||
unique_together={("tenant", "department_code")},
|
unique_together={("tenant", "code")},
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="performancereview",
|
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,
|
editable=False,
|
||||||
help_text='Unique department identifier'
|
help_text='Unique department identifier'
|
||||||
)
|
)
|
||||||
department_code = models.CharField(
|
code = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
help_text='Department code (e.g., CARD, EMER, SURG)'
|
help_text='Department code (e.g., CARD, EMER, SURG)'
|
||||||
)
|
)
|
||||||
@ -570,14 +570,14 @@ class Department(models.Model):
|
|||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['tenant', 'department_type']),
|
models.Index(fields=['tenant', 'department_type']),
|
||||||
models.Index(fields=['department_code']),
|
models.Index(fields=['code']),
|
||||||
models.Index(fields=['name']),
|
models.Index(fields=['name']),
|
||||||
models.Index(fields=['is_active']),
|
models.Index(fields=['is_active']),
|
||||||
]
|
]
|
||||||
unique_together = ['tenant', 'department_code']
|
unique_together = ['tenant', 'code']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.department_code} - {self.name}"
|
return f"{self.code} - {self.name}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
|
|||||||
10
hr/urls.py
10
hr/urls.py
@ -30,7 +30,15 @@ urlpatterns = [
|
|||||||
path('departments/<int:pk>/', views.DepartmentDetailView.as_view(), name='department_detail'),
|
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>/update/', views.DepartmentUpdateView.as_view(), name='department_update'),
|
||||||
path('departments/<int:pk>/delete/', views.DepartmentDeleteView.as_view(), name='department_delete'),
|
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)
|
# 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 django.db import transaction
|
||||||
from datetime import datetime, timedelta, date
|
from datetime import datetime, timedelta, date
|
||||||
import json
|
import json
|
||||||
|
from accounts.models import User
|
||||||
from .models import (
|
from .models import (
|
||||||
Employee, Department, Schedule, ScheduleAssignment,
|
Employee, Department, Schedule, ScheduleAssignment,
|
||||||
TimeEntry, PerformanceReview, TrainingRecord
|
TimeEntry, PerformanceReview, TrainingRecord
|
||||||
@ -26,6 +26,7 @@ from .forms import (
|
|||||||
EmployeeForm, DepartmentForm, ScheduleForm, ScheduleAssignmentForm,
|
EmployeeForm, DepartmentForm, ScheduleForm, ScheduleAssignmentForm,
|
||||||
TimeEntryForm, PerformanceReviewForm, TrainingRecordForm
|
TimeEntryForm, PerformanceReviewForm, TrainingRecordForm
|
||||||
)
|
)
|
||||||
|
from core.utils import AuditLogger
|
||||||
|
|
||||||
|
|
||||||
class HRDashboardView(LoginRequiredMixin, TemplateView):
|
class HRDashboardView(LoginRequiredMixin, TemplateView):
|
||||||
@ -1284,6 +1285,222 @@ def api_department_list(request):
|
|||||||
return JsonResponse({'departments': list(departments)})
|
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
|
# Query patterns to use if needed
|
||||||
# # All upcoming sessions for a tenant (next 30 days)
|
# # All upcoming sessions for a tenant (next 30 days)
|
||||||
# TrainingSession.objects.filter(
|
# TrainingSession.objects.filter(
|
||||||
|
|||||||
@ -106,7 +106,7 @@ def create_saudi_departments(tenants):
|
|||||||
for tenant in tenants:
|
for tenant in tenants:
|
||||||
# Check for existing departments to avoid duplicates
|
# Check for existing departments to avoid duplicates
|
||||||
existing_dept_codes = set(
|
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:
|
for dept_code, dept_name, dept_desc in SAUDI_DEPARTMENTS:
|
||||||
@ -129,7 +129,7 @@ def create_saudi_departments(tenants):
|
|||||||
try:
|
try:
|
||||||
department = Department.objects.create(
|
department = Department.objects.create(
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
department_code=dept_code,
|
code=dept_code,
|
||||||
name=dept_name,
|
name=dept_name,
|
||||||
description=dept_desc,
|
description=dept_desc,
|
||||||
department_type=dept_type,
|
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.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -403,6 +403,15 @@ class Migration(migrations.Migration):
|
|||||||
max_length=30,
|
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",
|
"room_type",
|
||||||
models.CharField(
|
models.CharField(
|
||||||
@ -518,10 +527,12 @@ class Migration(migrations.Migration):
|
|||||||
models.CharField(
|
models.CharField(
|
||||||
blank=True,
|
blank=True,
|
||||||
choices=[
|
choices=[
|
||||||
("WINDOW", "Window Side"),
|
("A", "A"),
|
||||||
("DOOR", "Door Side"),
|
("B", "B"),
|
||||||
("CENTER", "Center"),
|
("C", "C"),
|
||||||
("CORNER", "Corner"),
|
("D", "D"),
|
||||||
|
("E", "E"),
|
||||||
|
("F", "F"),
|
||||||
],
|
],
|
||||||
help_text="Position within room",
|
help_text="Position within room",
|
||||||
max_length=20,
|
max_length=20,
|
||||||
@ -619,7 +630,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"ward_id",
|
"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)),
|
("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.core.validators
|
||||||
import django.db.models.deletion
|
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.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -325,6 +325,12 @@ class Migration(migrations.Migration):
|
|||||||
default=0, help_text="Reorder quantity"
|
default=0, help_text="Reorder quantity"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"min_stock_level",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
blank=True, help_text="Minimum stock level", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"max_stock_level",
|
"max_stock_level",
|
||||||
models.PositiveIntegerField(
|
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.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
@ -19,6 +19,177 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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(
|
migrations.CreateModel(
|
||||||
name="LabTest",
|
name="LabTest",
|
||||||
fields=[
|
fields=[
|
||||||
@ -352,7 +523,7 @@ class Migration(migrations.Migration):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="LabOrder",
|
name="LabResult",
|
||||||
fields=[
|
fields=[
|
||||||
(
|
(
|
||||||
"id",
|
"id",
|
||||||
@ -364,89 +535,123 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"order_id",
|
"result_id",
|
||||||
models.UUIDField(
|
models.UUIDField(
|
||||||
default=uuid.uuid4,
|
default=uuid.uuid4,
|
||||||
editable=False,
|
editable=False,
|
||||||
help_text="Unique order identifier",
|
help_text="Unique result identifier",
|
||||||
unique=True,
|
unique=True,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"order_number",
|
"result_value",
|
||||||
|
models.TextField(
|
||||||
|
blank=True, help_text="Test result value", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"result_unit",
|
||||||
models.CharField(
|
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",
|
"result_type",
|
||||||
models.DateTimeField(
|
|
||||||
default=django.utils.timezone.now,
|
|
||||||
help_text="Date and time order was placed",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"priority",
|
|
||||||
models.CharField(
|
models.CharField(
|
||||||
choices=[
|
choices=[
|
||||||
("ROUTINE", "Routine"),
|
("NUMERIC", "Numeric"),
|
||||||
("URGENT", "Urgent"),
|
("TEXT", "Text"),
|
||||||
("STAT", "STAT"),
|
("CODED", "Coded"),
|
||||||
("ASAP", "ASAP"),
|
("NARRATIVE", "Narrative"),
|
||||||
("TIMED", "Timed"),
|
|
||||||
],
|
],
|
||||||
default="ROUTINE",
|
default="NUMERIC",
|
||||||
help_text="Order priority",
|
help_text="Type of result",
|
||||||
max_length=20,
|
max_length=20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"clinical_indication",
|
"reference_range",
|
||||||
models.TextField(
|
|
||||||
blank=True, help_text="Clinical indication for tests", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"diagnosis_code",
|
|
||||||
models.CharField(
|
models.CharField(
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="ICD-10 diagnosis code",
|
help_text="Reference range",
|
||||||
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,
|
max_length=100,
|
||||||
null=True,
|
null=True,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"fasting_status",
|
"abnormal_flag",
|
||||||
models.CharField(
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
choices=[
|
choices=[
|
||||||
("FASTING", "Fasting"),
|
("N", "Normal"),
|
||||||
("NON_FASTING", "Non-Fasting"),
|
("H", "High"),
|
||||||
("UNKNOWN", "Unknown"),
|
("L", "Low"),
|
||||||
|
("HH", "Critical High"),
|
||||||
|
("LL", "Critical Low"),
|
||||||
|
("A", "Abnormal"),
|
||||||
],
|
],
|
||||||
default="UNKNOWN",
|
help_text="Abnormal flag",
|
||||||
help_text="Patient fasting status",
|
max_length=10,
|
||||||
max_length=20,
|
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(
|
models.CharField(
|
||||||
choices=[
|
choices=[
|
||||||
("PENDING", "Pending"),
|
("PENDING", "Pending"),
|
||||||
("SCHEDULED", "Scheduled"),
|
|
||||||
("COLLECTED", "Collected"),
|
|
||||||
("IN_PROGRESS", "In Progress"),
|
("IN_PROGRESS", "In Progress"),
|
||||||
("COMPLETED", "Completed"),
|
("COMPLETED", "Completed"),
|
||||||
|
("VERIFIED", "Verified"),
|
||||||
|
("AMENDED", "Amended"),
|
||||||
("CANCELLED", "Cancelled"),
|
("CANCELLED", "Cancelled"),
|
||||||
("ON_HOLD", "On Hold"),
|
|
||||||
],
|
],
|
||||||
default="PENDING",
|
default="PENDING",
|
||||||
help_text="Order status",
|
help_text="Result status",
|
||||||
max_length=20,
|
max_length=20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"special_instructions",
|
"technician_comments",
|
||||||
models.TextField(
|
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,
|
blank=True,
|
||||||
help_text="Special instructions for collection or processing",
|
help_text="Date and time result was reported",
|
||||||
null=True,
|
null=True,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
("updated_at", models.DateTimeField(auto_now=True)),
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
(
|
(
|
||||||
"encounter",
|
"analyzed_by",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Related encounter",
|
help_text="Lab technician who analyzed",
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
related_name="lab_orders",
|
related_name="analyzed_results",
|
||||||
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,
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"patient",
|
"order",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
help_text="Patient",
|
help_text="Related lab order",
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="lab_orders",
|
related_name="results",
|
||||||
to="patients.patientprofile",
|
to="laboratory.laborder",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"tenant",
|
"verified_by",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
help_text="Organization tenant",
|
blank=True,
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
help_text="Lab professional who verified result",
|
||||||
related_name="lab_orders",
|
null=True,
|
||||||
to="core.tenant",
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="verified_results",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"tests",
|
"test",
|
||||||
models.ManyToManyField(
|
models.ForeignKey(
|
||||||
help_text="Ordered tests",
|
help_text="Lab test",
|
||||||
related_name="orders",
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="results",
|
||||||
to="laboratory.labtest",
|
to="laboratory.labtest",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"verbose_name": "Lab Order",
|
"verbose_name": "Lab Result",
|
||||||
"verbose_name_plural": "Lab Orders",
|
"verbose_name_plural": "Lab Results",
|
||||||
"db_table": "laboratory_lab_order",
|
"db_table": "laboratory_lab_result",
|
||||||
"ordering": ["-order_datetime"],
|
"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(
|
migrations.CreateModel(
|
||||||
name="QualityControl",
|
name="QualityControl",
|
||||||
fields=[
|
fields=[
|
||||||
@ -647,6 +878,14 @@ class Migration(migrations.Migration):
|
|||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"result",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="quality_controls",
|
||||||
|
to="laboratory.labresult",
|
||||||
|
),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"reviewed_by",
|
"reviewed_by",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
@ -1033,244 +1272,15 @@ class Migration(migrations.Migration):
|
|||||||
"ordering": ["-collected_datetime"],
|
"ordering": ["-collected_datetime"],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.AddField(
|
||||||
name="LabResult",
|
model_name="labresult",
|
||||||
fields=[
|
name="specimen",
|
||||||
(
|
field=models.ForeignKey(
|
||||||
"id",
|
help_text="Specimen used for test",
|
||||||
models.BigAutoField(
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
auto_created=True,
|
related_name="results",
|
||||||
primary_key=True,
|
to="laboratory.specimen",
|
||||||
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.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="labtest",
|
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 django.db.models.deletion
|
||||||
import uuid
|
import uuid
|
||||||
@ -1283,7 +1283,7 @@ class Migration(migrations.Migration):
|
|||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
help_text="Operating surgeon",
|
help_text="Operating surgeon",
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="surgical_notes",
|
related_name="surgeon_surgical_notes",
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1292,7 +1292,7 @@ class Migration(migrations.Migration):
|
|||||||
models.OneToOneField(
|
models.OneToOneField(
|
||||||
help_text="Related surgical case",
|
help_text="Related surgical case",
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="surgical_note",
|
related_name="surgical_notes",
|
||||||
to="operating_theatre.surgicalcase",
|
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:
|
class Meta:
|
||||||
model = EmergencyContact
|
model = EmergencyContact
|
||||||
|
exclude = ["patient"]
|
||||||
fields = [
|
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',
|
'mobile_number', 'email', 'address_line_1', 'address_line_2', 'city',
|
||||||
'state', 'zip_code', 'priority', 'is_authorized_for_medical_decisions'
|
'state', 'zip_code', 'priority', 'is_authorized_for_medical_decisions'
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'patient': forms.Select(attrs={'class': 'form-select'}),
|
# 'patient': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
|
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
|
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'relationship': forms.Select(attrs={'class': 'form-select'}),
|
'relationship': forms.Select(attrs={'class': 'form-select'}),
|
||||||
@ -102,11 +103,11 @@ class EmergencyContactForm(forms.ModelForm):
|
|||||||
user = kwargs.pop('user', None)
|
user = kwargs.pop('user', None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if user and hasattr(user, 'tenant'):
|
# if user and hasattr(user, 'tenant'):
|
||||||
self.fields['patient'].queryset = PatientProfile.objects.filter(
|
# self.fields['patient'].queryset = PatientProfile.objects.filter(
|
||||||
tenant=user.tenant,
|
# tenant=user.tenant,
|
||||||
is_active=True
|
# is_active=True
|
||||||
).order_by('last_name', 'first_name')
|
# ).order_by('last_name', 'first_name')
|
||||||
|
|
||||||
|
|
||||||
class InsuranceInfoForm(forms.ModelForm):
|
class InsuranceInfoForm(forms.ModelForm):
|
||||||
@ -115,15 +116,15 @@ class InsuranceInfoForm(forms.ModelForm):
|
|||||||
"""
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InsuranceInfo
|
model = InsuranceInfo
|
||||||
|
exclude = ["patient"]
|
||||||
fields = [
|
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',
|
'policy_number', 'group_number', 'subscriber_name', 'subscriber_relationship',
|
||||||
'subscriber_dob', 'subscriber_ssn', 'effective_date', 'termination_date',
|
'subscriber_dob', 'subscriber_ssn', 'effective_date', 'termination_date',
|
||||||
'copay_amount', 'deductible_amount', 'out_of_pocket_max', 'is_verified',
|
'copay_amount', 'deductible_amount', 'out_of_pocket_max', 'is_verified',
|
||||||
'requires_authorization'
|
'requires_authorization'
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'patient': forms.Select(attrs={'class': 'form-select'}),
|
|
||||||
'insurance_type': forms.Select(attrs={'class': 'form-select'}),
|
'insurance_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'insurance_company': forms.TextInput(attrs={'class': 'form-control'}),
|
'insurance_company': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'plan_name': 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)
|
user = kwargs.pop('user', None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if user and hasattr(user, 'tenant'):
|
# if user and hasattr(user, 'tenant'):
|
||||||
self.fields['patient'].queryset = PatientProfile.objects.filter(
|
# self.fields['patient'].queryset = PatientProfile.objects.filter(
|
||||||
tenant=user.tenant,
|
# tenant=user.tenant,
|
||||||
is_active=True
|
# is_active=True
|
||||||
).order_by('last_name', 'first_name')
|
# ).order_by('last_name', 'first_name')
|
||||||
|
|
||||||
|
|
||||||
class ConsentTemplateForm(forms.ModelForm):
|
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