from decimal import Decimal from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils import timezone class Trip(models.Model): class TripType(models.TextChoices): BUSINESS = "business", "Business" PERSONAL = "personal", "Personal" COMMUTING = "commuting", "Commuting" REPAIR = "repair", "Repair / Maintenance" class DistanceSource(models.TextChoices): ODOMETER = "odometer", "Odometer" MAP = "map", "Google Maps" date = models.DateField() start_time = models.TimeField() end_time = models.TimeField() start_location = models.CharField(max_length=255) end_location = models.CharField(max_length=255) business_purpose = models.TextField() trip_type = models.CharField(max_length=20, choices=TripType.choices) start_odometer = models.DecimalField(max_digits=10, decimal_places=1, null=True, blank=True) end_odometer = models.DecimalField(max_digits=10, decimal_places=1, null=True, blank=True) distance_miles = models.DecimalField(max_digits=8, decimal_places=1, null=True, blank=True) distance_source = models.CharField(max_length=20, choices=DistanceSource.choices, default=DistanceSource.MAP) notes = models.TextField(blank=True) created_at = models.DateTimeField(default=timezone.now) updated_at = models.DateTimeField(default=timezone.now) class Meta: ordering = ["-date", "-start_time", "-created_at"] def __str__(self): return f"{self.date:%Y-%m-%d} · {self.start_location} → {self.end_location}" def get_absolute_url(self): return reverse("trip_detail", args=[self.pk]) @property def miles_display(self): return self.distance_miles or Decimal("0.0") def clean(self): errors = {} if self.end_time and self.start_time and self.end_time < self.start_time: errors["end_time"] = "End time must be after the start time." if self.start_odometer is not None and self.end_odometer is not None: if self.end_odometer < self.start_odometer: errors["end_odometer"] = "Ending odometer must be greater than or equal to starting odometer." else: self.distance_miles = self.end_odometer - self.start_odometer self.distance_source = self.DistanceSource.ODOMETER elif self.distance_miles is not None: if self.distance_miles < 0: errors["distance_miles"] = "Distance must be zero or greater." else: self.distance_source = self.DistanceSource.MAP else: errors["distance_miles"] = "Add odometer readings or calculate mileage from Google Maps." if errors: raise ValidationError(errors) def save(self, *args, **kwargs): self.full_clean() super().save(*args, **kwargs)