Marwan Alwali 4d06ca4b5e update
2025-09-20 14:26:19 +03:00

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