Coverage for modules\logistics\models.py: 96%

179 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-06 16:58 +0800

1from django.db import models 

2from core.models import TenantScopedModel 

3 

4 

5class Warehouse(TenantScopedModel): 

6 """Warehousing facilities for storing goods.""" 

7 WAREHOUSE_TYPE_CHOICES = [ 

8 ('STORAGE', 'Storage'), 

9 ('DISTRIBUTION', 'Distribution'), 

10 ('FULFILLMENT', 'Fulfillment'), 

11 ('COLD_STORAGE', 'Cold Storage'), 

12 ('BONDED', 'Bonded'), 

13 ] 

14 name = models.CharField(max_length=255) 

15 warehouse_type = models.CharField(max_length=50, choices=WAREHOUSE_TYPE_CHOICES, default='STORAGE') 

16 address = models.TextField(blank=True) 

17 city = models.CharField(max_length=100, blank=True) 

18 country = models.CharField(max_length=100, blank=True) 

19 contact_name = models.CharField(max_length=255, blank=True) 

20 contact_phone = models.CharField(max_length=50, blank=True) 

21 contact_email = models.EmailField(blank=True) 

22 capacity_cubic_meters = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) 

23 capacity_tons = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

24 is_active = models.BooleanField(default=True) 

25 notes = models.TextField(blank=True) 

26 created_at = models.DateTimeField(auto_now_add=True) 

27 updated_at = models.DateTimeField(auto_now=True) 

28 

29 class Meta: 

30 ordering = ['-created_at'] 

31 

32 def __str__(self): 

33 return self.name 

34 

35 

36class WarehouseReceipt(TenantScopedModel): 

37 """Warehouse receipt for goods stored in a warehouse.""" 

38 RECEIPT_STATUS_CHOICES = [ 

39 ('PENDING', 'Pending'), 

40 ('RECEIVED', 'Received'), 

41 ('IN_STORAGE', 'In Storage'), 

42 ('RELEASED', 'Released'), 

43 ('CANCELLED', 'Cancelled'), 

44 ] 

45 receipt_number = models.CharField(max_length=50, unique=True) 

46 warehouse = models.ForeignKey(Warehouse, on_delete=models.SET_NULL, null=True, related_name='receipts') 

47 client_name = models.CharField(max_length=255) 

48 client_reference = models.CharField(max_length=100, blank=True) 

49 receipt_date = models.DateField() 

50 arrival_date = models.DateField(null=True, blank=True) 

51 release_date = models.DateField(null=True, blank=True) 

52 status = models.CharField(max_length=20, choices=RECEIPT_STATUS_CHOICES, default='PENDING') 

53 goods_description = models.TextField(blank=True) 

54 number_of_packages = models.IntegerField(default=0) 

55 gross_weight_kg = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

56 volume_cubic_meters = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

57 storage_days = models.IntegerField(default=0) 

58 handling_fee = models.DecimalField(max_digits=10, decimal_places=2, default=0) 

59 storage_fee = models.DecimalField(max_digits=10, decimal_places=2, default=0) 

60 total_fee = models.DecimalField(max_digits=10, decimal_places=2, default=0) 

61 notes = models.TextField(blank=True) 

62 created_at = models.DateTimeField(auto_now_add=True) 

63 updated_at = models.DateTimeField(auto_now=True) 

64 

65 class Meta: 

66 ordering = ['-created_at'] 

67 

68 def __str__(self): 

69 return self.receipt_number 

70 

71 

72class ShippingQuote(TenantScopedModel): 

73 """Quotes for shipping services.""" 

74 QUOTE_STATUS_CHOICES = [ 

75 ('DRAFT', 'Draft'), 

76 ('SENT', 'Sent'), 

77 ('ACCEPTED', 'Accepted'), 

78 ('REJECTED', 'Rejected'), 

79 ('EXPIRED', 'Expired'), 

80 ] 

81 SHIPMENT_TYPE_CHOICES = [ 

82 ('FCL', 'Full Container Load'), 

83 ('LCL', 'Less than Container Load'), 

84 ('AIR', 'Air Freight'), 

85 ('ROAD', 'Road Freight'), 

86 ('RAIL', 'Rail Freight'), 

87 ] 

88 quote_number = models.CharField(max_length=50, unique=True) 

89 client_name = models.CharField(max_length=255) 

90 client_email = models.EmailField(blank=True) 

91 client_phone = models.CharField(max_length=50, blank=True) 

92 shipment_type = models.CharField(max_length=20, choices=SHIPMENT_TYPE_CHOICES) 

93 origin = models.CharField(max_length=255) 

