Coverage for modules\logistics\api.py: 34%
288 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 17:01 +0800
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 17:01 +0800
1from ninja import Router
2from typing import List, Optional
3from uuid import UUID
4from core.auth import JWTAuth
5from core.permissions import require_tenant, require_admin
6from core.exceptions import NotFoundException
7from .schemas import (
8 WarehouseIn, WarehouseOut,
9 WarehouseReceiptIn, WarehouseReceiptOut,
10 ShippingQuoteIn, ShippingQuoteOut,
11 FreightQuoteIn, FreightQuoteOut,
12 TransportationRateIn, TransportationRateOut,
13 BookingIn, BookingOut,
14 TrackingEventIn, TrackingEventOut,
15)
16from .models import (
17 Warehouse, WarehouseReceipt,
18 ShippingQuote, FreightQuote,
19 TransportationRate, Booking, TrackingEvent,
20)
22router = Router(tags=["Logistics"])
25# ── Warehouses ──────────────────────────────────────────────────────────────────
26@router.get("/warehouses", auth=JWTAuth(), response={200: List[WarehouseOut]})
27def list_warehouses(request, is_active: Optional[bool] = None):
28 tenant_id = require_tenant(request)
29 qs = Warehouse.objects.filter(tenant_id=tenant_id)
30 if is_active is not None:
31 qs = qs.filter(is_active=is_active)
32 return list(qs)
35@router.post("/warehouses", auth=JWTAuth(), response={201: WarehouseOut})
36@require_admin
37def create_warehouse(request, payload: WarehouseIn):
38 tenant_id = require_tenant(request)
39 warehouse = Warehouse.objects.create(tenant_id=tenant_id, **payload.dict())
40 return 201, warehouse
43@router.get("/warehouses/{warehouse_id}", auth=JWTAuth(), response={200: WarehouseOut})
44def get_warehouse(request, warehouse_id: UUID):
45 tenant_id = require_tenant(request)
46 warehouse = Warehouse.objects.filter(id=warehouse_id, tenant_id=tenant_id).first()
47 if not warehouse:
48 raise NotFoundException("Warehouse not found.")
49 return warehouse
52@router.put("/warehouses/{warehouse_id}", auth=JWTAuth(), response={200: WarehouseOut})
53@require_admin
54def update_warehouse(request, warehouse_id: UUID, payload: WarehouseIn):
55 tenant_id = require_tenant(request)
56 warehouse = Warehouse.objects.filter(id=warehouse_id, tenant_id=tenant_id).first()
57 if not warehouse:
58 raise NotFoundException("Warehouse not found.")
59 for key, value in payload.dict().items():
60 setattr(warehouse, key, value)
61 warehouse.save()
62 return warehouse
65@router.delete("/warehouses/{warehouse_id}", auth=JWTAuth())
66@require_admin
67def delete_warehouse(request, warehouse_id: UUID):
68 tenant_id = require_tenant(request)
69 warehouse = Warehouse.objects.filter(id=warehouse_id, tenant_id=tenant_id).first()
70 if not warehouse:
71 raise NotFoundException("Warehouse not found.")
72 warehouse.delete()
73 return {"success": True}
76# ── Warehouse Receipts ─────────────────────────────────────────────────────────
77@router.get("/warehouse-receipts", auth=JWTAuth(), response={200: List[WarehouseReceiptOut]})
78def list_warehouse_receipts(request, warehouse_id: Optional[UUID] = None, status: Optional[str] = None):
79 tenant_id = require_tenant(request)
80 qs = WarehouseReceipt.objects.filter(tenant_id=tenant_id)
81 if warehouse_id:
82 qs = qs.filter(warehouse_id=warehouse_id)
83 if status:
84 qs = qs.filter(status=status)
85 return list(qs.select_related("warehouse"))
88@router.post("/warehouse-receipts", auth=JWTAuth(), response={201: WarehouseReceiptOut})
89@require_admin
90def create_warehouse_receipt(request, payload: WarehouseReceiptIn):
91 tenant_id = require_tenant(request)
92 receipt = WarehouseReceipt.objects.create(tenant_id=tenant_id, **payload.dict())
93 return 201, receipt
96@router.get("/warehouse-receipts/{receipt_id}", auth=JWTAuth(), response={200: WarehouseReceiptOut})
97def get_warehouse_receipt(request, receipt_id: UUID):
98 tenant_id = require_tenant(request)
99 receipt = WarehouseReceipt.objects.filter(id=receipt_id, tenant_id=tenant_id).first()
100 if not receipt:
101 raise NotFoundException("Warehouse receipt not found.")
102 return receipt
105@router.put("/warehouse-receipts/{receipt_id}", auth=JWTAuth(), response={200: WarehouseReceiptOut})
106@require_admin
107def update_warehouse_receipt(request, receipt_id: UUID, payload: WarehouseReceiptIn):
108 tenant_id = require_tenant(request)
109 receipt = WarehouseReceipt.objects.filter(id=receipt_id, tenant_id=tenant_id).first()
110 if not receipt:
111 raise NotFoundException("Warehouse receipt not found.")
112 for key, value in payload.dict().items():
113 setattr(receipt, key, value)
114 receipt.save()
115 return receipt
118@router.delete("/warehouse-receipts/{receipt_id}", auth=JWTAuth())
119@require_admin
120def delete_warehouse_receipt(request, receipt_id: UUID):
121 tenant_id = require_tenant(request)
122 receipt = WarehouseReceipt.objects.filter(id=receipt_id, tenant_id=tenant_id).first()
123 if not receipt:
124 raise NotFoundException("Warehouse receipt not found.")
125 receipt.delete()
126 return {"success": True}
129# ── Shipping Quotes ─────────────────────────────────────────────────────────────
130@router.get("/shipping-quotes", auth=JWTAuth(), response={200: List[ShippingQuoteOut]})
131def list_shipping_quotes(request, status: Optional[str] = None):
132 tenant_id = require_tenant(request)
133 qs = ShippingQuote.objects.filter(tenant_id=tenant_id)
134 if status:
135 qs = qs.filter(status=status)
136 return list(qs)
139@router.post("/shipping-quotes", auth=JWTAuth(), response={201: ShippingQuoteOut})
140@require_admin
141def create_shipping_quote(request, payload: ShippingQuoteIn):
142 tenant_id = require_tenant(request)
143 quote = ShippingQuote.objects.create(tenant_id=tenant_id, **payload.dict())
144 return 201, quote
147@router.get("/shipping-quotes/{quote_id}", auth=JWTAuth(), response={200: ShippingQuoteOut})
148def get_shipping_quote(request, quote_id: UUID):
149 tenant_id = require_tenant(request)
150 quote = ShippingQuote.objects.filter(id=quote_id, tenant_id=tenant_id).first()
151 if not quote:
152 raise NotFoundException("Shipping quote not found.")
153 return quote
156@router.put("/shipping-quotes/{quote_id}", auth=JWTAuth(), response={200: ShippingQuoteOut})
157@require_admin
158def update_shipping_quote(request, quote_id: UUID, payload: ShippingQuoteIn):
159 tenant_id = require_tenant(request)
160 quote = ShippingQuote.objects.filter(id=quote_id, tenant_id=tenant_id).first()
161 if not quote:
162 raise NotFoundException("Shipping quote not found.")
163 for key, value in payload.dict().items():
164 setattr(quote, key, value)
165 quote.save()
166 return quote
169@router.delete("/shipping-quotes/{quote_id}", auth=JWTAuth())
170@require_admin
171def delete_shipping_quote(request, quote_id: UUID):
172 tenant_id = require_tenant(request)
173 quote = ShippingQuote.objects.filter(id=quote_id, tenant_id=tenant_id).first()
174 if not quote:
175 raise NotFoundException("Shipping quote not found.")
176 quote.delete()
177 return {"success": True}
180# ── Freight Quotes ─────────────────────────────────────────────────────────────
181@router.get("/freight-quotes", auth=JWTAuth(), response={200: List[FreightQuoteOut]})
182def list_freight_quotes(request, status: Optional[str] = None):
183 tenant_id = require_tenant(request)
184 qs = FreightQuote.objects.filter(tenant_id=tenant_id)
185 if status:
186 qs = qs.filter(status=status)
187 return list(qs)
190@router.post("/freight-quotes", auth=JWTAuth(), response={201: FreightQuoteOut})
191@require_admin
192def create_freight_quote(request, payload: FreightQuoteIn):
193 tenant_id = require_tenant(request)
194 quote = FreightQuote.objects.create(tenant_id=tenant_id, **payload.dict())
195 return 201, quote
198@router.get("/freight-quotes/{quote_id}", auth=JWTAuth(), response={200: FreightQuoteOut})
199def get_freight_quote(request, quote_id: UUID):
200 tenant_id = require_tenant(request)
201 quote = FreightQuote.objects.filter(id=quote_id, tenant_id=tenant_id).first()
202 if not quote:
203 raise NotFoundException("Freight quote not found.")
204 return quote
207@router.put("/freight-quotes/{quote_id}", auth=JWTAuth(), response={200: FreightQuoteOut})
208@require_admin
209def update_freight_quote(request, quote_id: UUID, payload: FreightQuoteIn):
210 tenant_id = require_tenant(request)
211 quote = FreightQuote.objects.filter(id=quote_id, tenant_id=tenant_id).first()
212 if not quote:
213 raise NotFoundException("Freight quote not found.")
214 for key, value in payload.dict().items():
215 setattr(quote, key, value)
216 quote.save()
217 return quote
220@router.delete("/freight-quotes/{quote_id}", auth=JWTAuth())
221@require_admin
222def delete_freight_quote(request, quote_id: UUID):
223 tenant_id = require_tenant(request)
224 quote = FreightQuote.objects.filter(id=quote_id, tenant_id=tenant_id).first()
225 if not quote:
226 raise NotFoundException("Freight quote not found.")
227 quote.delete()
228 return {"success": True}
231# ── Transportation Rates ────────────────────────────────────────────────────────
232@router.get("/transportation-rates", auth=JWTAuth(), response={200: List[TransportationRateOut]})
233def list_transportation_rates(request, origin: Optional[str] = None, destination: Optional[str] = None, mode: Optional[str] = None):
234 tenant_id = require_tenant(request)
235 qs = TransportationRate.objects.filter(tenant_id=tenant_id, is_active=True)
236 if origin:
237 qs = qs.filter(origin__icontains=origin)
238 if destination:
239 qs = qs.filter(destination__icontains=destination)
240 if mode:
241 qs = qs.filter(mode=mode)
242 return list(qs)
245@router.post("/transportation-rates", auth=JWTAuth(), response={201: TransportationRateOut})
246@require_admin
247def create_transportation_rate(request, payload: TransportationRateIn):
248 tenant_id = require_tenant(request)
249 rate = TransportationRate.objects.create(tenant_id=tenant_id, **payload.dict())
250 return 201, rate
253@router.get("/transportation-rates/{rate_id}", auth=JWTAuth(), response={200: TransportationRateOut})
254def get_transportation_rate(request, rate_id: UUID):
255 tenant_id = require_tenant(request)
256 rate = TransportationRate.objects.filter(id=rate_id, tenant_id=tenant_id).first()
257 if not rate:
258 raise NotFoundException("Transportation rate not found.")
259 return rate
262@router.put("/transportation-rates/{rate_id}", auth=JWTAuth(), response={200: TransportationRateOut})
263@require_admin
264def update_transportation_rate(request, rate_id: UUID, payload: TransportationRateIn):
265 tenant_id = require_tenant(request)
266 rate = TransportationRate.objects.filter(id=rate_id, tenant_id=tenant_id).first()
267 if not rate:
268 raise NotFoundException("Transportation rate not found.")
269 for key, value in payload.dict().items():
270 setattr(rate, key, value)
271 rate.save()
272 return rate
275@router.delete("/transportation-rates/{rate_id}", auth=JWTAuth())
276@require_admin
277def delete_transportation_rate(request, rate_id: UUID):
278 tenant_id = require_tenant(request)
279 rate = TransportationRate.objects.filter(id=rate_id, tenant_id=tenant_id).first()
280 if not rate:
281 raise NotFoundException("Transportation rate not found.")
282 rate.delete()
283 return {"success": True}
286# ── Bookings ───────────────────────────────────────────────────────────────────
287@router.get("/bookings", auth=JWTAuth(), response={200: List[BookingOut]})
288def list_bookings(request, status: Optional[str] = None, booking_type: Optional[str] = None):
289 tenant_id = require_tenant(request)
290 qs = Booking.objects.filter(tenant_id=tenant_id)
291 if status:
292 qs = qs.filter(status=status)
293 if booking_type:
294 qs = qs.filter(booking_type=booking_type)
295 return list(qs)
298@router.post("/bookings", auth=JWTAuth(), response={201: BookingOut})
299@require_admin
300def create_booking(request, payload: BookingIn):
301 tenant_id = require_tenant(request)
302 booking = Booking.objects.create(tenant_id=tenant_id, **payload.dict())
303 return 201, booking
306@router.get("/bookings/{booking_id}", auth=JWTAuth(), response={200: BookingOut})
307def get_booking(request, booking_id: UUID):
308 tenant_id = require_tenant(request)
309 booking = Booking.objects.filter(id=booking_id, tenant_id=tenant_id).first()
310 if not booking:
311 raise NotFoundException("Booking not found.")
312 return booking
315@router.put("/bookings/{booking_id}", auth=JWTAuth(), response={200: BookingOut})
316@require_admin
317def update_booking(request, booking_id: UUID, payload: BookingIn):
318 tenant_id = require_tenant(request)
319 booking = Booking.objects.filter(id=booking_id, tenant_id=tenant_id).first()
320 if not booking:
321 raise NotFoundException("Booking not found.")
322 for key, value in payload.dict().items():
323 setattr(booking, key, value)
324 booking.save()
325 return booking
328@router.delete("/bookings/{booking_id}", auth=JWTAuth())
329@require_admin
330def delete_booking(request, booking_id: UUID):
331 tenant_id = require_tenant(request)
332 booking = Booking.objects.filter(id=booking_id, tenant_id=tenant_id).first()
333 if not booking:
334 raise NotFoundException("Booking not found.")
335 booking.delete()
336 return {"success": True}
339# ── Tracking Events ────────────────────────────────────────────────────────────
340@router.get("/tracking-events", auth=JWTAuth(), response={200: List[TrackingEventOut]})
341def list_tracking_events(request, booking_id: Optional[UUID] = None, shipment_reference: Optional[str] = None):
342 tenant_id = require_tenant(request)
343 qs = TrackingEvent.objects.filter(tenant_id=tenant_id)
344 if booking_id:
345 qs = qs.filter(booking_id=booking_id)
346 if shipment_reference:
347 qs = qs.filter(shipment_reference__icontains=shipment_reference)
348 return list(qs.select_related("booking"))
351@router.post("/tracking-events", auth=JWTAuth(), response={201: TrackingEventOut})
352@require_admin
353def create_tracking_event(request, payload: TrackingEventIn):
354 tenant_id = require_tenant(request)
355 event = TrackingEvent.objects.create(tenant_id=tenant_id, **payload.dict())
356 return 201, event
359@router.get("/tracking-events/{event_id}", auth=JWTAuth(), response={200: TrackingEventOut})
360def get_tracking_event(request, event_id: UUID):
361 tenant_id = require_tenant(request)
362 event = TrackingEvent.objects.filter(id=event_id, tenant_id=tenant_id).first()
363 if not event:
364 raise NotFoundException("Tracking event not found.")
365 return event
368@router.delete("/tracking-events/{event_id}", auth=JWTAuth())
369@require_admin
370def delete_tracking_event(request, event_id: UUID):
371 tenant_id = require_tenant(request)
372 event = TrackingEvent.objects.filter(id=event_id, tenant_id=tenant_id).first()
373 if not event:
374 raise NotFoundException("Tracking event not found.")
375 event.delete()
376 return {"success": True}