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
« 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
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)
29 class Meta:
30 ordering = ['-created_at']
32 def __str__(self):
33 return self.name
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)
65 class Meta:
66 ordering = ['-created_at']
68 def __str__(self):
69 return self.receipt_number
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)
112 class Meta:
113 ordering = ['-created_at']
115 def __str__(self):
116 return self.quote_number
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)
164 class Meta:
165 ordering = ['-created_at']
167 def __str__(self):
168 return self.quote_number
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)
204 class Meta:
205 ordering = ['-created_at']
207 def __str__(self):
208 return f"{self.rate_name} - {self.origin} to {self.destination}"
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)
251 class Meta:
252 ordering = ['-created_at']
254 def __str__(self):
255 return self.booking_number
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)
278 class Meta:
279 ordering = ['-event_date']
281 def __str__(self):
282 return f"{self.shipment_reference} - {self.event_type} at {self.location}"