94 destination = models.CharField(max_length=255) 

95 cargo_description = models.TextField(blank=True) 

96 weight_kg = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

97 volume_cbm = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

98 number_of_packages = models.IntegerField(default=0) 

99 estimated_departure = models.DateField(null=True, blank=True) 

100 estimated_arrival = models.DateField(null=True, blank=True) 

101 currency = models.CharField(max_length=3, default='USD') 

102 freight_cost = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

103 insurance_cost = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

104 other_charges = models.DecimalField(max_digits=10, decimal_places=2, default=0) 

105 total_amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

106 valid_until = models.DateField(null=True, blank=True) 

107 status = models.CharField(max_length=20, choices=QUOTE_STATUS_CHOICES, default='DRAFT') 

108 notes = models.TextField(blank=True) 

109 created_at = models.DateTimeField(auto_now_add=True) 

110 updated_at = models.DateTimeField(auto_now=True) 

111 

112 class Meta: 

113 ordering = ['-created_at'] 

114 

115 def __str__(self): 

116 return self.quote_number 

117 

118 

119class FreightQuote(TenantScopedModel): 

120 """Freight forwarding quotes.""" 

121 QUOTE_STATUS_CHOICES = [ 

122 ('DRAFT', 'Draft'), 

123 ('QUOTED', 'Quoted'), 

124 ('NEGOTIATING', 'Negotiating'), 

125 ('CONFIRMED', 'Confirmed'), 

126 ('CANCELLED', 'Cancelled'), 

127 ] 

128 SERVICE_TYPE_CHOICES = [ 

129 ('IMPORT', 'Import'), 

130 ('EXPORT', 'Export'), 

131 ('TRANSIT', 'Transit'), 

132 ('DOOR_TO_DOOR', 'Door to Door'), 

133 ('PORT_TO_PORT', 'Port to Port'), 

134 ] 

135 quote_number = models.CharField(max_length=50, unique=True) 

136 client_name = models.CharField(max_length=255) 

137 client_email = models.EmailField(blank=True) 

138 carrier = models.CharField(max_length=255, blank=True) 

139 service_type = models.CharField(max_length=20, choices=SERVICE_TYPE_CHOICES) 

140 origin_port = models.CharField(max_length=255) 

141 destination_port = models.CharField(max_length=255) 

142 vessel_name = models.CharField(max_length=255, blank=True) 

143 voyage_number = models.CharField(max_length=50, blank=True) 

144 etd = models.DateField(null=True, blank=True) 

145 eta = models.DateField(null=True, blank=True) 

146 container_type = models.CharField(max_length=50, blank=True) 

147 number_of_containers = models.IntegerField(default=0) 

148 cargo_description = models.TextField(blank=True) 

149 weight_kg = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

150 volume_cbm = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

151 currency = models.CharField(max_length=3, default='USD') 

152 ocean_freight = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

153 thc_origin = models.DecimalField(max_digits=10, decimal_places=2, default=0) 

154 thc_destination = models.DecimalField(max_digits=10, decimal_places=2, default=0) 

155 clearance_cost = models.DecimalField(max_digits=10, decimal_places=2, default=0) 

156 delivery_cost = models.DecimalField(max_digits=10, decimal_places=2, default=0) 

157 total_quote = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

158 valid_until = models.DateField(null=True, blank=True) 

159 status = models.CharField(max_length=20, choices=QUOTE_STATUS_CHOICES, default='DRAFT') 

160 notes = models.TextField(blank=True) 

161 created_at = models.DateTimeField(auto_now_add=True) 

162 updated_at = models.DateTimeField(auto_now=True) 

163 

164 class Meta: 

165 ordering = ['-created_at'] 

166 

167 def __str__(self): 

168 return self.quote_number 

169 

170 

171class TransportationRate(TenantScopedModel): 

172 """Transportation rates by route.""" 

173 RATE_TYPE_CHOICES = [ 

174 ('PER_KG', 'Per Kilogram'), 

175 ('PER_CBM', 'Per Cubic Meter'), 

176 ('FLAT_RATE', 'Flat Rate'), 

177 ('CONTAINER_RATE', 'Container Rate'), 

178 ] 

179 MODE_CHOICES = [ 

180 ('SEA', 'Sea'), 

181 ('AIR', 'Air'), 

182 ('ROAD', 'Road'), 

183 ('RAIL', 'Rail'), 

184 ('MULTIMODAL', 'Multimodal'), 

185 ] 

186 rate_name = models.CharField(max_length=255) 

187 rate_type = models.CharField(max_length=20, choices=RATE_TYPE_CHOICES) 

