694 lines
28 KiB
Python
694 lines
28 KiB
Python
from django.db import models
|
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
|
from decimal import Decimal
|
|
import uuid
|
|
from django.conf import settings
|
|
|
|
|
|
|
|
class Building(models.Model):
|
|
"""Airport buildings and structures"""
|
|
class BuildingType(models.TextChoices):
|
|
CLINICAL = 'CLINICAL', 'Clinical'
|
|
NON_CLINICAL = 'NON_CLINICAL', 'Non Clinical'
|
|
OTHER = 'OTHER', 'Other'
|
|
|
|
tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='buildings')
|
|
name = models.CharField(max_length=100)
|
|
building_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
|
code = models.CharField(max_length=10, unique=True)
|
|
building_type = models.CharField(max_length=20, choices=BuildingType.choices, default=BuildingType.CLINICAL)
|
|
|
|
# Location and dimensions
|
|
address = models.TextField()
|
|
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
|
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
|
floor_count = models.PositiveIntegerField(default=1)
|
|
total_area_sqm = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
|
|
|
# Construction details
|
|
construction_year = models.PositiveIntegerField(null=True, blank=True)
|
|
architect = models.CharField(max_length=100, blank=True)
|
|
contractor = models.CharField(max_length=100, blank=True)
|
|
|
|
# Status
|
|
is_active = models.BooleanField(default=True)
|
|
last_major_renovation = models.DateField(null=True, blank=True)
|
|
|
|
# Management
|
|
facility_manager = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_buildings'
|
|
verbose_name_plural = 'Buildings'
|
|
ordering = ['code', 'name']
|
|
unique_together = ['code', 'name']
|
|
indexes = [
|
|
models.Index(fields=['code', 'name']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.code} - {self.name}"
|
|
|
|
@property
|
|
def occupancy_rate(self):
|
|
"""Calculate the occupancy rate for all rooms in this building"""
|
|
total_rooms = Room.objects.filter(floor__building=self).count()
|
|
if total_rooms == 0:
|
|
return 0
|
|
occupied_rooms = Room.objects.filter(
|
|
floor__building=self,
|
|
occupancy_status=Room.OccupancyStatus.OCCUPIED
|
|
).count()
|
|
return (occupied_rooms / total_rooms) * 100
|
|
|
|
|
|
class Floor(models.Model):
|
|
"""Building floors"""
|
|
building = models.ForeignKey(Building, on_delete=models.CASCADE, related_name='floors')
|
|
floor_number = models.IntegerField(help_text="Use negative numbers for basement levels")
|
|
name = models.CharField(max_length=50, help_text="e.g., Ground Floor, Mezzanine, B1")
|
|
area_sqm = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
|
ceiling_height_m = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
|
|
is_public_access = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_floors'
|
|
ordering = ['building', 'floor_number']
|
|
unique_together = ['building', 'floor_number']
|
|
indexes = [
|
|
models.Index(fields=['building', 'floor_number']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.building.code} - {self.name}"
|
|
|
|
|
|
class Room(models.Model):
|
|
"""Individual rooms and spaces"""
|
|
|
|
class OccupancyStatus(models.TextChoices):
|
|
VACANT = 'VACANT', 'Vacant'
|
|
OCCUPIED = 'OCCUPIED', 'Occupied'
|
|
MAINTENANCE = 'MAINTENANCE', 'Under Maintenance'
|
|
RESERVED = 'RESERVED', 'Reserved'
|
|
|
|
floor = models.ForeignKey(Floor, on_delete=models.CASCADE, related_name='rooms')
|
|
room_number = models.CharField(max_length=20)
|
|
name = models.CharField(max_length=100, blank=True)
|
|
|
|
# Dimensions
|
|
area_sqm = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)
|
|
capacity = models.PositiveIntegerField(null=True, blank=True, help_text="Maximum occupancy")
|
|
|
|
# Status
|
|
occupancy_status = models.CharField(max_length=20, choices=OccupancyStatus.choices, default=OccupancyStatus.VACANT)
|
|
is_accessible = models.BooleanField(default=True, help_text="ADA/accessibility compliant")
|
|
|
|
notes = models.TextField(blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_rooms'
|
|
ordering = ['floor', 'room_number']
|
|
unique_together = ['floor', 'room_number']
|
|
indexes = [
|
|
models.Index(fields=['floor', 'room_number']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.floor.building.code}-{self.floor.name}-{self.room_number}"
|
|
|
|
|
|
class AssetCategory(models.Model):
|
|
"""Categories for facility assets"""
|
|
name = models.CharField(max_length=100, unique=True)
|
|
code = models.CharField(max_length=20, unique=True)
|
|
description = models.TextField(blank=True)
|
|
parent_category = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)
|
|
is_active = models.BooleanField(default=True)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_asset_categories'
|
|
ordering = ['name']
|
|
verbose_name_plural = 'Asset Categories'
|
|
indexes = [
|
|
models.Index(fields=['code']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class Asset(models.Model):
|
|
"""Facility assets and equipment"""
|
|
class AssetStatus(models.TextChoices):
|
|
OPERATIONAL = 'OPERATIONAL', 'Operational'
|
|
MAINTENANCE = 'MAINTENANCE', 'Under Maintenance'
|
|
REPAIR = 'REPAIR', 'Needs Repair'
|
|
RETIRED = 'RETIRED', 'Retired'
|
|
DISPOSED = 'DISPOSED', 'Disposed'
|
|
|
|
class AssetCondition(models.TextChoices):
|
|
EXCELLENT = 'EXCELLENT', 'Excellent'
|
|
GOOD = 'GOOD', 'Good'
|
|
FAIR = 'FAIR', 'Fair'
|
|
POOR = 'POOR', 'Poor'
|
|
CRITICAL = 'CRITICAL', 'Critical'
|
|
|
|
asset_id = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
|
|
name = models.CharField(max_length=200)
|
|
category = models.ForeignKey(AssetCategory, on_delete=models.CASCADE)
|
|
|
|
# Location
|
|
building = models.ForeignKey(Building, on_delete=models.CASCADE)
|
|
floor = models.ForeignKey(Floor, on_delete=models.CASCADE, null=True, blank=True)
|
|
room = models.ForeignKey(Room, on_delete=models.CASCADE, null=True, blank=True)
|
|
location_description = models.CharField(max_length=200, blank=True)
|
|
|
|
# Asset details
|
|
manufacturer = models.CharField(max_length=100, blank=True)
|
|
model = models.CharField(max_length=100, blank=True)
|
|
serial_number = models.CharField(max_length=100, blank=True)
|
|
|
|
# Financial information
|
|
purchase_date = models.DateField(null=True, blank=True)
|
|
purchase_cost = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
current_value = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
depreciation_rate = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal('10.00'), help_text="Annual depreciation percentage")
|
|
|
|
# Warranty and service
|
|
warranty_start_date = models.DateField(null=True, blank=True)
|
|
warranty_end_date = models.DateField(null=True, blank=True)
|
|
service_provider = models.CharField(max_length=100, blank=True)
|
|
service_contract_number = models.CharField(max_length=50, blank=True)
|
|
|
|
# Status and condition
|
|
status = models.CharField(max_length=20, choices=AssetStatus.choices, default=AssetStatus.OPERATIONAL)
|
|
condition = models.CharField(max_length=20, choices=AssetCondition.choices, default=AssetCondition.GOOD, help_text="Current condition of the asset")
|
|
last_inspection_date = models.DateField(null=True, blank=True)
|
|
next_maintenance_date = models.DateField(null=True, blank=True)
|
|
|
|
# Responsible person
|
|
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True)
|
|
|
|
notes = models.TextField(blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_assets'
|
|
unique_together = ['building', 'floor', 'room', 'serial_number']
|
|
ordering = ['asset_id']
|
|
indexes = [
|
|
models.Index(fields=['building', 'floor', 'room', 'serial_number']),
|
|
]
|
|
verbose_name_plural = 'Assets'
|
|
|
|
def __str__(self):
|
|
return f"{self.asset_id} - {self.name}"
|
|
|
|
@property
|
|
def is_under_warranty(self):
|
|
from django.utils import timezone
|
|
if self.warranty_end_date:
|
|
return self.warranty_end_date >= timezone.now().date()
|
|
return False
|
|
|
|
@property
|
|
def needs_maintenance(self):
|
|
from django.utils import timezone
|
|
if self.next_maintenance_date:
|
|
return self.next_maintenance_date <= timezone.now().date()
|
|
return False
|
|
|
|
|
|
class MaintenanceType(models.Model):
|
|
"""Types of maintenance activities"""
|
|
name = models.CharField(max_length=100, unique=True)
|
|
code = models.CharField(max_length=20, unique=True)
|
|
description = models.TextField(blank=True)
|
|
estimated_duration_hours = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
|
|
is_active = models.BooleanField(default=True)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_maintenance_types'
|
|
indexes = [
|
|
models.Index(fields=['code']),
|
|
]
|
|
ordering = ['name']
|
|
verbose_name_plural = 'Maintenance Types'
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class MaintenanceRequest(models.Model):
|
|
"""Maintenance requests and work orders"""
|
|
class Priority(models.TextChoices):
|
|
LOW = 'LOW', 'Low'
|
|
MEDIUM = 'MEDIUM', 'Medium'
|
|
HIGH = 'HIGH', 'High'
|
|
URGENT = 'URGENT', 'Urgent'
|
|
EMERGENCY = 'EMERGENCY', 'Emergency'
|
|
|
|
class MaintenanceStatus(models.TextChoices):
|
|
SUBMITTED = 'SUBMITTED', 'Submitted'
|
|
ASSIGNED = 'ASSIGNED', 'Assigned'
|
|
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
|
|
COMPLETED = 'COMPLETED', 'Completed'
|
|
CANCELLED = 'CANCELLED', 'Cancelled'
|
|
ON_HOLD = 'ON_HOLD', 'On Hold'
|
|
|
|
request_id = models.CharField(max_length=50, unique=True, editable=False)
|
|
title = models.CharField(max_length=200)
|
|
description = models.TextField()
|
|
maintenance_type = models.ForeignKey(MaintenanceType, on_delete=models.CASCADE)
|
|
|
|
# Location
|
|
building = models.ForeignKey(Building, on_delete=models.CASCADE)
|
|
floor = models.ForeignKey(Floor, on_delete=models.CASCADE, null=True, blank=True)
|
|
room = models.ForeignKey(Room, on_delete=models.CASCADE, null=True, blank=True)
|
|
asset = models.ForeignKey(Asset, on_delete=models.CASCADE, null=True, blank=True)
|
|
|
|
# Request details
|
|
priority = models.CharField(max_length=20, choices=Priority.choices, default=Priority.MEDIUM)
|
|
status = models.CharField(max_length=20, choices=MaintenanceStatus.choices, default=MaintenanceStatus.SUBMITTED, help_text="Current status of the request")
|
|
|
|
# People involved
|
|
requested_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='maintenance_requests')
|
|
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_maintenance')
|
|
|
|
# Timing
|
|
requested_date = models.DateTimeField(auto_now_add=True)
|
|
scheduled_date = models.DateTimeField(null=True, blank=True)
|
|
started_date = models.DateTimeField(null=True, blank=True)
|
|
completed_date = models.DateTimeField(null=True, blank=True)
|
|
|
|
# Cost estimation
|
|
estimated_hours = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
|
|
estimated_cost = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
|
actual_cost = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
|
|
|
# Additional information
|
|
notes = models.TextField(blank=True)
|
|
completion_notes = models.TextField(blank=True)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_maintenance_requests'
|
|
indexes = [
|
|
models.Index(fields=['building', 'floor', 'room', 'asset']),
|
|
]
|
|
ordering = ['-requested_date']
|
|
verbose_name_plural = 'Maintenance Requests'
|
|
|
|
def __str__(self):
|
|
return f"{self.request_id} - {self.title}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.request_id:
|
|
# Generate request ID
|
|
from django.utils import timezone
|
|
today = timezone.now().date()
|
|
daily_count = MaintenanceRequest.objects.filter(requested_date__date=today).count() + 1
|
|
self.request_id = f"MR-{today.strftime('%Y%m%d')}-{daily_count:04d}"
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class MaintenanceSchedule(models.Model):
|
|
"""Scheduled maintenance activities"""
|
|
class FrequencyInterval(models.IntegerChoices):
|
|
DAILY = 1, 'Daily'
|
|
WEEKLY = 7, 'Weekly'
|
|
MONTHLY = 30, 'Monthly'
|
|
QUARTERLY = 90, 'Quarterly'
|
|
SEMI_ANNUAL = 182, 'Semi-Annual'
|
|
ANNUAL = 365, 'Annual'
|
|
|
|
name = models.CharField(max_length=200)
|
|
description = models.TextField()
|
|
maintenance_type = models.ForeignKey(MaintenanceType, on_delete=models.CASCADE)
|
|
|
|
# Scope
|
|
asset = models.ForeignKey(Asset, on_delete=models.CASCADE, null=True, blank=True)
|
|
building = models.ForeignKey(Building, on_delete=models.CASCADE, null=True, blank=True)
|
|
room = models.ForeignKey(Room, on_delete=models.CASCADE, null=True, blank=True)
|
|
|
|
# Schedule details
|
|
frequency_interval = models.IntegerField(choices=FrequencyInterval.choices, default=FrequencyInterval.ANNUAL)
|
|
start_date = models.DateField()
|
|
end_date = models.DateField(null=True, blank=True)
|
|
|
|
# Assignment
|
|
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True)
|
|
estimated_duration_hours = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
|
|
|
|
# Status
|
|
is_active = models.BooleanField(default=True)
|
|
last_generated_date = models.DateField(null=True, blank=True)
|
|
next_due_date = models.DateField(null=True, blank=True)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_maintenance_schedules'
|
|
unique_together = ['asset', 'building', 'room']
|
|
indexes = [
|
|
models.Index(fields=['asset', 'building', 'room']),
|
|
]
|
|
ordering = ['next_due_date']
|
|
|
|
def __str__(self):
|
|
return f"{self.name} - {self.FrequencyInterval(self.frequency_interval).label}"
|
|
|
|
|
|
class Vendor(models.Model):
|
|
"""Service vendors and contractors"""
|
|
|
|
class VendorType(models.TextChoices):
|
|
MAINTENANCE = 'MAINTENANCE', 'Maintenance Contractor'
|
|
CLEANING = 'CLEANING', 'Cleaning Service'
|
|
SECURITY = 'SECURITY', 'Security Service'
|
|
LANDSCAPING = 'LANDSCAPING', 'Landscaping'
|
|
HVAC = 'HVAC', 'HVAC Service'
|
|
ELECTRICAL = 'ELECTRICAL', 'Electrical Service'
|
|
PLUMBING = 'PLUMBING', 'Plumbing Service'
|
|
IT = 'IT', 'IT Service'
|
|
OTHER = 'OTHER', 'Other'
|
|
|
|
tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE)
|
|
name = models.CharField(max_length=200)
|
|
vendor_type = models.CharField(max_length=20, choices=VendorType.choices)
|
|
contact_person = models.CharField(max_length=100)
|
|
email = models.EmailField()
|
|
phone = models.CharField(max_length=20)
|
|
address = models.TextField()
|
|
crn = models.CharField(max_length=10, verbose_name="Commercial Registration Number", null=True, blank=True)
|
|
vrn = models.CharField(max_length=15, verbose_name="VAT Registration Number", null=True, blank=True)
|
|
rating = models.DecimalField(
|
|
max_digits=3, decimal_places=2, null=True, blank=True,
|
|
validators=[MinValueValidator(Decimal('0.00')), MaxValueValidator(Decimal('5.00'))]
|
|
)
|
|
total_contracts = models.PositiveIntegerField(default=0)
|
|
|
|
is_active = models.BooleanField(default=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_vendors'
|
|
indexes = [
|
|
models.Index(fields=['tenant','name']),
|
|
]
|
|
ordering = ['name']
|
|
verbose_name_plural = 'Vendors'
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class ServiceContract(models.Model):
|
|
"""Service contracts with vendors"""
|
|
|
|
class ContractStatus(models.TextChoices):
|
|
DRAFT = 'DRAFT', 'Draft'
|
|
ACTIVE = 'ACTIVE', 'Active'
|
|
EXPIRED = 'EXPIRED', 'Expired'
|
|
TERMINATED = 'TERMINATED', 'Terminated'
|
|
|
|
contract_number = models.CharField(max_length=50, unique=True)
|
|
vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE)
|
|
title = models.CharField(max_length=200)
|
|
description = models.TextField()
|
|
|
|
# Contract terms
|
|
start_date = models.DateField()
|
|
end_date = models.DateField()
|
|
contract_value = models.DecimalField(max_digits=12, decimal_places=2)
|
|
payment_terms = models.CharField(max_length=100, help_text="e.g., Net 30 days")
|
|
|
|
# Scope
|
|
buildings = models.ManyToManyField(Building, blank=True)
|
|
service_areas = models.TextField(help_text="Description of service areas")
|
|
|
|
# Status
|
|
status = models.CharField(max_length=20, choices=ContractStatus.choices, default=ContractStatus.DRAFT)
|
|
auto_renewal = models.BooleanField(default=False)
|
|
renewal_notice_days = models.PositiveIntegerField(default=30)
|
|
|
|
# Management
|
|
contract_manager = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True)
|
|
|
|
notes = models.TextField(blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_service_contracts'
|
|
indexes = [
|
|
models.Index(fields=['vendor']),
|
|
]
|
|
ordering = ['-start_date']
|
|
verbose_name_plural = 'Service Contracts'
|
|
|
|
def __str__(self):
|
|
return f"{self.contract_number} - {self.vendor.name}"
|
|
|
|
@property
|
|
def is_expiring_soon(self):
|
|
from django.utils import timezone
|
|
if self.end_date:
|
|
days_until_expiry = (self.end_date - timezone.now().date()).days
|
|
return days_until_expiry <= self.renewal_notice_days
|
|
return False
|
|
|
|
|
|
class Inspection(models.Model):
|
|
"""Facility inspections"""
|
|
class InspectionType(models.TextChoices):
|
|
SAFETY = 'SAFETY', 'Safety Inspection'
|
|
FIRE = 'FIRE', 'Fire Safety'
|
|
HEALTH = 'HEALTH', 'Health Inspection'
|
|
SECURITY = 'SECURITY', 'Security Audit'
|
|
ENVIRONMENTAL = 'ENVIRONMENTAL', 'Environmental'
|
|
STRUCTURAL = 'STRUCTURAL', 'Structural'
|
|
ELECTRICAL = 'ELECTRICAL', 'Electrical'
|
|
HVAC = 'HVAC', 'HVAC'
|
|
OTHER = 'OTHER', 'Other'
|
|
|
|
class Status(models.TextChoices):
|
|
SCHEDULED = 'SCHEDULED', 'Scheduled'
|
|
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
|
|
COMPLETED = 'COMPLETED', 'Completed'
|
|
CANCELLED = 'CANCELLED', 'Cancelled'
|
|
|
|
inspection_id = models.CharField(max_length=50, unique=True)
|
|
inspection_type = models.CharField(max_length=20, choices=InspectionType.choices, help_text="Type of inspection")
|
|
title = models.CharField(max_length=200)
|
|
description = models.TextField(blank=True)
|
|
|
|
# Scope
|
|
building = models.ForeignKey(Building, on_delete=models.CASCADE)
|
|
floors = models.ManyToManyField(Floor, blank=True)
|
|
rooms = models.ManyToManyField(Room, blank=True)
|
|
assets = models.ManyToManyField(Asset, blank=True)
|
|
|
|
# Scheduling
|
|
scheduled_date = models.DateTimeField()
|
|
estimated_duration_hours = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
|
|
|
|
# Personnel
|
|
inspector = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='inspections_conducted')
|
|
inspector_external = models.CharField(max_length=100, blank=True, help_text="External inspector name")
|
|
inspector_organization = models.CharField(max_length=100, blank=True)
|
|
|
|
# Status and results
|
|
status = models.CharField(max_length=20, choices=Status.choices, default=Status.SCHEDULED, help_text="Current status of the inspection")
|
|
started_date = models.DateTimeField(null=True, blank=True)
|
|
completed_date = models.DateTimeField(null=True, blank=True)
|
|
|
|
# Results
|
|
overall_rating = models.CharField(max_length=20, blank=True, help_text="Pass/Fail/Conditional")
|
|
findings = models.TextField(blank=True)
|
|
recommendations = models.TextField(blank=True)
|
|
|
|
# Follow-up
|
|
requires_followup = models.BooleanField(default=False)
|
|
followup_date = models.DateField(null=True, blank=True)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_inspections'
|
|
verbose_name_plural = 'Inspections'
|
|
ordering = ['-scheduled_date']
|
|
|
|
def __str__(self):
|
|
return f"{self.inspection_id} - {self.title}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.inspection_id:
|
|
# Generate inspection ID
|
|
from django.utils import timezone
|
|
today = timezone.now().date()
|
|
daily_count = Inspection.objects.filter(created_at__date=today).count() + 1
|
|
self.inspection_id = f"INS-{today.strftime('%Y%m%d')}-{daily_count:04d}"
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class EnergyMeter(models.Model):
|
|
"""Energy consumption meters"""
|
|
class MeterType(models.TextChoices):
|
|
ELECTRICITY = 'ELECTRICITY', 'Electricity'
|
|
GAS = 'GAS', 'Natural Gas'
|
|
WATER = 'WATER', 'Water'
|
|
STEAM = 'STEAM', 'Steam'
|
|
CHILLED_WATER = 'CHILLED_WATER', 'Chilled Water'
|
|
OTHER = 'OTHER', 'Other'
|
|
|
|
meter_id = models.CharField(max_length=50, unique=True)
|
|
meter_type = models.CharField(max_length=20, choices=MeterType.choices, help_text="Meter type")
|
|
building = models.ForeignKey(Building, on_delete=models.CASCADE)
|
|
location_description = models.CharField(max_length=200)
|
|
|
|
# Meter details
|
|
manufacturer = models.CharField(max_length=100, blank=True)
|
|
model = models.CharField(max_length=100, blank=True)
|
|
serial_number = models.CharField(max_length=100, blank=True)
|
|
installation_date = models.DateField(null=True, blank=True)
|
|
|
|
# Current reading
|
|
current_reading = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
|
|
last_reading_date = models.DateTimeField(null=True, blank=True)
|
|
|
|
# Status
|
|
is_active = models.BooleanField(default=True)
|
|
calibration_date = models.DateField(null=True, blank=True)
|
|
next_calibration_date = models.DateField(null=True, blank=True)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_energy_meters'
|
|
ordering = ['building', 'meter_type', 'meter_id']
|
|
verbose_name_plural = 'Energy Meters'
|
|
|
|
def __str__(self):
|
|
return f"{self.meter_id} - {self.MeterType(self.meter_type).label}"
|
|
|
|
|
|
class EnergyReading(models.Model):
|
|
"""Energy meter readings"""
|
|
meter = models.ForeignKey(EnergyMeter, on_delete=models.CASCADE, related_name='readings')
|
|
reading_date = models.DateTimeField()
|
|
reading_value = models.DecimalField(max_digits=12, decimal_places=2)
|
|
consumption = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
cost = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
|
|
|
# Reading details
|
|
read_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
is_estimated = models.BooleanField(default=False)
|
|
notes = models.TextField(blank=True)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
ordering = ['-reading_date']
|
|
unique_together = ['meter', 'reading_date']
|
|
|
|
def __str__(self):
|
|
return f"{self.meter.meter_id} - {self.reading_date.strftime('%Y-%m-%d')}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
# Calculate consumption from previous reading
|
|
if not self.consumption:
|
|
previous_reading = EnergyReading.objects.filter(
|
|
meter=self.meter,
|
|
reading_date__lt=self.reading_date
|
|
).order_by('-reading_date').first()
|
|
|
|
if previous_reading:
|
|
self.consumption = self.reading_value - previous_reading.reading_value
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
# Update meter's current reading
|
|
self.meter.current_reading = self.reading_value
|
|
self.meter.last_reading_date = self.reading_date
|
|
self.meter.save()
|
|
|
|
|
|
class SpaceReservation(models.Model):
|
|
"""Room and space reservations"""
|
|
|
|
class ReservationStatus(models.TextChoices):
|
|
PENDING = 'PENDING', 'Pending'
|
|
CONFIRMED = 'CONFIRMED', 'Confirmed'
|
|
CANCELLED = 'CANCELLED', 'Cancelled'
|
|
COMPLETED = 'COMPLETED', 'Completed'
|
|
NO_SHOW = 'NO_SHOW', 'No Show'
|
|
|
|
reservation_id = models.CharField(max_length=50, unique=True)
|
|
room = models.ForeignKey(Room, on_delete=models.CASCADE)
|
|
|
|
# Reservation details
|
|
title = models.CharField(max_length=200)
|
|
description = models.TextField(blank=True)
|
|
start_datetime = models.DateTimeField()
|
|
end_datetime = models.DateTimeField()
|
|
|
|
# Requester information
|
|
reserved_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
contact_person = models.CharField(max_length=100, blank=True)
|
|
contact_email = models.EmailField(blank=True)
|
|
contact_phone = models.CharField(max_length=20, blank=True)
|
|
|
|
# Event details
|
|
expected_attendees = models.PositiveIntegerField(null=True, blank=True)
|
|
setup_requirements = models.TextField(blank=True)
|
|
catering_required = models.BooleanField(default=False)
|
|
av_equipment_required = models.BooleanField(default=False, help_text='Whether audio/visual equipment (projector, microphone, etc.) is needed')
|
|
|
|
# Status and approval
|
|
status = models.CharField(max_length=20, choices=ReservationStatus.choices, default=ReservationStatus.PENDING)
|
|
approved_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='approved_reservations')
|
|
approved_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
# Billing
|
|
hourly_rate = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)
|
|
total_cost = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
|
|
|
notes = models.TextField(blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'facility_management_space_reservations'
|
|
ordering = ['-start_datetime']
|
|
verbose_name_plural = 'Space Reservations'
|
|
|
|
def __str__(self):
|
|
return f"{self.reservation_id} - {self.title}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.reservation_id:
|
|
# Generate reservation ID
|
|
from django.utils import timezone
|
|
today = timezone.now().date()
|
|
daily_count = SpaceReservation.objects.filter(created_at__date=today).count() + 1
|
|
self.reservation_id = f"RES-{today.strftime('%Y%m%d')}-{daily_count:04d}"
|
|
|
|
# Calculate total cost
|
|
if self.hourly_rate and self.start_datetime and self.end_datetime:
|
|
duration_hours = (self.end_datetime - self.start_datetime).total_seconds() / 3600
|
|
self.total_cost = self.hourly_rate * Decimal(str(duration_hours))
|
|
|
|
super().save(*args, **kwargs)
|
|
|