This commit is contained in:
Marwan Alwali 2025-09-08 19:52:52 +03:00
parent 23158e9fbf
commit 84c1fb798e
167 changed files with 12990 additions and 4471 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -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

View File

@ -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

View File

@ -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)),
], ],

View File

@ -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

View File

@ -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,
),
),
]

View File

@ -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

View File

@ -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

View File

@ -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')),

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'),

View File

@ -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 CSV response # Create HTTP response with CSV content type
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

View File

@ -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

View File

@ -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
),
),
]

View 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")},
),
]

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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=[

View File

@ -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

View File

@ -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

View File

@ -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
# #

View File

@ -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,

Binary file not shown.

View File

@ -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

View File

@ -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.

View File

@ -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', {

View File

@ -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'
] ]

View File

@ -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",

View File

@ -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"),
),
]

View File

@ -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):

View File

@ -30,6 +30,14 @@ 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)

View File

@ -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(

View File

@ -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,

View File

@ -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)),
( (

View File

@ -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),
),
]

View File

@ -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,
),
),
]

View File

@ -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"),
),
]

View File

@ -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

View File

@ -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(

View File

@ -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
),
),
]

View File

@ -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,245 +1272,16 @@ 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",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"result_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique result identifier",
unique=True,
),
),
(
"result_value",
models.TextField(
blank=True, help_text="Test result value", null=True
),
),
(
"result_unit",
models.CharField(
blank=True,
help_text="Result unit of measure",
max_length=20,
null=True,
),
),
(
"result_type",
models.CharField(
choices=[
("NUMERIC", "Numeric"),
("TEXT", "Text"),
("CODED", "Coded"),
("NARRATIVE", "Narrative"),
],
default="NUMERIC",
help_text="Type of result",
max_length=20,
),
),
(
"reference_range",
models.CharField(
blank=True,
help_text="Reference range",
max_length=100,
null=True,
),
),
(
"abnormal_flag",
models.CharField(
blank=True,
choices=[
("N", "Normal"),
("H", "High"),
("L", "Low"),
("HH", "Critical High"),
("LL", "Critical Low"),
("A", "Abnormal"),
],
help_text="Abnormal flag",
max_length=10,
null=True,
),
),
(
"is_critical",
models.BooleanField(
default=False, help_text="Result is critical value"
),
),
(
"critical_called",
models.BooleanField(
default=False, help_text="Critical value was called to provider"
),
),
(
"critical_called_datetime",
models.DateTimeField(
blank=True,
help_text="Date and time critical value was called",
null=True,
),
),
(
"critical_called_to",
models.CharField(
blank=True,
help_text="Person critical value was called to",
max_length=100,
null=True,
),
),
(
"analyzed_datetime",
models.DateTimeField(
blank=True, help_text="Date and time analyzed", null=True
),
),
(
"analyzer",
models.CharField(
blank=True,
help_text="Analyzer/instrument used",
max_length=100,
null=True,
),
),
(
"verified",
models.BooleanField(
default=False, help_text="Result has been verified"
),
),
(
"verified_datetime",
models.DateTimeField(
blank=True, help_text="Date and time of verification", null=True
),
),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("IN_PROGRESS", "In Progress"),
("COMPLETED", "Completed"),
("VERIFIED", "Verified"),
("AMENDED", "Amended"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
help_text="Result status",
max_length=20,
),
),
(
"technician_comments",
models.TextField(
blank=True, help_text="Technician comments", null=True
),
),
(
"pathologist_comments",
models.TextField(
blank=True, help_text="Pathologist comments", null=True
),
),
(
"qc_passed",
models.BooleanField(
default=True, help_text="Quality control passed"
),
),
(
"qc_notes",
models.TextField(
blank=True, help_text="Quality control notes", null=True
),
),
(
"reported_datetime",
models.DateTimeField(
blank=True,
help_text="Date and time result was reported",
null=True,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"analyzed_by",
models.ForeignKey(
blank=True,
help_text="Lab technician who analyzed",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="analyzed_results",
to=settings.AUTH_USER_MODEL,
),
),
(
"order",
models.ForeignKey(
help_text="Related lab order",
on_delete=django.db.models.deletion.CASCADE,
related_name="results",
to="laboratory.laborder",
),
),
(
"verified_by",
models.ForeignKey(
blank=True,
help_text="Lab professional who verified result",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="verified_results",
to=settings.AUTH_USER_MODEL,
),
),
(
"test",
models.ForeignKey(
help_text="Lab test",
on_delete=django.db.models.deletion.CASCADE,
related_name="results",
to="laboratory.labtest",
),
),
(
"specimen",
models.ForeignKey(
help_text="Specimen used for test", help_text="Specimen used for test",
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
related_name="results", related_name="results",
to="laboratory.specimen", 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",
index=models.Index( index=models.Index(

View File

@ -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,
),
]

File diff suppressed because it is too large Load Diff

View File

@ -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",
), ),
), ),

View File

@ -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",
),
),
]

View File

@ -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