karrio-server-manager 2025.5rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- karrio/server/manager/__init__.py +1 -0
- karrio/server/manager/admin.py +1 -0
- karrio/server/manager/apps.py +13 -0
- karrio/server/manager/migrations/0001_initial.py +1358 -0
- karrio/server/manager/migrations/0002_auto_20201127_0721.py +61 -0
- karrio/server/manager/migrations/0003_auto_20201230_0820.py +34 -0
- karrio/server/manager/migrations/0004_auto_20210125_2125.py +18 -0
- karrio/server/manager/migrations/0005_auto_20210216_0758.py +27 -0
- karrio/server/manager/migrations/0006_auto_20210307_0438.py +24 -0
- karrio/server/manager/migrations/0006_auto_20210308_0302.py +53 -0
- karrio/server/manager/migrations/0007_merge_20210311_1428.py +14 -0
- karrio/server/manager/migrations/0008_remove_shipment_doc_images.py +17 -0
- karrio/server/manager/migrations/0009_auto_20210326_1425.py +28 -0
- karrio/server/manager/migrations/0010_auto_20210403_1404.py +28 -0
- karrio/server/manager/migrations/0011_auto_20210426_1924.py +48 -0
- karrio/server/manager/migrations/0012_auto_20210427_1319.py +24 -0
- karrio/server/manager/migrations/0013_customs_invoice_date.py +18 -0
- karrio/server/manager/migrations/0014_auto_20210515_0928.py +24 -0
- karrio/server/manager/migrations/0015_auto_20210601_0340.py +182 -0
- karrio/server/manager/migrations/0016_shipment_archived.py +18 -0
- karrio/server/manager/migrations/0017_auto_20210629_1650.py +22 -0
- karrio/server/manager/migrations/0018_auto_20210705_1049.py +23 -0
- karrio/server/manager/migrations/0019_auto_20210722_1131.py +43 -0
- karrio/server/manager/migrations/0020_tracking_messages.py +20 -0
- karrio/server/manager/migrations/0021_tracking_estimated_delivery.py +18 -0
- karrio/server/manager/migrations/0022_auto_20211122_2100.py +53 -0
- karrio/server/manager/migrations/0023_auto_20211227_2141.py +118 -0
- karrio/server/manager/migrations/0024_alter_parcel_items.py +18 -0
- karrio/server/manager/migrations/0025_auto_20220113_1158.py +25 -0
- karrio/server/manager/migrations/0026_parcel_reference_number.py +18 -0
- karrio/server/manager/migrations/0027_custom_migration_2021_1.py +47 -0
- karrio/server/manager/migrations/0028_auto_20220303_1153.py +39 -0
- karrio/server/manager/migrations/0029_auto_20220303_1249.py +55 -0
- karrio/server/manager/migrations/0030_alter_shipment_status.py +44 -0
- karrio/server/manager/migrations/0031_shipment_invoice.py +34 -0
- karrio/server/manager/migrations/0032_custom_migration_2022_3.py +26 -0
- karrio/server/manager/migrations/0033_auto_20220504_1335.py +57 -0
- karrio/server/manager/migrations/0034_commodity_hs_code.py +18 -0
- karrio/server/manager/migrations/0035_parcel_options.py +26 -0
- karrio/server/manager/migrations/0036_alter_tracking_shipment.py +24 -0
- karrio/server/manager/migrations/0037_auto_20220710_1350.py +28 -0
- karrio/server/manager/migrations/0038_alter_tracking_status.py +18 -0
- karrio/server/manager/migrations/0039_documentuploadrecord.py +43 -0
- karrio/server/manager/migrations/0040_parcel_freight_class.py +18 -0
- karrio/server/manager/migrations/0041_alter_commodity_options_alter_parcel_options.py +29 -0
- karrio/server/manager/migrations/0042_remove_shipment_shipment_tracking_number_idx_and_more.py +658 -0
- karrio/server/manager/migrations/0043_customs_duty_billing_address_and_more.py +62 -0
- karrio/server/manager/migrations/0044_address_address_line1_temp_and_more.py +326 -0
- karrio/server/manager/migrations/0045_alter_customs_duty_billing_address_and_more.py +45 -0
- karrio/server/manager/migrations/0046_auto_20230114_0930.py +78 -0
- karrio/server/manager/migrations/0047_remove_shipment_shipment_tracking_number_idx_and_more.py +595 -0
- karrio/server/manager/migrations/0048_commodity_title_alter_commodity_description_and_more.py +53 -0
- karrio/server/manager/migrations/0049_auto_20230318_0708.py +39 -0
- karrio/server/manager/migrations/0050_address_street_number_tracking_account_number_and_more.py +60 -0
- karrio/server/manager/migrations/0051_auto_20230330_0556.py +56 -0
- karrio/server/manager/migrations/0052_auto_20230520_0811.py +35 -0
- karrio/server/manager/migrations/0053_alter_commodity_weight_unit_alter_parcel_weight_unit.py +32 -0
- karrio/server/manager/migrations/0054_alter_address_company_name_alter_address_person_name.py +22 -0
- karrio/server/manager/migrations/0055_alter_tracking_status.py +32 -0
- karrio/server/manager/migrations/0056_tracking_delivery_image_tracking_signature_image.py +22 -0
- karrio/server/manager/migrations/0057_alter_customs_invoice_date.py +18 -0
- karrio/server/manager/migrations/0058_manifest_shipment_manifest.py +124 -0
- karrio/server/manager/migrations/0059_shipment_return_address.py +24 -0
- karrio/server/manager/migrations/0060_pickup_meta_alter_address_country_code_and_more.py +527 -0
- karrio/server/manager/migrations/0061_alter_customs_incoterm.py +37 -0
- karrio/server/manager/migrations/0062_alter_tracking_status.py +35 -0
- karrio/server/manager/migrations/__init__.py +0 -0
- karrio/server/manager/models.py +984 -0
- karrio/server/manager/router.py +3 -0
- karrio/server/manager/serializers/__init__.py +50 -0
- karrio/server/manager/serializers/address.py +82 -0
- karrio/server/manager/serializers/commodity.py +51 -0
- karrio/server/manager/serializers/customs.py +84 -0
- karrio/server/manager/serializers/document.py +113 -0
- karrio/server/manager/serializers/manifest.py +85 -0
- karrio/server/manager/serializers/parcel.py +84 -0
- karrio/server/manager/serializers/pickup.py +285 -0
- karrio/server/manager/serializers/rate.py +19 -0
- karrio/server/manager/serializers/shipment.py +869 -0
- karrio/server/manager/serializers/tracking.py +250 -0
- karrio/server/manager/signals.py +70 -0
- karrio/server/manager/tests/__init__.py +10 -0
- karrio/server/manager/tests/test_addresses.py +110 -0
- karrio/server/manager/tests/test_custom_infos.py +97 -0
- karrio/server/manager/tests/test_parcels.py +104 -0
- karrio/server/manager/tests/test_pickups.py +345 -0
- karrio/server/manager/tests/test_shipments.py +833 -0
- karrio/server/manager/tests/test_trackers.py +215 -0
- karrio/server/manager/urls.py +10 -0
- karrio/server/manager/views/__init__.py +9 -0
- karrio/server/manager/views/addresses.py +154 -0
- karrio/server/manager/views/customs.py +159 -0
- karrio/server/manager/views/documents.py +131 -0
- karrio/server/manager/views/manifests.py +160 -0
- karrio/server/manager/views/parcels.py +155 -0
- karrio/server/manager/views/pickups.py +182 -0
- karrio/server/manager/views/shipments.py +335 -0
- karrio/server/manager/views/trackers.py +364 -0
- karrio_server_manager-2025.5rc1.dist-info/METADATA +28 -0
- karrio_server_manager-2025.5rc1.dist-info/RECORD +102 -0
- karrio_server_manager-2025.5rc1.dist-info/WHEEL +5 -0
- karrio_server_manager-2025.5rc1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,335 @@
|
|
1
|
+
import io
|
2
|
+
import base64
|
3
|
+
import logging
|
4
|
+
|
5
|
+
from rest_framework import status
|
6
|
+
from rest_framework.request import Request
|
7
|
+
from rest_framework.response import Response
|
8
|
+
from rest_framework.pagination import LimitOffsetPagination
|
9
|
+
from django_filters.rest_framework import DjangoFilterBackend
|
10
|
+
from django_downloadview import VirtualDownloadView
|
11
|
+
from django.core.files.base import ContentFile
|
12
|
+
from django.urls import path, re_path
|
13
|
+
|
14
|
+
import karrio.lib as lib
|
15
|
+
import karrio.server.openapi as openapi
|
16
|
+
import karrio.server.core.filters as filters
|
17
|
+
import karrio.server.manager.models as models
|
18
|
+
from karrio.server.core.views.api import GenericAPIView, APIView
|
19
|
+
from karrio.server.core.filters import ShipmentFilters
|
20
|
+
from karrio.server.manager.router import router
|
21
|
+
from karrio.server.manager.serializers import (
|
22
|
+
process_dictionaries_mutations,
|
23
|
+
fetch_shipment_rates,
|
24
|
+
PaginatedResult,
|
25
|
+
ErrorResponse,
|
26
|
+
ErrorMessages,
|
27
|
+
Shipment,
|
28
|
+
ShipmentData,
|
29
|
+
buy_shipment_label,
|
30
|
+
can_mutate_shipment,
|
31
|
+
ShipmentSerializer,
|
32
|
+
ShipmentRateData,
|
33
|
+
ShipmentUpdateData,
|
34
|
+
ShipmentPurchaseData,
|
35
|
+
ShipmentCancelSerializer,
|
36
|
+
)
|
37
|
+
|
38
|
+
ENDPOINT_ID = "$$$$$" # This endpoint id is used to make operation ids unique make sure not to duplicate
|
39
|
+
logger = logging.getLogger(__name__)
|
40
|
+
Shipments = PaginatedResult("ShipmentList", Shipment)
|
41
|
+
|
42
|
+
|
43
|
+
class ShipmentList(GenericAPIView):
|
44
|
+
throttle_scope = "carrier_request"
|
45
|
+
pagination_class = type(
|
46
|
+
"CustomPagination", (LimitOffsetPagination,), dict(default_limit=20)
|
47
|
+
)
|
48
|
+
filter_backends = (DjangoFilterBackend,)
|
49
|
+
filterset_class = ShipmentFilters
|
50
|
+
serializer_class = Shipments
|
51
|
+
model = models.Shipment
|
52
|
+
|
53
|
+
@openapi.extend_schema(
|
54
|
+
tags=["Shipments"],
|
55
|
+
operation_id=f"{ENDPOINT_ID}list",
|
56
|
+
extensions={"x-operationId": "listShipments"},
|
57
|
+
summary="List all shipments",
|
58
|
+
parameters=filters.ShipmentFilters.parameters,
|
59
|
+
responses={
|
60
|
+
200: Shipment(),
|
61
|
+
404: ErrorResponse(),
|
62
|
+
500: ErrorResponse(),
|
63
|
+
},
|
64
|
+
)
|
65
|
+
def get(self, _: Request):
|
66
|
+
"""
|
67
|
+
Retrieve all shipments.
|
68
|
+
"""
|
69
|
+
shipments = self.filter_queryset(self.get_queryset())
|
70
|
+
response = self.paginate_queryset(Shipment(shipments, many=True).data)
|
71
|
+
|
72
|
+
return self.get_paginated_response(response)
|
73
|
+
|
74
|
+
@openapi.extend_schema(
|
75
|
+
tags=["Shipments"],
|
76
|
+
operation_id=f"{ENDPOINT_ID}create",
|
77
|
+
extensions={"x-operationId": "createShipment"},
|
78
|
+
summary="Create a shipment",
|
79
|
+
responses={
|
80
|
+
201: Shipment(),
|
81
|
+
400: ErrorResponse(),
|
82
|
+
424: ErrorMessages(),
|
83
|
+
500: ErrorResponse(),
|
84
|
+
},
|
85
|
+
request=ShipmentData(),
|
86
|
+
)
|
87
|
+
def post(self, request: Request):
|
88
|
+
"""
|
89
|
+
Create a new shipment instance.
|
90
|
+
"""
|
91
|
+
shipment = (
|
92
|
+
ShipmentSerializer.map(data=request.data, context=request).save().instance
|
93
|
+
)
|
94
|
+
|
95
|
+
return Response(Shipment(shipment).data, status=status.HTTP_201_CREATED)
|
96
|
+
|
97
|
+
|
98
|
+
class ShipmentDetails(APIView):
|
99
|
+
throttle_scope = "carrier_request"
|
100
|
+
|
101
|
+
@openapi.extend_schema(
|
102
|
+
tags=["Shipments"],
|
103
|
+
operation_id=f"{ENDPOINT_ID}retrieve",
|
104
|
+
extensions={"x-operationId": "retrieveShipment"},
|
105
|
+
summary="Retrieve a shipment",
|
106
|
+
responses={
|
107
|
+
200: Shipment(),
|
108
|
+
404: ErrorResponse(),
|
109
|
+
500: ErrorResponse(),
|
110
|
+
},
|
111
|
+
)
|
112
|
+
def get(self, request: Request, pk: str):
|
113
|
+
"""
|
114
|
+
Retrieve a shipment.
|
115
|
+
"""
|
116
|
+
shipment = models.Shipment.access_by(request).get(pk=pk)
|
117
|
+
|
118
|
+
return Response(Shipment(shipment).data)
|
119
|
+
|
120
|
+
@openapi.extend_schema(
|
121
|
+
tags=["Shipments"],
|
122
|
+
operation_id=f"{ENDPOINT_ID}update",
|
123
|
+
extensions={"x-operationId": "updateShipment"},
|
124
|
+
summary="Update a shipment",
|
125
|
+
responses={
|
126
|
+
200: Shipment(),
|
127
|
+
404: ErrorResponse(),
|
128
|
+
400: ErrorResponse(),
|
129
|
+
409: ErrorResponse(),
|
130
|
+
424: ErrorMessages(),
|
131
|
+
500: ErrorResponse(),
|
132
|
+
},
|
133
|
+
request=ShipmentUpdateData(),
|
134
|
+
)
|
135
|
+
def put(self, request: Request, pk: str):
|
136
|
+
"""
|
137
|
+
This operation allows for updating properties of a shipment including `label_type`, `reference`, `payment`, `options` and `metadata`.
|
138
|
+
It is not for editing the parcels of a shipment.
|
139
|
+
"""
|
140
|
+
shipment = models.Shipment.access_by(request).get(pk=pk)
|
141
|
+
payload = ShipmentUpdateData.map(data=request.data).data
|
142
|
+
can_mutate_shipment(shipment, update=True, payload=request.data)
|
143
|
+
|
144
|
+
update = (
|
145
|
+
ShipmentSerializer.map(
|
146
|
+
shipment,
|
147
|
+
context=request,
|
148
|
+
data=process_dictionaries_mutations(
|
149
|
+
["metadata", "options"], payload, shipment
|
150
|
+
),
|
151
|
+
)
|
152
|
+
.save()
|
153
|
+
.instance
|
154
|
+
)
|
155
|
+
|
156
|
+
return Response(Shipment(update).data)
|
157
|
+
|
158
|
+
|
159
|
+
class ShipmentCancel(APIView):
|
160
|
+
throttle_scope = "carrier_request"
|
161
|
+
|
162
|
+
@openapi.extend_schema(
|
163
|
+
tags=["Shipments"],
|
164
|
+
operation_id=f"{ENDPOINT_ID}cancel",
|
165
|
+
extensions={"x-operationId": "cancelShipment"},
|
166
|
+
summary="Cancel a shipment",
|
167
|
+
request=None,
|
168
|
+
responses={
|
169
|
+
200: Shipment(),
|
170
|
+
404: ErrorResponse(),
|
171
|
+
400: ErrorResponse(),
|
172
|
+
409: ErrorResponse(),
|
173
|
+
424: ErrorMessages(),
|
174
|
+
500: ErrorResponse(),
|
175
|
+
},
|
176
|
+
)
|
177
|
+
def post(self, request: Request, pk: str):
|
178
|
+
"""
|
179
|
+
Void a shipment with the associated label.
|
180
|
+
"""
|
181
|
+
shipment = models.Shipment.access_by(request).get(pk=pk)
|
182
|
+
can_mutate_shipment(shipment, delete=True)
|
183
|
+
|
184
|
+
update = ShipmentCancelSerializer.map(shipment, context=request).save().instance
|
185
|
+
|
186
|
+
return Response(Shipment(update).data)
|
187
|
+
|
188
|
+
|
189
|
+
class ShipmentRates(APIView):
|
190
|
+
throttle_scope = "carrier_request"
|
191
|
+
|
192
|
+
@openapi.extend_schema(
|
193
|
+
tags=["Shipments"],
|
194
|
+
operation_id=f"{ENDPOINT_ID}rates",
|
195
|
+
summary="Fetch new shipment rates",
|
196
|
+
responses={
|
197
|
+
200: Shipment(),
|
198
|
+
404: ErrorResponse(),
|
199
|
+
400: ErrorResponse(),
|
200
|
+
409: ErrorResponse(),
|
201
|
+
424: ErrorMessages(),
|
202
|
+
500: ErrorResponse(),
|
203
|
+
},
|
204
|
+
request=ShipmentRateData(),
|
205
|
+
)
|
206
|
+
def post(self, request: Request, pk: str):
|
207
|
+
"""
|
208
|
+
Refresh the list of the shipment rates
|
209
|
+
"""
|
210
|
+
shipment = models.Shipment.access_by(request).get(pk=pk)
|
211
|
+
can_mutate_shipment(shipment, update=True)
|
212
|
+
|
213
|
+
payload = ShipmentRateData.map(data=request.data).data
|
214
|
+
|
215
|
+
update = fetch_shipment_rates(
|
216
|
+
shipment,
|
217
|
+
context=request,
|
218
|
+
data=process_dictionaries_mutations(
|
219
|
+
["metadata", "options"], payload, shipment
|
220
|
+
),
|
221
|
+
)
|
222
|
+
|
223
|
+
return Response(Shipment(update).data)
|
224
|
+
|
225
|
+
|
226
|
+
class ShipmentPurchase(APIView):
|
227
|
+
throttle_scope = "carrier_request"
|
228
|
+
|
229
|
+
@openapi.extend_schema(
|
230
|
+
tags=["Shipments"],
|
231
|
+
operation_id=f"{ENDPOINT_ID}purchase",
|
232
|
+
summary="Buy a shipment label",
|
233
|
+
responses={
|
234
|
+
200: Shipment(),
|
235
|
+
404: ErrorResponse(),
|
236
|
+
400: ErrorResponse(),
|
237
|
+
409: ErrorResponse(),
|
238
|
+
424: ErrorMessages(),
|
239
|
+
500: ErrorResponse(),
|
240
|
+
},
|
241
|
+
request=ShipmentPurchaseData(),
|
242
|
+
)
|
243
|
+
def post(self, request: Request, pk: str):
|
244
|
+
"""
|
245
|
+
Select your preferred rates to buy a shipment label.
|
246
|
+
"""
|
247
|
+
shipment = models.Shipment.access_by(request).get(pk=pk)
|
248
|
+
can_mutate_shipment(shipment, purchase=True, update=True)
|
249
|
+
|
250
|
+
payload = ShipmentPurchaseData.map(data=request.data).data
|
251
|
+
|
252
|
+
update = buy_shipment_label(
|
253
|
+
shipment,
|
254
|
+
context=request,
|
255
|
+
data=process_dictionaries_mutations(["metadata"], payload, shipment),
|
256
|
+
)
|
257
|
+
|
258
|
+
return Response(Shipment(update).data)
|
259
|
+
|
260
|
+
|
261
|
+
class ShipmentDocs(VirtualDownloadView):
|
262
|
+
@openapi.extend_schema(exclude=True)
|
263
|
+
def get(
|
264
|
+
self,
|
265
|
+
request: Request,
|
266
|
+
pk: str,
|
267
|
+
doc: str = "label",
|
268
|
+
format: str = "pdf",
|
269
|
+
**kwargs,
|
270
|
+
):
|
271
|
+
"""Retrieve a shipment label."""
|
272
|
+
self.shipment = models.Shipment.objects.get(pk=pk, label__isnull=False)
|
273
|
+
self.document = getattr(self.shipment, doc, None)
|
274
|
+
self.name = f"{doc}_{self.shipment.tracking_number}.{format}"
|
275
|
+
|
276
|
+
query_params = request.GET.dict()
|
277
|
+
self.preview = "preview" in query_params
|
278
|
+
self.attachment = "download" in query_params
|
279
|
+
|
280
|
+
response = super(ShipmentDocs, self).get(request, pk, doc, format, **kwargs)
|
281
|
+
response["X-Frame-Options"] = "ALLOWALL"
|
282
|
+
return response
|
283
|
+
|
284
|
+
def get_file(self):
|
285
|
+
content = base64.b64decode(self.document or "")
|
286
|
+
buffer = io.BytesIO()
|
287
|
+
|
288
|
+
if self.preview and "ZPL" in self.shipment.label_type or "":
|
289
|
+
width, height, dpmm = (4, 6, 12)
|
290
|
+
|
291
|
+
if "8" in self.shipment.label_type:
|
292
|
+
width, height, dpmm = (8, 4, 12)
|
293
|
+
|
294
|
+
_label = lib.failsafe(
|
295
|
+
lambda: lib.zpl_to_pdf(
|
296
|
+
self.document,
|
297
|
+
width,
|
298
|
+
height,
|
299
|
+
dpmm=dpmm,
|
300
|
+
)
|
301
|
+
)
|
302
|
+
|
303
|
+
if _label is not None:
|
304
|
+
content = base64.b64decode(_label)
|
305
|
+
self.name = self.name.replace("zpl", "pdf")
|
306
|
+
|
307
|
+
buffer.write(content)
|
308
|
+
|
309
|
+
return ContentFile(buffer.getvalue(), name=self.name)
|
310
|
+
|
311
|
+
|
312
|
+
router.urls.append(path("shipments", ShipmentList.as_view(), name="shipment-list"))
|
313
|
+
router.urls.append(
|
314
|
+
path("shipments/<str:pk>", ShipmentDetails.as_view(), name="shipment-details")
|
315
|
+
)
|
316
|
+
router.urls.append(
|
317
|
+
path("shipments/<str:pk>/cancel", ShipmentCancel.as_view(), name="shipment-cancel")
|
318
|
+
)
|
319
|
+
router.urls.append(
|
320
|
+
path("shipments/<str:pk>/rates", ShipmentRates.as_view(), name="shipment-rates")
|
321
|
+
)
|
322
|
+
router.urls.append(
|
323
|
+
path(
|
324
|
+
"shipments/<str:pk>/purchase",
|
325
|
+
ShipmentPurchase.as_view(),
|
326
|
+
name="shipment-purchase",
|
327
|
+
)
|
328
|
+
)
|
329
|
+
router.urls.append(
|
330
|
+
re_path(
|
331
|
+
r"^shipments/(?P<pk>\w+)/(?P<doc>[a-z0-9]+).(?P<format>[a-z0-9]+)",
|
332
|
+
ShipmentDocs.as_view(),
|
333
|
+
name="shipment-docs",
|
334
|
+
)
|
335
|
+
)
|
@@ -0,0 +1,364 @@
|
|
1
|
+
import io
|
2
|
+
import base64
|
3
|
+
import logging
|
4
|
+
import django_downloadview
|
5
|
+
|
6
|
+
from django.db.models import Q
|
7
|
+
from django.urls import path, re_path
|
8
|
+
from django_filters.rest_framework import DjangoFilterBackend
|
9
|
+
from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
10
|
+
from rest_framework.pagination import LimitOffsetPagination
|
11
|
+
from django.core.files.base import ContentFile
|
12
|
+
from rest_framework.response import Response
|
13
|
+
from rest_framework.request import Request
|
14
|
+
from rest_framework import status
|
15
|
+
|
16
|
+
from karrio.server.core.views.api import GenericAPIView, APIView
|
17
|
+
from karrio.server.manager.router import router
|
18
|
+
import karrio.server.manager.serializers as serializers
|
19
|
+
import karrio.server.core.dataunits as dataunits
|
20
|
+
import karrio.server.manager.models as models
|
21
|
+
import karrio.server.core.filters as filters
|
22
|
+
import karrio.server.openapi as openapi
|
23
|
+
|
24
|
+
ENDPOINT_ID = "$$$$$$" # This endpoint id is used to make operation ids unique make sure not to duplicate
|
25
|
+
logger = logging.getLogger(__name__)
|
26
|
+
Trackers = serializers.PaginatedResult("TrackerList", serializers.TrackingStatus)
|
27
|
+
|
28
|
+
|
29
|
+
class TrackerList(GenericAPIView):
|
30
|
+
throttle_scope = "carrier_request"
|
31
|
+
pagination_class = type(
|
32
|
+
"CustomPagination", (LimitOffsetPagination,), dict(default_limit=20)
|
33
|
+
)
|
34
|
+
filter_backends = (DjangoFilterBackend,)
|
35
|
+
filterset_class = filters.TrackerFilters
|
36
|
+
serializer_class = Trackers
|
37
|
+
model = models.Tracking
|
38
|
+
|
39
|
+
def get_queryset(self):
|
40
|
+
queryset = super().get_queryset()
|
41
|
+
_filters = tuple()
|
42
|
+
query_params = getattr(self.request, "query_params", {})
|
43
|
+
carrier_name = query_params.get("carrier_name")
|
44
|
+
|
45
|
+
if carrier_name is not None:
|
46
|
+
_filters += (Q(tracking_carrier__carrier_code=carrier_name),)
|
47
|
+
|
48
|
+
return queryset.filter(*_filters)
|
49
|
+
|
50
|
+
@openapi.extend_schema(
|
51
|
+
tags=["Trackers"],
|
52
|
+
operation_id=f"{ENDPOINT_ID}list",
|
53
|
+
extensions={"x-operationId": "listTrackers"},
|
54
|
+
summary="List all package trackers",
|
55
|
+
parameters=filters.TrackerFilters.parameters,
|
56
|
+
responses={
|
57
|
+
200: Trackers(),
|
58
|
+
404: serializers.ErrorResponse(),
|
59
|
+
500: serializers.ErrorResponse(),
|
60
|
+
},
|
61
|
+
)
|
62
|
+
def get(self, request: Request):
|
63
|
+
"""
|
64
|
+
Retrieve all shipment trackers.
|
65
|
+
"""
|
66
|
+
trackers = self.filter_queryset(self.get_queryset())
|
67
|
+
response = self.paginate_queryset(
|
68
|
+
serializers.TrackingStatus(trackers, many=True).data
|
69
|
+
)
|
70
|
+
return self.get_paginated_response(response)
|
71
|
+
|
72
|
+
@openapi.extend_schema(
|
73
|
+
tags=["Trackers"],
|
74
|
+
operation_id=f"{ENDPOINT_ID}add",
|
75
|
+
extensions={"x-operationId": "addTracker"},
|
76
|
+
summary="Add a package tracker",
|
77
|
+
request=serializers.TrackingData(),
|
78
|
+
responses={
|
79
|
+
200: serializers.TrackingStatus(),
|
80
|
+
400: serializers.ErrorResponse(),
|
81
|
+
424: serializers.ErrorMessages(),
|
82
|
+
500: serializers.ErrorResponse(),
|
83
|
+
},
|
84
|
+
parameters=[
|
85
|
+
openapi.OpenApiParameter(
|
86
|
+
"hub",
|
87
|
+
location=openapi.OpenApiParameter.QUERY,
|
88
|
+
type=openapi.OpenApiTypes.STR,
|
89
|
+
required=False,
|
90
|
+
),
|
91
|
+
openapi.OpenApiParameter(
|
92
|
+
"pending_pickup",
|
93
|
+
location=openapi.OpenApiParameter.QUERY,
|
94
|
+
type=openapi.OpenApiTypes.BOOL,
|
95
|
+
required=False,
|
96
|
+
description=(
|
97
|
+
"Add this flag to add the tracker whether the tracking info exist or not."
|
98
|
+
"When the package is eventually picked up, the tracker with capture real time updates."
|
99
|
+
),
|
100
|
+
),
|
101
|
+
],
|
102
|
+
)
|
103
|
+
def post(self, request: Request):
|
104
|
+
"""
|
105
|
+
This API creates or retrieves (if existent) a tracking status object containing the
|
106
|
+
details and events of a shipping in progress.
|
107
|
+
"""
|
108
|
+
query = request.query_params
|
109
|
+
serializer = serializers.TrackingData(data=request.data)
|
110
|
+
serializer.is_valid(raise_exception=True)
|
111
|
+
data = serializer.validated_data
|
112
|
+
|
113
|
+
carrier_name = query.get("hub") if "hub" in query else data["carrier_name"]
|
114
|
+
pending_pickup = serializers.get_query_flag(
|
115
|
+
"pending_pickup", query, nullable=False
|
116
|
+
)
|
117
|
+
|
118
|
+
instance = (
|
119
|
+
models.Tracking.access_by(request)
|
120
|
+
.filter(tracking_number=data["tracking_number"])
|
121
|
+
.first()
|
122
|
+
)
|
123
|
+
|
124
|
+
carrier_filter = {
|
125
|
+
**{k: v for k, v in query.items() if k != "hub"},
|
126
|
+
# If a hub is specified, use the hub as carrier to track the package
|
127
|
+
"carrier_name": carrier_name,
|
128
|
+
}
|
129
|
+
data = {
|
130
|
+
**data,
|
131
|
+
"tracking_number": data["tracking_number"],
|
132
|
+
"options": (
|
133
|
+
{data["tracking_number"]: {"carrier": data["carrier_name"]}}
|
134
|
+
if "hub" in query
|
135
|
+
else {}
|
136
|
+
),
|
137
|
+
}
|
138
|
+
|
139
|
+
tracker = (
|
140
|
+
serializers.TrackingSerializer.map(instance, data=data, context=request)
|
141
|
+
.save(carrier_filter=carrier_filter, pending_pickup=pending_pickup)
|
142
|
+
.instance
|
143
|
+
)
|
144
|
+
|
145
|
+
return Response(
|
146
|
+
serializers.TrackingStatus(tracker).data,
|
147
|
+
status=status.HTTP_202_ACCEPTED,
|
148
|
+
)
|
149
|
+
|
150
|
+
|
151
|
+
class TrackersCreate(APIView):
|
152
|
+
throttle_scope = "carrier_request"
|
153
|
+
|
154
|
+
@openapi.extend_schema(
|
155
|
+
tags=["Trackers"],
|
156
|
+
operation_id=f"{ENDPOINT_ID}create",
|
157
|
+
extensions={"x-operationId": "createTracker"},
|
158
|
+
summary="Create a package tracker",
|
159
|
+
deprecated=True,
|
160
|
+
responses={
|
161
|
+
200: serializers.TrackingStatus(),
|
162
|
+
400: serializers.ErrorResponse(),
|
163
|
+
424: serializers.ErrorMessages(),
|
164
|
+
500: serializers.ErrorResponse(),
|
165
|
+
},
|
166
|
+
parameters=[
|
167
|
+
openapi.OpenApiParameter(
|
168
|
+
"tracking_number",
|
169
|
+
location=openapi.OpenApiParameter.PATH,
|
170
|
+
type=openapi.OpenApiTypes.STR,
|
171
|
+
required=True,
|
172
|
+
),
|
173
|
+
openapi.OpenApiParameter(
|
174
|
+
"carrier_name",
|
175
|
+
location=openapi.OpenApiParameter.QUERY,
|
176
|
+
type=openapi.OpenApiTypes.STR,
|
177
|
+
enum=dataunits.NON_HUBS_CARRIERS,
|
178
|
+
required=True,
|
179
|
+
),
|
180
|
+
openapi.OpenApiParameter(
|
181
|
+
"hub",
|
182
|
+
location=openapi.OpenApiParameter.QUERY,
|
183
|
+
type=openapi.OpenApiTypes.STR,
|
184
|
+
required=False,
|
185
|
+
),
|
186
|
+
],
|
187
|
+
request=None,
|
188
|
+
)
|
189
|
+
def get(self, request: Request, carrier_name: str, tracking_number: str):
|
190
|
+
"""
|
191
|
+
This API creates or retrieves (if existent) a tracking status object containing the
|
192
|
+
details and events of a shipping in progress.
|
193
|
+
"""
|
194
|
+
instance = (
|
195
|
+
models.Tracking.access_by(request)
|
196
|
+
.filter(tracking_number=tracking_number)
|
197
|
+
.first()
|
198
|
+
)
|
199
|
+
|
200
|
+
query = request.query_params
|
201
|
+
carrier_filter = {
|
202
|
+
**{k: v for k, v in query.items() if k != "hub"},
|
203
|
+
# If a hub is specified, use the hub as carrier to track the package
|
204
|
+
"carrier_name": (query.get("hub") if "hub" in query else carrier_name),
|
205
|
+
}
|
206
|
+
data = {
|
207
|
+
"tracking_number": tracking_number,
|
208
|
+
"options": (
|
209
|
+
{tracking_number: {"carrier": carrier_name}} if "hub" in query else {}
|
210
|
+
),
|
211
|
+
}
|
212
|
+
|
213
|
+
tracker = (
|
214
|
+
serializers.TrackingSerializer.map(instance, data=data, context=request)
|
215
|
+
.save(carrier_filter=carrier_filter, pending_pickup=False)
|
216
|
+
.instance
|
217
|
+
)
|
218
|
+
|
219
|
+
return Response(
|
220
|
+
serializers.TrackingStatus(tracker).data,
|
221
|
+
status=status.HTTP_202_ACCEPTED,
|
222
|
+
)
|
223
|
+
|
224
|
+
|
225
|
+
class TrackersDetails(APIView):
|
226
|
+
throttle_scope = "carrier_request"
|
227
|
+
permission_classes = [IsAuthenticatedOrReadOnly]
|
228
|
+
|
229
|
+
@openapi.extend_schema(
|
230
|
+
tags=["Trackers"],
|
231
|
+
operation_id=f"{ENDPOINT_ID}retrieve",
|
232
|
+
extensions={"x-operationId": "retrieveTracker"},
|
233
|
+
summary="Retrieves a package tracker",
|
234
|
+
responses={
|
235
|
+
200: serializers.TrackingStatus(),
|
236
|
+
404: serializers.ErrorMessages(),
|
237
|
+
500: serializers.ErrorResponse(),
|
238
|
+
},
|
239
|
+
)
|
240
|
+
def get(self, request: Request, id_or_tracking_number: str):
|
241
|
+
"""
|
242
|
+
Retrieve a package tracker
|
243
|
+
"""
|
244
|
+
__filter = Q(pk=id_or_tracking_number) | Q(
|
245
|
+
tracking_number=id_or_tracking_number
|
246
|
+
)
|
247
|
+
trackers = models.Tracking.objects.filter(__filter)
|
248
|
+
|
249
|
+
if len(trackers) == 0:
|
250
|
+
models.Tracking.objects.get(__filter)
|
251
|
+
|
252
|
+
return Response(serializers.TrackingStatus(trackers.first()).data)
|
253
|
+
|
254
|
+
@openapi.extend_schema(
|
255
|
+
tags=["Trackers"],
|
256
|
+
operation_id=f"{ENDPOINT_ID}update",
|
257
|
+
extensions={"x-operationId": "updateTracker"},
|
258
|
+
summary="Update tracker data",
|
259
|
+
responses={
|
260
|
+
200: serializers.TrackingStatus(),
|
261
|
+
404: serializers.ErrorResponse(),
|
262
|
+
400: serializers.ErrorResponse(),
|
263
|
+
409: serializers.ErrorResponse(),
|
264
|
+
500: serializers.ErrorResponse(),
|
265
|
+
},
|
266
|
+
request=serializers.TrackerUpdateData(),
|
267
|
+
)
|
268
|
+
def put(self, request: Request, id_or_tracking_number: str):
|
269
|
+
tracker = models.Tracking.access_by(request).get(
|
270
|
+
Q(pk=id_or_tracking_number) | Q(tracking_number=id_or_tracking_number)
|
271
|
+
)
|
272
|
+
serializers.can_mutate_tracker(tracker, update=True, payload=request.data)
|
273
|
+
|
274
|
+
payload = serializers.TrackerUpdateData.map(data=request.data).data
|
275
|
+
update = (
|
276
|
+
serializers.TrackerUpdateData.map(
|
277
|
+
tracker,
|
278
|
+
context=request,
|
279
|
+
data=serializers.process_dictionaries_mutations(
|
280
|
+
["metadata", "options", "info"], payload, tracker
|
281
|
+
),
|
282
|
+
)
|
283
|
+
.save()
|
284
|
+
.instance
|
285
|
+
)
|
286
|
+
|
287
|
+
return Response(serializers.TrackingStatus(update).data)
|
288
|
+
|
289
|
+
@openapi.extend_schema(
|
290
|
+
tags=["Trackers"],
|
291
|
+
operation_id=f"{ENDPOINT_ID}remove",
|
292
|
+
extensions={"x-operationId": "removeTracker"},
|
293
|
+
summary="Discard a package tracker",
|
294
|
+
responses={
|
295
|
+
200: serializers.TrackingStatus(),
|
296
|
+
404: serializers.ErrorResponse(),
|
297
|
+
500: serializers.ErrorResponse(),
|
298
|
+
},
|
299
|
+
)
|
300
|
+
def delete(self, request: Request, id_or_tracking_number: str):
|
301
|
+
"""
|
302
|
+
Discard a package tracker.
|
303
|
+
"""
|
304
|
+
tracker = models.Tracking.access_by(request).get(
|
305
|
+
Q(pk=id_or_tracking_number) | Q(tracking_number=id_or_tracking_number)
|
306
|
+
)
|
307
|
+
|
308
|
+
tracker.delete(keep_parents=True)
|
309
|
+
|
310
|
+
return Response(serializers.TrackingStatus(tracker).data)
|
311
|
+
|
312
|
+
|
313
|
+
class TrackerDocs(django_downloadview.VirtualDownloadView):
|
314
|
+
@openapi.extend_schema(exclude=True)
|
315
|
+
def get(
|
316
|
+
self,
|
317
|
+
request: Request,
|
318
|
+
pk: str,
|
319
|
+
doc: str = "delivery_image",
|
320
|
+
**kwargs,
|
321
|
+
):
|
322
|
+
"""Retrieve a tracker image."""
|
323
|
+
self.tracker = models.Tracker.objects.get(pk=pk)
|
324
|
+
self.image = getattr(self.tracker, doc)
|
325
|
+
self.name = f"{doc}_{self.tracker.tracking_number}"
|
326
|
+
|
327
|
+
query_params = request.GET.dict()
|
328
|
+
self.preview = "preview" in query_params
|
329
|
+
self.attachment = "download" in query_params
|
330
|
+
|
331
|
+
response = super(TrackerDocs, self).get(request, pk, doc, format, **kwargs)
|
332
|
+
response["X-Frame-Options"] = "ALLOWALL"
|
333
|
+
return response
|
334
|
+
|
335
|
+
def get_file(self):
|
336
|
+
content = base64.b64decode(self.image or "")
|
337
|
+
buffer = io.BytesIO()
|
338
|
+
buffer.write(content)
|
339
|
+
|
340
|
+
return ContentFile(buffer.getvalue(), name=self.name)
|
341
|
+
|
342
|
+
|
343
|
+
router.urls.append(path("trackers", TrackerList.as_view(), name="trackers-list"))
|
344
|
+
router.urls.append(
|
345
|
+
path(
|
346
|
+
"trackers/<str:id_or_tracking_number>",
|
347
|
+
TrackersDetails.as_view(),
|
348
|
+
name="tracker-details",
|
349
|
+
)
|
350
|
+
)
|
351
|
+
router.urls.append(
|
352
|
+
path(
|
353
|
+
"trackers/<str:carrier_name>/<str:tracking_number>",
|
354
|
+
TrackersCreate.as_view(),
|
355
|
+
name="shipment-tracker",
|
356
|
+
)
|
357
|
+
)
|
358
|
+
router.urls.append(
|
359
|
+
re_path(
|
360
|
+
r"^trackers/(?P<pk>\w+)/(?P<doc>[a-z0-9]+)",
|
361
|
+
TrackerDocs.as_view(),
|
362
|
+
name="tracker-docs",
|
363
|
+
)
|
364
|
+
)
|