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

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) 

21 

22router = Router(tags=["Logistics"]) 

23 

24 

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) 

33 

34 

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 

41 

42 

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 

50 

51 

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 

63 

64 

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} 

74 

75 

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")) 

86 

87 

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 

94 

95 

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 

103 

104 

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 

116 

117 

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} 

127 

128 

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) 

137 

138 

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 

145 

146 

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 

154 

155 

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 

167 

168 

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} 

178 

179 

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) 

188 

189 

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 

196 

197 

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 

205 

206 

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 

218 

219 

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} 

229 

230 

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) 

243 

244 

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 

251 

252 

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 

260 

261 

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 

273 

274 

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} 

284 

285 

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) 

296 

297 

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 

304 

305 

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 

313 

314 

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 

326 

327 

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} 

337 

338 

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")) 

349 

350 

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 

357 

358 

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 

366 

367 

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}