188 mode = models.CharField(max_length=20, choices=MODE_CHOICES) 

189 origin = models.CharField(max_length=255) 

190 destination = models.CharField(max_length=255) 

191 carrier = models.CharField(max_length=255, blank=True) 

192 transit_days = models.IntegerField(default=0) 

193 currency = models.CharField(max_length=3, default='USD') 

194 rate_value = models.DecimalField(max_digits=10, decimal_places=2) 

195 minimum_charge = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

196 fuel_surcharge = models.DecimalField(max_digits=10, decimal_places=2, default=0) 

197 effective_from = models.DateField() 

198 effective_to = models.DateField(null=True, blank=True) 

199 is_active = models.BooleanField(default=True) 

200 notes = models.TextField(blank=True) 

201 created_at = models.DateTimeField(auto_now_add=True) 

202 updated_at = models.DateTimeField(auto_now=True) 

203 

204 class Meta: 

205 ordering = ['-created_at'] 

206 

207 def __str__(self): 

208 return f"{self.rate_name} - {self.origin} to {self.destination}" 

209 

210 

211class Booking(TenantScopedModel): 

212 """Booking records for shipments.""" 

213 BOOKING_STATUS_CHOICES = [ 

214 ('PENDING', 'Pending'), 

215 ('CONFIRMED', 'Confirmed'), 

216 ('LOADED', 'Loaded'), 

217 ('IN_TRANSIT', 'In Transit'), 

218 ('DELIVERED', 'Delivered'), 

219 ('CANCELLED', 'Cancelled'), 

220 ] 

221 BOOKING_TYPE_CHOICES = [ 

222 ('SHIPMENT', 'Shipment'), 

223 ('WAREHOUSE', 'Warehouse'), 

224 ('FREIGHT', 'Freight'), 

225 ('TRANSPORT', 'Transport'), 

226 ] 

227 booking_number = models.CharField(max_length=50, unique=True) 

228 booking_type = models.CharField(max_length=20, choices=BOOKING_TYPE_CHOICES) 

229 client_name = models.CharField(max_length=255) 

230 client_email = models.EmailField(blank=True) 

231 client_phone = models.CharField(max_length=50, blank=True) 

232 origin = models.CharField(max_length=255) 

233 destination = models.CharField(max_length=255) 

234 booking_date = models.DateField() 

235 scheduled_date = models.DateField(null=True, blank=True) 

236 completed_date = models.DateField(null=True, blank=True) 

237 status = models.CharField(max_length=20, choices=BOOKING_STATUS_CHOICES, default='PENDING') 

238 cargo_description = models.TextField(blank=True) 

239 weight_kg = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

240 volume_cbm = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

241 number_of_packages = models.IntegerField(default=0) 

242 vessel_name = models.CharField(max_length=255, blank=True) 

243 voyage_number = models.CharField(max_length=50, blank=True) 

244 container_number = models.CharField(max_length=50, blank=True) 

245 seal_number = models.CharField(max_length=50, blank=True) 

246 total_amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) 

247 notes = models.TextField(blank=True) 

248 created_at = models.DateTimeField(auto_now_add=True) 

249 updated_at = models.DateTimeField(auto_now=True) 

250 

251 class Meta: 

252 ordering = ['-created_at'] 

253 

254 def __str__(self): 

255 return self.booking_number 

256 

257 

258class TrackingEvent(TenantScopedModel): 

259 """Tracking events for shipments.""" 

260 EVENT_TYPE_CHOICES = [ 

261 ('PICKUP', 'Pickup'), 

262 ('IN_TRANSIT', 'In Transit'), 

263 ('ARRIVED', 'Arrived'), 

264 ('CUSTOMS', 'Customs'), 

265 ('OUT_FOR_DELIVERY', 'Out for Delivery'), 

266 ('DELIVERED', 'Delivered'), 

267 ('EXCEPTION', 'Exception'), 

268 ('RETURNED', 'Returned'), 

269 ] 

270 booking = models.ForeignKey(Booking, on_delete=models.CASCADE, null=True, blank=True, related_name='tracking_events') 

271 shipment_reference = models.CharField(max_length=100, db_index=True) 

272 event_type = models.CharField(max_length=20, choices=EVENT_TYPE_CHOICES) 

273 location = models.CharField(max_length=255) 

274 event_date = models.DateTimeField() 

275 description = models.TextField(blank=True) 

276 created_at = models.DateTimeField(auto_now_add=True) 

277 

278 class Meta: 

279 ordering = ['-event_date'] 

280 

281 def __str__(self): 

282 return f"{self.shipment_reference} - {self.event_type} at {self.location}"