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

1from django.db import models 

2from decimal import Decimal 

3from core.models import TenantScopedModel 

4 

5from .delivery import Delivery 

6 

7__all__ = [ 

8 "Vehicle", 

9 "Driver", 

10 "FuelCard", 

11 "FuelLog", 

12 "MaintenanceLog", 

13 "Trip", 

14 "Delivery", 

15] 

16 

17 

18class Vehicle(TenantScopedModel): 

19 """Truck or trailer vehicle in the fleet.""" 

20 

21 VEHICLE_TYPES = [ 

22 ("TRUCK", "Box Truck"), 

23 ("SEMI", "Semi Truck"), 

24 ("TRAILER", "Trailer"), 

25 ("VAN", "Cargo Van"), 

26 ("FLATBED", "Flatbed"), 

27 ] 

28 

29 STATUS_CHOICES = [ 

30 ("ACTIVE", "Active"), 

31 ("MAINTENANCE", "Under Maintenance"), 

32 ("RETIRED", "Retired"), 

33 ("OUT_OF_SERVICE", "Out of Service"), 

34 ] 

35 

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) 

48 

49 class Meta: 

50 ordering = ["-created_at"] 

51 

52 def __str__(self): 

53 return f"{self.plate_number} ({self.get_vehicle_type_display()})" 

54 

55 

56class Driver(TenantScopedModel): 

57 """Fleet driver.""" 

58 

59 STATUS_CHOICES = [ 

60 ("ACTIVE", "Active"), 

61 ("ON_LEAVE", "On Leave"), 

62 ("TERMINATED", "Terminated"), 

63 ] 

64 

65 LICENSE_TYPES = [ 

66 ("A", "Class A CDL"), 

67 ("B", "Class B CDL"), 

68 ("C", "Class C CDL"), 

69 ] 

70 

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) 

85 

86 class Meta: 

87 ordering = ["last_name", "first_name"] 

88 

89 @property 

90 def full_name(self): 

91 return f"{self.first_name} {self.last_name}".strip() 

92 

93 def __str__(self): 

94 return self.full_name 

95 

96 

97class FuelCard(TenantScopedModel): 

98 """Fuel card linked to a vehicle or driver.""" 

99 

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) 

111 

112 def __str__(self): 

113 return f"Fuel Card {self.card_number}" 

114 

115 

116class FuelLog(TenantScopedModel): 

117 """Fuel purchase log entry.""" 

118 

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) 

135 

136 class Meta: 

137 ordering = ["-created_at"] 

138 

139 def __str__(self): 

140 return f"Fuel log {self.id} - {self.vehicle}" 

141 

142 

143class MaintenanceLog(TenantScopedModel): 

144 """Vehicle maintenance and repair log.""" 

145 

146 TYPE_CHOICES = [ 

147 ("SCHEDULED", "Scheduled Service"), 

148 ("REPAIR", "Repair"), 

149 ("INSPECTION", "Inspection"), 

150 ("TIRE", "Tire Service"), 

151 ("OTHER", "Other"), 

152 ] 

153 

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) 

166 

167 class Meta: 

168 ordering = ["-created_at"] 

169 

170 def __str__(self): 

171 return f"Maintenance {self.id} - {self.vehicle}" 

172 

173 

174class Trip(TenantScopedModel): 

175 """A trip made by a driver with a vehicle.""" 

176 

177 STATUS_CHOICES = [ 

178 ("PLANNED", "Planned"), 

179 ("IN_PROGRESS", "In Progress"), 

180 ("COMPLETED", "Completed"), 

181 ("CANCELLED", "Cancelled"), 

182 ] 

183 

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) 

204 

205 class Meta: 

206 ordering = ["-created_at"] 

207 

208 @property 

209 def total_cost(self): 

210 return self.fuel_cost + self.tolls_cost + self.other_costs 

211 

212 def __str__(self): 

213 return f"Trip {self.id}: {self.origin} → {self.destination}"