Coverage for modules\trucking\models\__init__.py: 93%
114 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 16:58 +0800
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 16:58 +0800
1from django.db import models
2from decimal import Decimal
3from core.models import TenantScopedModel
5from .delivery import Delivery
7__all__ = [
8 "Vehicle",
9 "Driver",
10 "FuelCard",
11 "FuelLog",
12 "MaintenanceLog",
13 "Trip",
14 "Delivery",
15]
18class Vehicle(TenantScopedModel):
19 """Truck or trailer vehicle in the fleet."""
21 VEHICLE_TYPES = [
22 ("TRUCK", "Box Truck"),
23 ("SEMI", "Semi Truck"),
24 ("TRAILER", "Trailer"),
25 ("VAN", "Cargo Van"),
26 ("FLATBED", "Flatbed"),
27 ]
29 STATUS_CHOICES = [
30 ("ACTIVE", "Active"),
31 ("MAINTENANCE", "Under Maintenance"),
32 ("RETIRED", "Retired"),
33 ("OUT_OF_SERVICE", "Out of Service"),
34 ]
36 plate_number = models.CharField(max_length=32, unique=True, db_index=True)
37 vin = models.CharField(max_length=64, blank=True, null=True)
38 vehicle_type = models.CharField(max_length=16, choices=VEHICLE_TYPES, default="TRUCK")
39 make = models.CharField(max_length=64, blank=True, null=True)
40 model = models.CharField(max_length=64, blank=True, null=True)
41 year = models.IntegerField(null=True, blank=True)
42 capacity_tons = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal("0.00"))
43 fuel_type = models.CharField(max_length=16, blank=True, null=True) # diesel, gasoline, electric
44 status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="ACTIVE")
45 current_odometer = models.IntegerField(default=0)
46 image_url = models.URLField(blank=True, null=True)
47 notes = models.TextField(blank=True, null=True)
49 class Meta:
50 ordering = ["-created_at"]
52 def __str__(self):
53 return f"{self.plate_number} ({self.get_vehicle_type_display()})"
56class Driver(TenantScopedModel):
57 """Fleet driver."""
59 STATUS_CHOICES = [
60 ("ACTIVE", "Active"),
61 ("ON_LEAVE", "On Leave"),
62 ("TERMINATED", "Terminated"),
63 ]
65 LICENSE_TYPES = [
66 ("A", "Class A CDL"),
67 ("B", "Class B CDL"),
68 ("C", "Class C CDL"),
69 ]
71 first_name = models.CharField(max_length=64)
72 last_name = models.CharField(max_length=64)
73 email = models.EmailField(blank=True, null=True)
74 phone = models.CharField(max_length=32, blank=True, null=True)
75 license_number = models.CharField(max_length=64, blank=True, null=True)
76 license_type = models.CharField(max_length=1, choices=LICENSE_TYPES, blank=True, null=True)
77 license_expiry = models.DateField(null=True, blank=True)
78 date_of_birth = models.DateField(null=True, blank=True)
79 hire_date = models.DateField(null=True, blank=True)
80 status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="ACTIVE")
81 address = models.TextField(blank=True, null=True)
82 emergency_contact = models.CharField(max_length=128, blank=True, null=True)
83 emergency_phone = models.CharField(max_length=32, blank=True, null=True)
84 notes = models.TextField(blank=True, null=True)
86 class Meta:
87 ordering = ["last_name", "first_name"]
89 @property
90 def full_name(self):
91 return f"{self.first_name} {self.last_name}".strip()
93 def __str__(self):
94 return self.full_name
97class FuelCard(TenantScopedModel):
98 """Fuel card linked to a vehicle or driver."""
100 card_number = models.CharField(max_length=32, unique=True)
101 vehicle = models.ForeignKey(
102 Vehicle, on_delete=models.SET_NULL, null=True, blank=True, related_name="fuel_cards"
103 )
104 driver = models.ForeignKey(
105 Driver, on_delete=models.SET_NULL, null=True, blank=True, related_name="fuel_cards"
106 )
107 pin = models.CharField(max_length=8, blank=True, null=True)
108 daily_limit = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal("0.00"))
109 is_active = models.BooleanField(default=True)
110 expiry_date = models.DateField(null=True, blank=True)
112 def __str__(self):
113 return f"Fuel Card {self.card_number}"
116class FuelLog(TenantScopedModel):
117 """Fuel purchase log entry."""
119 vehicle = models.ForeignKey(
120 Vehicle, on_delete=models.CASCADE, related_name="fuel_logs"
121 )
122 driver = models.ForeignKey(
123 Driver, on_delete=models.SET_NULL, null=True, blank=True, related_name="fuel_logs"
124 )
125 fuel_card = models.ForeignKey(
126 FuelCard, on_delete=models.SET_NULL, null=True, blank=True, related_name="logs"
127 )
128 odometer = models.IntegerField()
129 liters = models.DecimalField(max_digits=10, decimal_places=2)
130 price_per_liter = models.DecimalField(max_digits=10, decimal_places=2)
131 total_cost = models.DecimalField(max_digits=10, decimal_places=2)
132 location = models.CharField(max_length=255, blank=True, null=True)
133 invoice_number = models.CharField(max_length=64, blank=True, null=True)
134 notes = models.TextField(blank=True, null=True)
136 class Meta:
137 ordering = ["-created_at"]
139 def __str__(self):
140 return f"Fuel log {self.id} - {self.vehicle}"
143class MaintenanceLog(TenantScopedModel):
144 """Vehicle maintenance and repair log."""
146 TYPE_CHOICES = [
147 ("SCHEDULED", "Scheduled Service"),
148 ("REPAIR", "Repair"),
149 ("INSPECTION", "Inspection"),
150 ("TIRE", "Tire Service"),
151 ("OTHER", "Other"),
152 ]
154 vehicle = models.ForeignKey(
155 Vehicle, on_delete=models.CASCADE, related_name="maintenance_logs"
156 )
157 maintenance_type = models.CharField(max_length=16, choices=TYPE_CHOICES, default="SCHEDULED")
158 description = models.TextField()
159 cost = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal("0.00"))
160 vendor = models.CharField(max_length=255, blank=True, null=True)
161 invoice_number = models.CharField(max_length=64, blank=True, null=True)
162 next_service_odometer = models.IntegerField(null=True, blank=True)
163 next_service_date = models.DateField(null=True, blank=True)
164 completed_at = models.DateTimeField(null=True, blank=True)
165 is_completed = models.BooleanField(default=False)
167 class Meta:
168 ordering = ["-created_at"]
170 def __str__(self):
171 return f"Maintenance {self.id} - {self.vehicle}"
174class Trip(TenantScopedModel):
175 """A trip made by a driver with a vehicle."""
177 STATUS_CHOICES = [
178 ("PLANNED", "Planned"),
179 ("IN_PROGRESS", "In Progress"),
180 ("COMPLETED", "Completed"),
181 ("CANCELLED", "Cancelled"),
182 ]
184 vehicle = models.ForeignKey(
185 Vehicle, on_delete=models.CASCADE, related_name="trips"
186 )
187 driver = models.ForeignKey(
188 Driver, on_delete=models.CASCADE, related_name="trips"
189 )
190 origin = models.CharField(max_length=255)
191 destination = models.CharField(max_length=255)
192 distance_km = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal("0.00"))
193 cargo_description = models.TextField(blank=True, null=True)
194 cargo_weight_tons = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal("0.00"))
195 status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="PLANNED")
196 planned_departure = models.DateTimeField(null=True, blank=True)
197 actual_departure = models.DateTimeField(null=True, blank=True)
198 planned_arrival = models.DateTimeField(null=True, blank=True)
199 actual_arrival = models.DateTimeField(null=True, blank=True)
200 fuel_cost = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal("0.00"))
201 tolls_cost = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal("0.00"))
202 other_costs = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal("0.00"))
203 notes = models.TextField(blank=True, null=True)
205 class Meta:
206 ordering = ["-created_at"]
208 @property
209 def total_cost(self):
210 return self.fuel_cost + self.tolls_cost + self.other_costs
212 def __str__(self):
213 return f"Trip {self.id}: {self.origin} → {self.destination}"