1287 lines
47 KiB
Python
1287 lines
47 KiB
Python
from django.contrib.auth.decorators import login_required
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView
|
|
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
|
from django.contrib import messages
|
|
from django.urls import reverse_lazy, reverse
|
|
from django.http import JsonResponse, HttpResponse
|
|
from django.utils import timezone
|
|
from django.db.models import Sum, Count, Q, Avg, F
|
|
from django.db import transaction
|
|
from decimal import Decimal
|
|
import csv
|
|
from datetime import datetime, timedelta
|
|
from .models import *
|
|
from .forms import *
|
|
|
|
|
|
class FacilityDashboardView(LoginRequiredMixin, TemplateView):
|
|
template_name = 'facility_management/dashboard.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['page_title'] = 'Facility Management Dashboard'
|
|
|
|
# Get today's date
|
|
today = timezone.now().date()
|
|
|
|
# Calculate statistics
|
|
context['stats'] = {
|
|
'total_buildings': Building.objects.filter(is_active=True).count(),
|
|
'total_assets': Asset.objects.count(),
|
|
'operational_assets': Asset.objects.filter(status=Asset.AssetStatus.OPERATIONAL).count(),
|
|
'assets_needing_maintenance': Asset.objects.filter(next_maintenance_date__lte=today).count(),
|
|
'open_work_orders': MaintenanceRequest.objects.filter(
|
|
status__in=[
|
|
MaintenanceRequest.MaintenanceStatus.SUBMITTED,
|
|
MaintenanceRequest.MaintenanceStatus.ASSIGNED,
|
|
MaintenanceRequest.MaintenanceStatus.IN_PROGRESS
|
|
]
|
|
).count(),
|
|
'completed_today': MaintenanceRequest.objects.filter(
|
|
completed_date__date=today,
|
|
status=MaintenanceRequest.MaintenanceStatus.COMPLETED
|
|
).count(),
|
|
'total_rooms': Room.objects.count(),
|
|
'occupied_rooms': Room.objects.filter(occupancy_status=Room.OccupancyStatus.OCCUPIED).count(),
|
|
'active_contracts': ServiceContract.objects.filter(status=ServiceContract.ContractStatus.ACTIVE).count(),
|
|
'contracts_expiring_soon': ServiceContract.objects.filter(
|
|
end_date__lte=today + timedelta(days=30),
|
|
status=ServiceContract.ContractStatus.ACTIVE
|
|
).count(),
|
|
}
|
|
|
|
# Recent maintenance requests
|
|
context['recent_requests'] = MaintenanceRequest.objects.select_related(
|
|
'building', 'requested_by', 'assigned_to'
|
|
).order_by('-requested_date')[:10]
|
|
|
|
# Assets needing maintenance
|
|
context['maintenance_alerts'] = Asset.objects.filter(
|
|
next_maintenance_date__lte=today + timedelta(days=7)
|
|
).select_related('building', 'category')[:5]
|
|
|
|
# Work orders by status
|
|
context['work_order_stats'] = MaintenanceRequest.objects.values('status').annotate(
|
|
count=Count('id')
|
|
).order_by('status')
|
|
|
|
# Assets by condition
|
|
context['asset_condition_stats'] = Asset.objects.values('condition').annotate(
|
|
count=Count('id')
|
|
).order_by('condition')
|
|
|
|
# Monthly maintenance costs
|
|
current_month = today.replace(day=1)
|
|
context['monthly_costs'] = MaintenanceRequest.objects.filter(
|
|
completed_date__gte=current_month,
|
|
status=MaintenanceRequest.MaintenanceStatus.COMPLETED
|
|
).aggregate(
|
|
total_cost=Sum('actual_cost'),
|
|
avg_cost=Avg('actual_cost'),
|
|
count=Count('id')
|
|
)
|
|
|
|
# Expiring contracts
|
|
context['expiring_contracts'] = ServiceContract.objects.filter(
|
|
end_date__lte=today + timedelta(days=30),
|
|
status=ServiceContract.ContractStatus.ACTIVE
|
|
).select_related('vendor')[:5]
|
|
|
|
return context
|
|
|
|
|
|
class BuildingListView(LoginRequiredMixin, ListView):
|
|
model = Building
|
|
template_name = 'facility_management/buildings/list.html'
|
|
context_object_name = 'buildings'
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
tenant = self.request.user.tenant
|
|
queryset = Building.objects.filter(tenant=tenant).order_by( 'name')
|
|
|
|
# Search functionality
|
|
search = self.request.GET.get('search')
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(name__icontains=search) |
|
|
Q(code__icontains=search) |
|
|
Q(airport_code__icontains=search)
|
|
)
|
|
|
|
# Filter by building type
|
|
building_type = self.request.GET.get('building_type')
|
|
if building_type:
|
|
queryset = queryset.filter(building_type=building_type)
|
|
|
|
# Filter by airport
|
|
# airport = self.request.GET.get('airport')
|
|
# if airport:
|
|
# queryset = queryset.filter(airport_code=airport)
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['building_types'] = Building.BuildingType.choices
|
|
# context['airports'] = Building.objects.values_list('airport_code', flat=True).distinct()
|
|
return context
|
|
|
|
|
|
class BuildingDetailView(LoginRequiredMixin, DetailView):
|
|
model = Building
|
|
template_name = 'facility_management/buildings/detail.html'
|
|
context_object_name = 'building'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
building = self.get_object()
|
|
|
|
# Building statistics
|
|
context['building_stats'] = {
|
|
'total_floors': building.floors.count(),
|
|
'total_rooms': Room.objects.filter(floor__building=building).count(),
|
|
'occupied_rooms': Room.objects.filter(floor__building=building, occupancy_status='occupied').count(),
|
|
'total_assets': Asset.objects.filter(building=building).count(),
|
|
'operational_assets': Asset.objects.filter(building=building, status='operational').count(),
|
|
}
|
|
|
|
# Recent maintenance requests for this building
|
|
context['recent_maintenance'] = MaintenanceRequest.objects.filter(
|
|
building=building
|
|
).select_related('requested_by', 'assigned_to').order_by('-requested_date')[:5]
|
|
|
|
# Assets in this building
|
|
context['building_assets'] = Asset.objects.filter(
|
|
building=building
|
|
).select_related('category')[:10]
|
|
|
|
return context
|
|
|
|
|
|
class BuildingCreateView(LoginRequiredMixin, CreateView):
|
|
model = Building
|
|
form_class = BuildingForm
|
|
template_name = 'facility_management/buildings/form.html'
|
|
success_url = reverse_lazy('facility_management:building_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Building created successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class BuildingUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = Building
|
|
form_class = BuildingForm
|
|
template_name = 'facility_management/buildings/form.html'
|
|
success_url = reverse_lazy('facility_management:building_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Building updated successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class AssetListView(LoginRequiredMixin, ListView):
|
|
model = Asset
|
|
template_name = 'facility_management/assets/list.html'
|
|
context_object_name = 'assets'
|
|
paginate_by = 25
|
|
|
|
def get_queryset(self):
|
|
queryset = Asset.objects.select_related('category', 'building').order_by('asset_id')
|
|
|
|
# Search functionality
|
|
search = self.request.GET.get('search')
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(name__icontains=search) |
|
|
Q(asset_id__icontains=search) |
|
|
Q(serial_number__icontains=search)
|
|
)
|
|
|
|
# Filter by category
|
|
category = self.request.GET.get('category')
|
|
if category:
|
|
queryset = queryset.filter(category_id=category)
|
|
|
|
# Filter by building
|
|
building = self.request.GET.get('building')
|
|
if building:
|
|
queryset = queryset.filter(building_id=building)
|
|
|
|
# Filter by status
|
|
status = self.request.GET.get('status')
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
|
|
# Filter by condition
|
|
condition = self.request.GET.get('condition')
|
|
if condition:
|
|
queryset = queryset.filter(condition=condition)
|
|
|
|
# Filter by maintenance needs
|
|
maintenance_filter = self.request.GET.get('maintenance')
|
|
if maintenance_filter == 'due':
|
|
queryset = queryset.filter(next_maintenance_date__lte=timezone.now().date())
|
|
elif maintenance_filter == 'overdue':
|
|
queryset = queryset.filter(next_maintenance_date__lt=timezone.now().date())
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['categories'] = AssetCategory.objects.filter(is_active=True)
|
|
context['buildings'] = Building.objects.filter(is_active=True)
|
|
context['status_choices'] = Asset.AssetStatus.choices
|
|
context['condition_choices'] = Asset.AssetCondition.choices
|
|
return context
|
|
|
|
|
|
class AssetDetailView(LoginRequiredMixin, DetailView):
|
|
model = Asset
|
|
template_name = 'facility_management/assets/detail.html'
|
|
context_object_name = 'asset'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
asset = self.get_object()
|
|
|
|
# Maintenance history
|
|
context['maintenance_history'] = MaintenanceRequest.objects.filter(
|
|
asset=asset
|
|
).select_related('requested_by', 'assigned_to').order_by('-requested_date')[:10]
|
|
|
|
# Asset value depreciation calculation
|
|
if asset.purchase_cost and asset.purchase_date:
|
|
years_owned = Decimal((timezone.now().date() - asset.purchase_date).days) / Decimal('365.25')
|
|
rate = Decimal(asset.depreciation_rate) / Decimal('100')
|
|
depreciated_value = asset.purchase_cost * (Decimal('1') - rate) * years_owned
|
|
context['calculated_value'] = max(depreciated_value, Decimal('0.00'))
|
|
|
|
return context
|
|
|
|
|
|
class AssetCreateView(LoginRequiredMixin, CreateView):
|
|
model = Asset
|
|
form_class = AssetForm
|
|
template_name = 'facility_management/assets/form.html'
|
|
success_url = reverse_lazy('facility_management:asset_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Asset created successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class AssetUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = Asset
|
|
form_class = AssetForm
|
|
template_name = 'facility_management/assets/form.html'
|
|
success_url = reverse_lazy('facility_management:asset_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Asset updated successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class MaintenanceRequestListView(LoginRequiredMixin, ListView):
|
|
model = MaintenanceRequest
|
|
template_name = 'facility_management/maintenance/list.html'
|
|
context_object_name = 'maintenance_requests'
|
|
paginate_by = 25
|
|
|
|
def get_queryset(self):
|
|
queryset = MaintenanceRequest.objects.select_related(
|
|
'building', 'requested_by', 'assigned_to', 'maintenance_type'
|
|
).order_by('-requested_date')
|
|
|
|
# Apply filters
|
|
form = MaintenanceFilterForm(self.request.GET)
|
|
if form.is_valid():
|
|
if form.cleaned_data['status']:
|
|
queryset = queryset.filter(status=form.cleaned_data['status'])
|
|
if form.cleaned_data['priority']:
|
|
queryset = queryset.filter(priority=form.cleaned_data['priority'])
|
|
if form.cleaned_data['building']:
|
|
queryset = queryset.filter(building=form.cleaned_data['building'])
|
|
if form.cleaned_data['assigned_to']:
|
|
queryset = queryset.filter(assigned_to=form.cleaned_data['assigned_to'])
|
|
if form.cleaned_data['date_from']:
|
|
queryset = queryset.filter(requested_date__date__gte=form.cleaned_data['date_from'])
|
|
if form.cleaned_data['date_to']:
|
|
queryset = queryset.filter(requested_date__date__lte=form.cleaned_data['date_to'])
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['filter_form'] = MaintenanceFilterForm(self.request.GET)
|
|
return context
|
|
|
|
|
|
class MaintenanceRequestDetailView(LoginRequiredMixin, DetailView):
|
|
model = MaintenanceRequest
|
|
template_name = 'facility_management/maintenance/detail.html'
|
|
context_object_name = 'maintenance'
|
|
|
|
def get_queryset(self):
|
|
return MaintenanceRequest.objects.select_related(
|
|
'building', 'floor', 'room', 'asset', 'requested_by', 'assigned_to', 'maintenance_type'
|
|
)
|
|
|
|
|
|
class MaintenanceRequestCreateView(LoginRequiredMixin, CreateView):
|
|
model = MaintenanceRequest
|
|
form_class = MaintenanceRequestForm
|
|
template_name = 'facility_management/maintenance/form.html'
|
|
success_url = reverse_lazy('facility_management:maintenance_list')
|
|
|
|
def form_valid(self, form):
|
|
form.instance.requested_by = self.request.user
|
|
messages.success(self.request, 'Maintenance request created successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class MaintenanceRequestUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = MaintenanceRequest
|
|
form_class = MaintenanceUpdateForm
|
|
template_name = 'facility_management/maintenance/form.html'
|
|
success_url = reverse_lazy('facility_management:maintenance_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Maintenance request updated successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class RoomListView(LoginRequiredMixin, ListView):
|
|
model = Room
|
|
template_name = 'facility_management/rooms/list.html'
|
|
context_object_name = 'rooms'
|
|
paginate_by = 25
|
|
|
|
def get_queryset(self):
|
|
queryset = Room.objects.select_related('floor__building').order_by('floor__building__name', 'floor__floor_number', 'room_number')
|
|
|
|
# Search functionality
|
|
search = self.request.GET.get('search')
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(room_number__icontains=search) |
|
|
Q(name__icontains=search) |
|
|
Q(current_tenant__icontains=search)
|
|
)
|
|
|
|
# Filter by building
|
|
building = self.request.GET.get('building')
|
|
if building:
|
|
queryset = queryset.filter(floor__building_id=building)
|
|
|
|
# Filter by room type
|
|
room_type = self.request.GET.get('room_type')
|
|
if room_type:
|
|
queryset = queryset.filter(room_type=room_type)
|
|
|
|
# Filter by occupancy status
|
|
occupancy = self.request.GET.get('occupancy')
|
|
if occupancy:
|
|
queryset = queryset.filter(occupancy_status=occupancy)
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['buildings'] = Building.objects.filter(is_active=True)
|
|
context['occupancy_statuses'] = Room.OccupancyStatus.choices
|
|
return context
|
|
|
|
|
|
class RoomDetailView(LoginRequiredMixin, DetailView):
|
|
model = Room
|
|
template_name = 'facility_management/rooms/detail.html'
|
|
context_object_name = 'room'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
room = self.get_object()
|
|
|
|
# Assets in this room
|
|
context['room_assets'] = Asset.objects.filter(room=room).select_related('category')
|
|
|
|
# Maintenance requests for this room
|
|
context['room_maintenance'] = MaintenanceRequest.objects.filter(
|
|
room=room
|
|
).select_related('requested_by', 'assigned_to').order_by('-requested_date')[:5]
|
|
|
|
return context
|
|
|
|
|
|
class RoomCreateView(LoginRequiredMixin, CreateView):
|
|
model = Room
|
|
form_class = RoomForm
|
|
template_name = 'facility_management/rooms/form.html'
|
|
success_url = reverse_lazy('facility_management:room_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Room created successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class RoomUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = Room
|
|
form_class = RoomForm
|
|
template_name = 'facility_management/rooms/form.html'
|
|
success_url = reverse_lazy('facility_management:room_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Room updated successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class VendorListView(LoginRequiredMixin, ListView):
|
|
model = Vendor
|
|
template_name = 'facility_management/vendors/list.html'
|
|
context_object_name = 'vendors'
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
queryset = Vendor.objects.order_by('name')
|
|
|
|
# Search functionality
|
|
search = self.request.GET.get('search')
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(name__icontains=search) |
|
|
Q(contact_person__icontains=search) |
|
|
Q(email__icontains=search)
|
|
)
|
|
|
|
# Filter by vendor type
|
|
vendor_type = self.request.GET.get('vendor_type')
|
|
if vendor_type:
|
|
queryset = queryset.filter(vendor_type=vendor_type)
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['vendor_types'] = Vendor.VendorType.choices
|
|
return context
|
|
|
|
|
|
class VendorDetailView(LoginRequiredMixin, DetailView):
|
|
model = Vendor
|
|
template_name = 'facility_management/vendors/detail.html'
|
|
context_object_name = 'vendor'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
vendor = self.get_object()
|
|
|
|
# Active contracts
|
|
context['active_contracts'] = ServiceContract.objects.filter(
|
|
vendor=vendor,
|
|
status='active'
|
|
).order_by('-start_date')
|
|
|
|
# Contract history
|
|
context['contract_history'] = ServiceContract.objects.filter(
|
|
vendor=vendor
|
|
).order_by('-start_date')[:10]
|
|
|
|
return context
|
|
|
|
|
|
class VendorCreateView(LoginRequiredMixin, CreateView):
|
|
model = Vendor
|
|
form_class = VendorForm
|
|
template_name = 'facility_management/vendors/form.html'
|
|
success_url = reverse_lazy('facility_management:vendor_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Vendor created successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class VendorUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = Vendor
|
|
form_class = VendorForm
|
|
template_name = 'facility_management/vendors/form.html'
|
|
success_url = reverse_lazy('facility_management:vendor_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Vendor updated successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class ServiceContractListView(LoginRequiredMixin, ListView):
|
|
model = ServiceContract
|
|
template_name = 'facility_management/contracts/list.html'
|
|
context_object_name = 'contracts'
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
return ServiceContract.objects.select_related('vendor').order_by('-start_date')
|
|
|
|
|
|
class ServiceContractDetailView(LoginRequiredMixin, DetailView):
|
|
model = ServiceContract
|
|
template_name = 'facility_management/contracts/detail.html'
|
|
context_object_name = 'contract'
|
|
|
|
|
|
class ServiceContractCreateView(LoginRequiredMixin, CreateView):
|
|
model = ServiceContract
|
|
form_class = ServiceContractForm
|
|
template_name = 'facility_management/contracts/form.html'
|
|
success_url = reverse_lazy('facility_management:contract_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Service contract created successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class ServiceContractUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = ServiceContract
|
|
form_class = ServiceContractForm
|
|
template_name = 'facility_management/contracts/form.html'
|
|
success_url = reverse_lazy('facility_management:contract_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Service contract updated successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class FacilityReportView(LoginRequiredMixin, TemplateView):
|
|
template_name = 'facility_management/reports/facility.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
today = timezone.now().date()
|
|
|
|
# Buildings
|
|
context['buildings'] = Building.objects.filter(is_active=True)
|
|
context['total_buildings'] = Building.objects.count()
|
|
context['active_buildings'] = Building.objects.filter(is_active=True).count()
|
|
|
|
# Assets
|
|
context['total_assets'] = Asset.objects.count()
|
|
context['operational_assets'] = Asset.objects.filter(status=Asset.AssetStatus.OPERATIONAL).count()
|
|
|
|
# Asset statistics
|
|
context['asset_stats'] = {
|
|
'by_category': AssetCategory.objects.annotate(
|
|
asset_count=Count('asset')
|
|
).order_by('-asset_count'),
|
|
'by_condition': Asset.objects.values('condition').annotate(
|
|
count=Count('id')
|
|
).order_by('condition'),
|
|
'by_status': Asset.objects.values('status').annotate(
|
|
count=Count('id')
|
|
).order_by('status'),
|
|
}
|
|
|
|
# Maintenance
|
|
context['pending_maintenance'] = MaintenanceRequest.objects.filter(
|
|
status__in=[
|
|
MaintenanceRequest.MaintenanceStatus.SUBMITTED,
|
|
MaintenanceRequest.MaintenanceStatus.ASSIGNED
|
|
]
|
|
).count()
|
|
context['overdue_maintenance'] = MaintenanceRequest.objects.filter(
|
|
scheduled_date__lt=timezone.now(),
|
|
status__in=[
|
|
MaintenanceRequest.MaintenanceStatus.SUBMITTED,
|
|
MaintenanceRequest.MaintenanceStatus.ASSIGNED
|
|
]
|
|
).count()
|
|
|
|
# Maintenance statistics
|
|
context['maintenance_stats'] = {
|
|
'by_status': MaintenanceRequest.objects.values('status').annotate(
|
|
count=Count('id')
|
|
).order_by('status'),
|
|
'by_priority': MaintenanceRequest.objects.values('priority').annotate(
|
|
count=Count('id')
|
|
).order_by('priority'),
|
|
'by_type': MaintenanceRequest.objects.values('maintenance_type__name').annotate(
|
|
count=Count('id')
|
|
).order_by('-count')[:5],
|
|
'monthly_costs': MaintenanceRequest.objects.filter(
|
|
completed_date__gte=today.replace(day=1)
|
|
).aggregate(
|
|
total=Sum('actual_cost'),
|
|
avg=Avg('actual_cost'),
|
|
count=Count('id')
|
|
),
|
|
}
|
|
|
|
# Maintenance trends (last 6 months)
|
|
context['maintenance_trends'] = {
|
|
'completed': [12, 15, 18, 14, 16, 20], # Placeholder data
|
|
'pending': [5, 4, 6, 3, 5, 4] # Placeholder data
|
|
}
|
|
|
|
# Performance metrics
|
|
context['avg_completion_time'] = 3.5 # Placeholder
|
|
context['on_time_completion'] = 85 # Placeholder
|
|
|
|
# Contracts
|
|
context['active_contracts'] = ServiceContract.objects.filter(
|
|
status=ServiceContract.ContractStatus.ACTIVE
|
|
).count()
|
|
context['total_contract_value'] = ServiceContract.objects.filter(
|
|
status=ServiceContract.ContractStatus.ACTIVE
|
|
).aggregate(total=Sum('contract_value'))['total'] or 0
|
|
|
|
# Space utilization
|
|
total_rooms = Room.objects.count()
|
|
occupied_rooms = Room.objects.filter(occupancy_status=Room.OccupancyStatus.OCCUPIED).count()
|
|
|
|
context['space_stats'] = {
|
|
'total_rooms': total_rooms,
|
|
'occupied_rooms': occupied_rooms,
|
|
'by_type': Room.objects.values('occupancy_status').annotate(
|
|
count=Count('id')
|
|
).order_by('-count'),
|
|
}
|
|
context['total_rooms'] = total_rooms
|
|
context['occupied_rooms'] = occupied_rooms
|
|
context['available_rooms'] = Room.objects.filter(occupancy_status=Room.OccupancyStatus.VACANT).count()
|
|
context['maintenance_rooms'] = Room.objects.filter(occupancy_status=Room.OccupancyStatus.MAINTENANCE).count()
|
|
context['reserved_rooms'] = Room.objects.filter(occupancy_status=Room.OccupancyStatus.RESERVED).count()
|
|
|
|
# Financial overview
|
|
context['total_maintenance_cost'] = MaintenanceRequest.objects.filter(
|
|
status=MaintenanceRequest.MaintenanceStatus.COMPLETED
|
|
).aggregate(total=Sum('actual_cost'))['total'] or 0
|
|
|
|
context['cost_breakdown'] = {
|
|
'maintenance': context['total_maintenance_cost'],
|
|
'utilities': 0, # Placeholder
|
|
'contracts': context['total_contract_value'],
|
|
'supplies': 0, # Placeholder
|
|
'other': 0, # Placeholder
|
|
}
|
|
|
|
# Vendor performance
|
|
context['vendor_performance'] = Vendor.objects.filter(is_active=True).annotate(
|
|
active_contracts_count=Count('servicecontract', filter=Q(servicecontract__status=ServiceContract.ContractStatus.ACTIVE)),
|
|
total_contract_value=Sum('servicecontract__contract_value', filter=Q(servicecontract__status=ServiceContract.ContractStatus.ACTIVE))
|
|
)[:10]
|
|
|
|
# KPIs
|
|
context['facility_utilization'] = round((occupied_rooms / total_rooms * 100) if total_rooms > 0 else 0, 1)
|
|
context['maintenance_efficiency'] = 75 # Placeholder
|
|
context['cost_per_sqft'] = 0 # Placeholder
|
|
context['energy_efficiency'] = 80 # Placeholder
|
|
context['budget_utilization'] = 65 # Placeholder
|
|
|
|
# Report metadata
|
|
context['report_date'] = timezone.now()
|
|
context['report_start_date'] = today - timedelta(days=30)
|
|
context['report_end_date'] = today
|
|
context['report_period_days'] = 30
|
|
|
|
return context
|
|
|
|
|
|
@login_required
|
|
def get_buildings(request):
|
|
"""Get building data via AJAX"""
|
|
buildings = Building.objects.filter(is_active=True).values('id', 'name')
|
|
return JsonResponse({'buildings': list(buildings)})
|
|
|
|
|
|
@login_required
|
|
def get_floors_by_building(request):
|
|
"""Get floors for a specific building via AJAX"""
|
|
building_id = request.GET.get('building_id')
|
|
if building_id:
|
|
floors = Floor.objects.filter(building_id=building_id).values('id', 'name')
|
|
return JsonResponse({'floors': list(floors)})
|
|
return JsonResponse({'floors': []})
|
|
|
|
|
|
@login_required
|
|
def get_rooms_by_floor(request):
|
|
"""Get rooms for a specific floor via AJAX"""
|
|
floor_id = request.GET.get('floor_id')
|
|
if floor_id:
|
|
rooms = Room.objects.filter(floor_id=floor_id).values('id', 'room_number', 'name')
|
|
return JsonResponse({'rooms': list(rooms)})
|
|
return JsonResponse({'rooms': []})
|
|
|
|
|
|
@login_required
|
|
def get_assets_by_location(request):
|
|
"""Get assets for a specific location via AJAX"""
|
|
building_id = request.GET.get('building_id')
|
|
floor_id = request.GET.get('floor_id')
|
|
room_id = request.GET.get('room_id')
|
|
|
|
assets = Asset.objects.all()
|
|
|
|
if building_id:
|
|
assets = assets.filter(building_id=building_id)
|
|
if floor_id:
|
|
assets = assets.filter(floor_id=floor_id)
|
|
if room_id:
|
|
assets = assets.filter(room_id=room_id)
|
|
|
|
asset_data = assets.values('id', 'asset_id', 'name')
|
|
return JsonResponse({'assets': list(asset_data)})
|
|
|
|
|
|
@login_required
|
|
def export_assets_csv(request):
|
|
"""Export assets data to CSV"""
|
|
response = HttpResponse(content_type='text/csv')
|
|
response['Content-Disposition'] = 'attachment; filename="assets_report.csv"'
|
|
|
|
writer = csv.writer(response)
|
|
writer.writerow([
|
|
'Asset ID', 'Name', 'Category', 'Building', 'Status', 'Condition',
|
|
'Purchase Date', 'Purchase Cost', 'Current Value', 'Last Inspection'
|
|
])
|
|
|
|
assets = Asset.objects.select_related('category', 'building').order_by('asset_id')
|
|
|
|
for asset in assets:
|
|
writer.writerow([
|
|
asset.asset_id,
|
|
asset.name,
|
|
asset.category.name,
|
|
asset.building.name,
|
|
asset.get_status_display(),
|
|
asset.get_condition_display(),
|
|
asset.purchase_date,
|
|
asset.purchase_cost,
|
|
asset.current_value,
|
|
asset.last_inspection_date
|
|
])
|
|
|
|
return response
|
|
|
|
|
|
# ============================================================================
|
|
# INSPECTION VIEWS
|
|
# ============================================================================
|
|
|
|
class InspectionListView(LoginRequiredMixin, ListView):
|
|
model = Inspection
|
|
template_name = 'facility_management/inspections/list.html'
|
|
context_object_name = 'inspections'
|
|
paginate_by = 25
|
|
|
|
def get_queryset(self):
|
|
queryset = Inspection.objects.select_related(
|
|
'building', 'inspector'
|
|
).order_by('-scheduled_date')
|
|
|
|
# Search functionality
|
|
search = self.request.GET.get('search')
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(inspection_id__icontains=search) |
|
|
Q(title__icontains=search) |
|
|
Q(inspector_external__icontains=search)
|
|
)
|
|
|
|
# Filter by inspection type
|
|
inspection_type = self.request.GET.get('inspection_type')
|
|
if inspection_type:
|
|
queryset = queryset.filter(inspection_type=inspection_type)
|
|
|
|
# Filter by status
|
|
status = self.request.GET.get('status')
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
|
|
# Filter by building
|
|
building = self.request.GET.get('building')
|
|
if building:
|
|
queryset = queryset.filter(building_id=building)
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['inspection_types'] = Inspection.InspectionType.choices
|
|
context['status_choices'] = Inspection.Status.choices
|
|
context['buildings'] = Building.objects.filter(is_active=True)
|
|
return context
|
|
|
|
|
|
class InspectionDetailView(LoginRequiredMixin, DetailView):
|
|
model = Inspection
|
|
template_name = 'facility_management/inspections/detail.html'
|
|
context_object_name = 'inspection'
|
|
|
|
def get_queryset(self):
|
|
return Inspection.objects.select_related(
|
|
'building', 'inspector'
|
|
).prefetch_related('floors', 'rooms', 'assets')
|
|
|
|
|
|
class InspectionCreateView(LoginRequiredMixin, CreateView):
|
|
model = Inspection
|
|
form_class = InspectionForm
|
|
template_name = 'facility_management/inspections/form.html'
|
|
success_url = reverse_lazy('facility_management:inspection_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Inspection created successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class InspectionUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = Inspection
|
|
form_class = InspectionForm
|
|
template_name = 'facility_management/inspections/form.html'
|
|
success_url = reverse_lazy('facility_management:inspection_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Inspection updated successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
# ============================================================================
|
|
# ENERGY METER VIEWS
|
|
# ============================================================================
|
|
|
|
class EnergyMeterListView(LoginRequiredMixin, ListView):
|
|
model = EnergyMeter
|
|
template_name = 'facility_management/energy_meters/list.html'
|
|
context_object_name = 'meters'
|
|
paginate_by = 25
|
|
|
|
def get_queryset(self):
|
|
queryset = EnergyMeter.objects.select_related('building').order_by('building', 'meter_type', 'meter_id')
|
|
|
|
# Search functionality
|
|
search = self.request.GET.get('search')
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(meter_id__icontains=search) |
|
|
Q(serial_number__icontains=search) |
|
|
Q(location_description__icontains=search)
|
|
)
|
|
|
|
# Filter by meter type
|
|
meter_type = self.request.GET.get('meter_type')
|
|
if meter_type:
|
|
queryset = queryset.filter(meter_type=meter_type)
|
|
|
|
# Filter by building
|
|
building = self.request.GET.get('building')
|
|
if building:
|
|
queryset = queryset.filter(building_id=building)
|
|
|
|
# Filter by active status
|
|
is_active = self.request.GET.get('is_active')
|
|
if is_active:
|
|
queryset = queryset.filter(is_active=is_active == 'true')
|
|
|
|
# Filter by calibration status
|
|
calibration = self.request.GET.get('calibration')
|
|
if calibration == 'due':
|
|
queryset = queryset.filter(next_calibration_date__lte=timezone.now().date())
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['meter_types'] = EnergyMeter.MeterType.choices
|
|
context['buildings'] = Building.objects.filter(is_active=True)
|
|
return context
|
|
|
|
|
|
class EnergyMeterDetailView(LoginRequiredMixin, DetailView):
|
|
model = EnergyMeter
|
|
template_name = 'facility_management/energy_meters/detail.html'
|
|
context_object_name = 'meter'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
meter = self.get_object()
|
|
|
|
# Recent readings
|
|
context['recent_readings'] = EnergyReading.objects.filter(
|
|
meter=meter
|
|
).select_related('read_by').order_by('-reading_date')[:10]
|
|
|
|
# Calculate consumption trends
|
|
last_30_days = timezone.now() - timedelta(days=30)
|
|
context['monthly_consumption'] = EnergyReading.objects.filter(
|
|
meter=meter,
|
|
reading_date__gte=last_30_days
|
|
).aggregate(
|
|
total_consumption=Sum('consumption'),
|
|
total_cost=Sum('cost'),
|
|
avg_consumption=Avg('consumption')
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class EnergyMeterCreateView(LoginRequiredMixin, CreateView):
|
|
model = EnergyMeter
|
|
form_class = EnergyMeterForm
|
|
template_name = 'facility_management/energy_meters/form.html'
|
|
success_url = reverse_lazy('facility_management:energy_meter_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Energy meter created successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class EnergyMeterUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = EnergyMeter
|
|
form_class = EnergyMeterForm
|
|
template_name = 'facility_management/energy_meters/form.html'
|
|
success_url = reverse_lazy('facility_management:energy_meter_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Energy meter updated successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
# ============================================================================
|
|
# ENERGY READING VIEWS
|
|
# ============================================================================
|
|
|
|
class EnergyReadingListView(LoginRequiredMixin, ListView):
|
|
model = EnergyReading
|
|
template_name = 'facility_management/energy_readings/list.html'
|
|
context_object_name = 'readings'
|
|
paginate_by = 50
|
|
|
|
def get_queryset(self):
|
|
queryset = EnergyReading.objects.select_related(
|
|
'meter__building', 'read_by'
|
|
).order_by('-reading_date')
|
|
|
|
# Filter by meter
|
|
meter = self.request.GET.get('meter')
|
|
if meter:
|
|
queryset = queryset.filter(meter_id=meter)
|
|
|
|
# Filter by date range
|
|
date_from = self.request.GET.get('date_from')
|
|
if date_from:
|
|
queryset = queryset.filter(reading_date__date__gte=date_from)
|
|
|
|
date_to = self.request.GET.get('date_to')
|
|
if date_to:
|
|
queryset = queryset.filter(reading_date__date__lte=date_to)
|
|
|
|
# Filter by estimated readings
|
|
is_estimated = self.request.GET.get('is_estimated')
|
|
if is_estimated:
|
|
queryset = queryset.filter(is_estimated=is_estimated == 'true')
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['meters'] = EnergyMeter.objects.filter(is_active=True).select_related('building')
|
|
|
|
# Calculate summary statistics
|
|
queryset = self.get_queryset()
|
|
context['summary'] = queryset.aggregate(
|
|
total_consumption=Sum('consumption'),
|
|
total_cost=Sum('cost'),
|
|
avg_consumption=Avg('consumption'),
|
|
count=Count('id')
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class EnergyReadingDetailView(LoginRequiredMixin, DetailView):
|
|
model = EnergyReading
|
|
template_name = 'facility_management/energy_readings/detail.html'
|
|
context_object_name = 'reading'
|
|
|
|
def get_queryset(self):
|
|
return EnergyReading.objects.select_related('meter__building', 'read_by')
|
|
|
|
|
|
class EnergyReadingCreateView(LoginRequiredMixin, CreateView):
|
|
model = EnergyReading
|
|
form_class = EnergyReadingForm
|
|
template_name = 'facility_management/energy_readings/form.html'
|
|
success_url = reverse_lazy('facility_management:energy_reading_list')
|
|
|
|
def form_valid(self, form):
|
|
form.instance.read_by = self.request.user
|
|
messages.success(self.request, 'Energy reading recorded successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class EnergyReadingUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = EnergyReading
|
|
form_class = EnergyReadingForm
|
|
template_name = 'facility_management/energy_readings/form.html'
|
|
success_url = reverse_lazy('facility_management:energy_reading_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Energy reading updated successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
# ============================================================================
|
|
# SPACE RESERVATION VIEWS
|
|
# ============================================================================
|
|
|
|
class SpaceReservationListView(LoginRequiredMixin, ListView):
|
|
model = SpaceReservation
|
|
template_name = 'facility_management/reservations/list.html'
|
|
context_object_name = 'reservations'
|
|
paginate_by = 25
|
|
|
|
def get_queryset(self):
|
|
queryset = SpaceReservation.objects.select_related(
|
|
'room__floor__building', 'reserved_by', 'approved_by'
|
|
).order_by('-start_datetime')
|
|
|
|
# Search functionality
|
|
search = self.request.GET.get('search')
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(title__icontains=search) |
|
|
Q(contact_person__icontains=search) |
|
|
Q(contact_email__icontains=search)
|
|
)
|
|
|
|
# Filter by room
|
|
room = self.request.GET.get('room')
|
|
if room:
|
|
queryset = queryset.filter(room_id=room)
|
|
|
|
# Filter by status
|
|
status = self.request.GET.get('status')
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
|
|
# Filter by date range
|
|
date_from = self.request.GET.get('date_from')
|
|
if date_from:
|
|
queryset = queryset.filter(start_datetime__date__gte=date_from)
|
|
|
|
date_to = self.request.GET.get('date_to')
|
|
if date_to:
|
|
queryset = queryset.filter(start_datetime__date__lte=date_to)
|
|
|
|
# Filter by user's reservations
|
|
my_reservations = self.request.GET.get('my_reservations')
|
|
if my_reservations:
|
|
queryset = queryset.filter(reserved_by=self.request.user)
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['rooms'] = Room.objects.select_related('floor__building')
|
|
context['status_choices'] = SpaceReservation.ReservationStatus.choices
|
|
|
|
# Upcoming reservations
|
|
context['upcoming_count'] = SpaceReservation.objects.filter(
|
|
start_datetime__gte=timezone.now(),
|
|
status__in=['PENDING', 'CONFIRMED']
|
|
).count()
|
|
|
|
return context
|
|
|
|
|
|
class SpaceReservationDetailView(LoginRequiredMixin, DetailView):
|
|
model = SpaceReservation
|
|
template_name = 'facility_management/reservations/detail.html'
|
|
context_object_name = 'reservation'
|
|
|
|
def get_queryset(self):
|
|
return SpaceReservation.objects.select_related(
|
|
'room__floor__building', 'reserved_by', 'approved_by'
|
|
)
|
|
|
|
|
|
class SpaceReservationCreateView(LoginRequiredMixin, CreateView):
|
|
model = SpaceReservation
|
|
form_class = SpaceReservationForm
|
|
template_name = 'facility_management/reservations/form.html'
|
|
success_url = reverse_lazy('facility_management:reservation_list')
|
|
|
|
def form_valid(self, form):
|
|
form.instance.reserved_by = self.request.user
|
|
messages.success(self.request, 'Space reservation created successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class SpaceReservationUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = SpaceReservation
|
|
form_class = SpaceReservationForm
|
|
template_name = 'facility_management/reservations/form.html'
|
|
success_url = reverse_lazy('facility_management:reservation_list')
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, 'Space reservation updated successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
@login_required
|
|
def approve_reservation(request, pk):
|
|
"""Approve a space reservation"""
|
|
reservation = get_object_or_404(SpaceReservation, pk=pk)
|
|
|
|
if request.method == 'POST':
|
|
reservation.status = 'CONFIRMED'
|
|
reservation.approved_by = request.user
|
|
reservation.approved_at = timezone.now()
|
|
reservation.save()
|
|
messages.success(request, f'Reservation "{reservation.title}" approved successfully.')
|
|
|
|
return redirect('facility_management:reservation_detail', pk=pk)
|
|
|
|
|
|
@login_required
|
|
def cancel_reservation(request, pk):
|
|
"""Cancel a space reservation"""
|
|
reservation = get_object_or_404(SpaceReservation, pk=pk)
|
|
|
|
if request.method == 'POST':
|
|
reservation.status = 'CANCELLED'
|
|
reservation.save()
|
|
messages.success(request, f'Reservation "{reservation.title}" cancelled successfully.')
|
|
|
|
return redirect('facility_management:reservation_detail', pk=pk)
|
|
|
|
|
|
@login_required
|
|
def export_inspections_csv(request):
|
|
"""Export inspections to CSV"""
|
|
response = HttpResponse(content_type='text/csv')
|
|
response['Content-Disposition'] = 'attachment; filename="inspections_report.csv"'
|
|
|
|
writer = csv.writer(response)
|
|
writer.writerow([
|
|
'Inspection ID', 'Type', 'Title', 'Building', 'Status',
|
|
'Scheduled Date', 'Inspector', 'Overall Rating', 'Requires Follow-up'
|
|
])
|
|
|
|
inspections = Inspection.objects.select_related('building', 'inspector').order_by('-scheduled_date')
|
|
|
|
for inspection in inspections:
|
|
writer.writerow([
|
|
inspection.inspection_id,
|
|
inspection.get_inspection_type_display(),
|
|
inspection.title,
|
|
inspection.building.name,
|
|
inspection.get_status_display(),
|
|
inspection.scheduled_date.strftime('%Y-%m-%d %H:%M'),
|
|
inspection.inspector.get_full_name() if inspection.inspector else inspection.inspector_external,
|
|
inspection.overall_rating,
|
|
'Yes' if inspection.requires_followup else 'No'
|
|
])
|
|
|
|
return response
|
|
|
|
|
|
@login_required
|
|
def export_energy_readings_csv(request):
|
|
"""Export energy readings to CSV"""
|
|
response = HttpResponse(content_type='text/csv')
|
|
response['Content-Disposition'] = 'attachment; filename="energy_readings_report.csv"'
|
|
|
|
writer = csv.writer(response)
|
|
writer.writerow([
|
|
'Meter ID', 'Meter Type', 'Building', 'Reading Date',
|
|
'Reading Value', 'Consumption', 'Cost', 'Is Estimated', 'Read By'
|
|
])
|
|
|
|
readings = EnergyReading.objects.select_related(
|
|
'meter__building', 'read_by'
|
|
).order_by('-reading_date')
|
|
|
|
for reading in readings:
|
|
writer.writerow([
|
|
reading.meter.meter_id,
|
|
reading.meter.get_meter_type_display(),
|
|
reading.meter.building.name,
|
|
reading.reading_date.strftime('%Y-%m-%d %H:%M'),
|
|
reading.reading_value,
|
|
reading.consumption,
|
|
reading.cost,
|
|
'Yes' if reading.is_estimated else 'No',
|
|
reading.read_by.get_full_name()
|
|
])
|
|
|
|
return response
|
|
|
|
|
|
@login_required
|
|
def export_reservations_csv(request):
|
|
"""Export space reservations to CSV"""
|
|
response = HttpResponse(content_type='text/csv')
|
|
response['Content-Disposition'] = 'attachment; filename="reservations_report.csv"'
|
|
|
|
writer = csv.writer(response)
|
|
writer.writerow([
|
|
'Reservation ID', 'Room', 'Title', 'Start Date', 'End Date',
|
|
'Status', 'Reserved By', 'Contact Person', 'Expected Attendees',
|
|
'Total Cost', 'Approved By'
|
|
])
|
|
|
|
reservations = SpaceReservation.objects.select_related(
|
|
'room__floor__building', 'reserved_by', 'approved_by'
|
|
).order_by('-start_datetime')
|
|
|
|
for reservation in reservations:
|
|
writer.writerow([
|
|
reservation.reservation_id,
|
|
f"{reservation.room.floor.building.code}-{reservation.room.floor.name}-{reservation.room.room_number}",
|
|
reservation.title,
|
|
reservation.start_datetime.strftime('%Y-%m-%d %H:%M'),
|
|
reservation.end_datetime.strftime('%Y-%m-%d %H:%M'),
|
|
reservation.get_status_display(),
|
|
reservation.reserved_by.get_full_name(),
|
|
reservation.contact_person,
|
|
reservation.expected_attendees,
|
|
reservation.total_cost,
|
|
reservation.approved_by.get_full_name() if reservation.approved_by else ''
|
|
])
|
|
|
|
return response
|
|
|
|
|
|
@login_required
|
|
def export_maintenance_csv(request):
|
|
"""Export maintenance requests to CSV"""
|
|
response = HttpResponse(content_type='text/csv')
|
|
response['Content-Disposition'] = 'attachment; filename="maintenance_report.csv"'
|
|
|
|
writer = csv.writer(response)
|
|
writer.writerow([
|
|
'Request ID', 'Title', 'Building', 'Priority', 'Status',
|
|
'Requested Date', 'Requested By', 'Assigned To', 'Estimated Cost', 'Actual Cost'
|
|
])
|
|
|
|
requests = MaintenanceRequest.objects.select_related(
|
|
'building', 'requested_by', 'assigned_to'
|
|
).order_by('-requested_date')
|
|
|
|
for req in requests:
|
|
writer.writerow([
|
|
req.request_id,
|
|
req.title,
|
|
req.building.name,
|
|
req.get_priority_display(),
|
|
req.get_status_display(),
|
|
req.requested_date.strftime('%Y-%m-%d %H:%M:%S'),
|
|
req.requested_by.get_full_name(),
|
|
req.assigned_to.get_full_name() if req.assigned_to else '',
|
|
req.estimated_cost,
|
|
req.actual_cost
|
|
])
|
|
|
|
return response
|