from django.db import models, transaction from django.core.validators import MinValueValidator, MaxValueValidator from decimal import Decimal import uuid from django.conf import settings from django.db.models import F from django.utils.timezone import localdate class DailyCounter(models.Model): date = models.DateField() name = models.CharField(max_length=50) # e.g., "inspection" / "maintenance" value = models.PositiveIntegerField(default=0) class Meta: unique_together = ('date', 'name') @classmethod def next_for_today(cls, name: str) -> int: with transaction.atomic(): obj, _ = cls.objects.select_for_update().get_or_create( date=localdate(), name=name, defaults={'value': 0}, ) obj.value = F('value') + 1 obj.save(update_fields=['value']) obj.refresh_from_db(fields=['value']) return obj.value 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 _make_request_id(self): n = DailyCounter.next_for_today(name="maintenance") return f"MR-{localdate():%Y%m%d}-{n:04d}" def save(self, *args, **kwargs): if not self.pk and not self.request_id: # attempt-save loop to be extra safe against any leftover collisions for _ in range(3): self.request_id = self._make_request_id() try: return super().save(*args, **kwargs) except IntegrityError: # extremely rare: loop and try next counter value continue # if we got here, something else is wrong raise return 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 _make_inspection_id(self) -> str: n = DailyCounter.next_for_today(name="inspection") return f"INS-{localdate():%Y%m%d}-{n:04d}" def save(self, *args, **kwargs): # Only generate on first save if not self.pk and not self.inspection_id: for _ in range(3): # tiny retry loop in case of extremely rare collisions self.inspection_id = self._make_inspection_id() try: return super().save(*args, **kwargs) except IntegrityError: # try next counter value continue # if still failing, surface the error raise return 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.UUIDField(default=uuid.uuid4, editable=False, 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)