karrio-server-manager 2026.1__py3-none-any.whl → 2026.1.3__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/migrations/0070_add_meta_and_product_fields.py +98 -0
- karrio/server/manager/migrations/0071_product_proxy.py +25 -0
- karrio/server/manager/migrations/0072_populate_json_fields.py +267 -0
- karrio/server/manager/migrations/0073_make_shipment_fk_nullable.py +36 -0
- karrio/server/manager/migrations/0074_clean_model_refactoring.py +207 -0
- karrio/server/manager/migrations/0075_populate_template_meta.py +69 -0
- karrio/server/manager/migrations/0076_remove_customs_model.py +66 -0
- karrio/server/manager/migrations/0077_add_carrier_snapshot_fields.py +83 -0
- karrio/server/manager/migrations/0078_populate_carrier_snapshots.py +112 -0
- karrio/server/manager/migrations/0079_remove_carrier_fk_fields.py +56 -0
- karrio/server/manager/migrations/0080_add_carrier_json_indexes.py +137 -0
- karrio/server/manager/migrations/0081_cleanup.py +62 -0
- karrio/server/manager/migrations/0082_shipment_fees.py +26 -0
- karrio/server/manager/models.py +421 -321
- karrio/server/manager/serializers/__init__.py +5 -4
- karrio/server/manager/serializers/address.py +8 -2
- karrio/server/manager/serializers/commodity.py +11 -4
- karrio/server/manager/serializers/document.py +29 -15
- karrio/server/manager/serializers/manifest.py +6 -3
- karrio/server/manager/serializers/parcel.py +5 -2
- karrio/server/manager/serializers/pickup.py +194 -67
- karrio/server/manager/serializers/shipment.py +232 -152
- karrio/server/manager/serializers/tracking.py +53 -12
- karrio/server/manager/tests/__init__.py +0 -1
- karrio/server/manager/tests/test_addresses.py +53 -0
- karrio/server/manager/tests/test_parcels.py +50 -0
- karrio/server/manager/tests/test_pickups.py +286 -50
- karrio/server/manager/tests/test_products.py +597 -0
- karrio/server/manager/tests/test_shipments.py +237 -92
- karrio/server/manager/tests/test_trackers.py +65 -1
- karrio/server/manager/views/__init__.py +1 -1
- karrio/server/manager/views/addresses.py +38 -2
- karrio/server/manager/views/documents.py +1 -1
- karrio/server/manager/views/parcels.py +25 -2
- karrio/server/manager/views/products.py +239 -0
- karrio/server/manager/views/trackers.py +69 -1
- {karrio_server_manager-2026.1.dist-info → karrio_server_manager-2026.1.3.dist-info}/METADATA +1 -1
- {karrio_server_manager-2026.1.dist-info → karrio_server_manager-2026.1.3.dist-info}/RECORD +40 -28
- {karrio_server_manager-2026.1.dist-info → karrio_server_manager-2026.1.3.dist-info}/WHEEL +1 -1
- karrio/server/manager/serializers/customs.py +0 -84
- karrio/server/manager/tests/test_custom_infos.py +0 -101
- karrio/server/manager/views/customs.py +0 -159
- {karrio_server_manager-2026.1.dist-info → karrio_server_manager-2026.1.3.dist-info}/top_level.txt +0 -0
|
@@ -35,6 +35,14 @@ class AddressList(GenericAPIView):
|
|
|
35
35
|
operation_id=f"{ENDPOINT_ID}list",
|
|
36
36
|
extensions={"x-operationId": "listAddresses"},
|
|
37
37
|
summary="List all addresses",
|
|
38
|
+
description="""
|
|
39
|
+
Retrieve all addresses.
|
|
40
|
+
|
|
41
|
+
Query Parameters:
|
|
42
|
+
- label: Filter by meta.label (case-insensitive contains)
|
|
43
|
+
- keyword: Search across label, address fields, contact info
|
|
44
|
+
- usage: Filter by meta.usage (exact match in array)
|
|
45
|
+
""",
|
|
38
46
|
responses={
|
|
39
47
|
200: Addresses(),
|
|
40
48
|
404: ErrorResponse(),
|
|
@@ -45,14 +53,42 @@ class AddressList(GenericAPIView):
|
|
|
45
53
|
"""
|
|
46
54
|
Retrieve all addresses.
|
|
47
55
|
"""
|
|
48
|
-
|
|
56
|
+
from django.db.models import Q
|
|
57
|
+
|
|
58
|
+
queryset = models.Address.access_by(request).filter(
|
|
49
59
|
**{
|
|
50
60
|
f"{prop}__isnull": True
|
|
51
61
|
for prop in models.Address.HIDDEN_PROPS
|
|
52
62
|
if prop != "org"
|
|
53
63
|
}
|
|
54
64
|
)
|
|
55
|
-
|
|
65
|
+
|
|
66
|
+
# Apply query parameter filters
|
|
67
|
+
label = request.query_params.get("label")
|
|
68
|
+
keyword = request.query_params.get("keyword")
|
|
69
|
+
usage = request.query_params.get("usage")
|
|
70
|
+
|
|
71
|
+
if label:
|
|
72
|
+
queryset = queryset.filter(meta__label__icontains=label)
|
|
73
|
+
|
|
74
|
+
if keyword:
|
|
75
|
+
queryset = queryset.filter(
|
|
76
|
+
Q(meta__label__icontains=keyword)
|
|
77
|
+
| Q(address_line1__icontains=keyword)
|
|
78
|
+
| Q(address_line2__icontains=keyword)
|
|
79
|
+
| Q(postal_code__icontains=keyword)
|
|
80
|
+
| Q(person_name__icontains=keyword)
|
|
81
|
+
| Q(company_name__icontains=keyword)
|
|
82
|
+
| Q(country_code__icontains=keyword)
|
|
83
|
+
| Q(city__icontains=keyword)
|
|
84
|
+
| Q(email__icontains=keyword)
|
|
85
|
+
| Q(phone_number__icontains=keyword)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if usage:
|
|
89
|
+
queryset = queryset.filter(meta__usage__contains=usage)
|
|
90
|
+
|
|
91
|
+
response = self.paginate_queryset(Address(queryset, many=True).data)
|
|
56
92
|
return self.get_paginated_response(response)
|
|
57
93
|
|
|
58
94
|
@openapi.extend_schema(
|
|
@@ -74,7 +74,7 @@ class DocumentList(GenericAPIView):
|
|
|
74
74
|
models.Shipment.access_by(request)
|
|
75
75
|
.filter(
|
|
76
76
|
pk=request.data.get("shipment_id"),
|
|
77
|
-
|
|
77
|
+
selected_rate__isnull=False, # Ensure shipment was purchased (has carrier info)
|
|
78
78
|
)
|
|
79
79
|
.first()
|
|
80
80
|
)
|
|
@@ -36,6 +36,14 @@ class ParcelList(GenericAPIView):
|
|
|
36
36
|
operation_id=f"{ENDPOINT_ID}list",
|
|
37
37
|
extensions={"x-operationId": "listParcels"},
|
|
38
38
|
summary="List all parcels",
|
|
39
|
+
description="""
|
|
40
|
+
Retrieve all stored parcels.
|
|
41
|
+
|
|
42
|
+
Query Parameters:
|
|
43
|
+
- label: Filter by meta.label (case-insensitive contains)
|
|
44
|
+
- keyword: Search by label
|
|
45
|
+
- usage: Filter by meta.usage (exact match in array)
|
|
46
|
+
""",
|
|
39
47
|
responses={
|
|
40
48
|
200: Parcels(),
|
|
41
49
|
404: ErrorResponse(),
|
|
@@ -46,14 +54,29 @@ class ParcelList(GenericAPIView):
|
|
|
46
54
|
"""
|
|
47
55
|
Retrieve all stored parcels.
|
|
48
56
|
"""
|
|
49
|
-
|
|
57
|
+
queryset = models.Parcel.access_by(request).filter(
|
|
50
58
|
**{
|
|
51
59
|
f"{prop}__isnull": True
|
|
52
60
|
for prop in models.Parcel.HIDDEN_PROPS
|
|
53
61
|
if prop != "org"
|
|
54
62
|
}
|
|
55
63
|
)
|
|
56
|
-
|
|
64
|
+
|
|
65
|
+
# Apply query parameter filters
|
|
66
|
+
label = request.query_params.get("label")
|
|
67
|
+
keyword = request.query_params.get("keyword")
|
|
68
|
+
usage = request.query_params.get("usage")
|
|
69
|
+
|
|
70
|
+
if label:
|
|
71
|
+
queryset = queryset.filter(meta__label__icontains=label)
|
|
72
|
+
|
|
73
|
+
if keyword:
|
|
74
|
+
queryset = queryset.filter(meta__label__icontains=keyword)
|
|
75
|
+
|
|
76
|
+
if usage:
|
|
77
|
+
queryset = queryset.filter(meta__usage__contains=usage)
|
|
78
|
+
|
|
79
|
+
serializer = Parcel(queryset, many=True)
|
|
57
80
|
response = self.paginate_queryset(serializer.data)
|
|
58
81
|
|
|
59
82
|
return self.get_paginated_response(response)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Product REST API Views
|
|
3
|
+
|
|
4
|
+
Provides full CRUD operations for product templates (commodities with meta.label).
|
|
5
|
+
Products are reusable commodity templates for customs declarations and shipment items.
|
|
6
|
+
|
|
7
|
+
Endpoints:
|
|
8
|
+
GET /v1/products - List all products
|
|
9
|
+
POST /v1/products - Create a new product
|
|
10
|
+
GET /v1/products/<pk> - Retrieve a product
|
|
11
|
+
PATCH /v1/products/<pk> - Update a product
|
|
12
|
+
DELETE /v1/products/<pk> - Delete a product
|
|
13
|
+
"""
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
from django.urls import path
|
|
17
|
+
from rest_framework import status
|
|
18
|
+
from rest_framework.request import Request
|
|
19
|
+
from rest_framework.response import Response
|
|
20
|
+
from rest_framework.pagination import LimitOffsetPagination
|
|
21
|
+
|
|
22
|
+
from karrio.server.manager.router import router
|
|
23
|
+
from karrio.server.core.views.api import GenericAPIView, APIView
|
|
24
|
+
from karrio.server.manager.serializers import (
|
|
25
|
+
PaginatedResult,
|
|
26
|
+
ErrorResponse,
|
|
27
|
+
CommodityData,
|
|
28
|
+
Commodity,
|
|
29
|
+
CommoditySerializer,
|
|
30
|
+
can_mutate_commodity,
|
|
31
|
+
)
|
|
32
|
+
import karrio.server.manager.models as models
|
|
33
|
+
import karrio.server.openapi as openapi
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# Unique endpoint ID for OpenAPI operation IDs
|
|
38
|
+
ENDPOINT_ID = "$&" # This endpoint id is used to make operation ids unique make sure not to duplicate
|
|
39
|
+
|
|
40
|
+
# Response serializer for paginated list
|
|
41
|
+
Products = PaginatedResult("ProductList", Commodity)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ProductList(GenericAPIView):
|
|
45
|
+
"""
|
|
46
|
+
List and create product templates.
|
|
47
|
+
|
|
48
|
+
Products are commodity templates that can be reused across shipments
|
|
49
|
+
and customs declarations. They are identified by having a `meta.label` field.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
queryset = models.Commodity.objects
|
|
53
|
+
pagination_class = type(
|
|
54
|
+
"CustomPagination", (LimitOffsetPagination,), dict(default_limit=20)
|
|
55
|
+
)
|
|
56
|
+
serializer_class = Products
|
|
57
|
+
|
|
58
|
+
@openapi.extend_schema(
|
|
59
|
+
tags=["Products"],
|
|
60
|
+
operation_id=f"{ENDPOINT_ID}list",
|
|
61
|
+
extensions={"x-operationId": "listProducts"},
|
|
62
|
+
summary="List all products",
|
|
63
|
+
description="""
|
|
64
|
+
Retrieve all product templates.
|
|
65
|
+
|
|
66
|
+
Products are reusable commodity definitions that can be used
|
|
67
|
+
in customs declarations and shipment items.
|
|
68
|
+
|
|
69
|
+
Query Parameters:
|
|
70
|
+
- label: Filter by meta.label (case-insensitive contains)
|
|
71
|
+
- keyword: Search across label, title, sku, description, hs_code
|
|
72
|
+
- usage: Filter by meta.usage (exact match in array)
|
|
73
|
+
""",
|
|
74
|
+
responses={
|
|
75
|
+
200: Products(),
|
|
76
|
+
404: ErrorResponse(),
|
|
77
|
+
500: ErrorResponse(),
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
def get(self, request: Request):
|
|
81
|
+
"""
|
|
82
|
+
Retrieve all stored products.
|
|
83
|
+
"""
|
|
84
|
+
from django.db.models import Q
|
|
85
|
+
|
|
86
|
+
# Filter to only show product templates (those with meta.label)
|
|
87
|
+
# Also exclude products linked to other entities (parcels, customs)
|
|
88
|
+
queryset = models.Commodity.access_by(request).filter(
|
|
89
|
+
meta__label__isnull=False, # Only templates with labels
|
|
90
|
+
**{
|
|
91
|
+
f"{prop}__isnull": True
|
|
92
|
+
for prop in models.Commodity.HIDDEN_PROPS
|
|
93
|
+
if prop != "org"
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Apply query parameter filters
|
|
98
|
+
label = request.query_params.get("label")
|
|
99
|
+
keyword = request.query_params.get("keyword")
|
|
100
|
+
usage = request.query_params.get("usage")
|
|
101
|
+
|
|
102
|
+
if label:
|
|
103
|
+
queryset = queryset.filter(meta__label__icontains=label)
|
|
104
|
+
|
|
105
|
+
if keyword:
|
|
106
|
+
queryset = queryset.filter(
|
|
107
|
+
Q(meta__label__icontains=keyword)
|
|
108
|
+
| Q(title__icontains=keyword)
|
|
109
|
+
| Q(sku__icontains=keyword)
|
|
110
|
+
| Q(description__icontains=keyword)
|
|
111
|
+
| Q(hs_code__icontains=keyword)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if usage:
|
|
115
|
+
queryset = queryset.filter(meta__usage__contains=usage)
|
|
116
|
+
|
|
117
|
+
serializer = Commodity(queryset, many=True)
|
|
118
|
+
response = self.paginate_queryset(serializer.data)
|
|
119
|
+
|
|
120
|
+
return self.get_paginated_response(response)
|
|
121
|
+
|
|
122
|
+
@openapi.extend_schema(
|
|
123
|
+
tags=["Products"],
|
|
124
|
+
operation_id=f"{ENDPOINT_ID}create",
|
|
125
|
+
extensions={"x-operationId": "createProduct"},
|
|
126
|
+
summary="Create a product",
|
|
127
|
+
description="""
|
|
128
|
+
Create a new product template.
|
|
129
|
+
|
|
130
|
+
Products must include a `meta.label` to be identified as templates.
|
|
131
|
+
""",
|
|
132
|
+
request=CommodityData(),
|
|
133
|
+
responses={
|
|
134
|
+
201: Commodity(),
|
|
135
|
+
400: ErrorResponse(),
|
|
136
|
+
500: ErrorResponse(),
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
def post(self, request: Request):
|
|
140
|
+
"""
|
|
141
|
+
Create a new product template.
|
|
142
|
+
"""
|
|
143
|
+
# Ensure meta.label is set for product templates
|
|
144
|
+
data = request.data.copy() if hasattr(request.data, 'copy') else dict(request.data)
|
|
145
|
+
|
|
146
|
+
# Validate that meta.label is provided
|
|
147
|
+
meta = data.get("meta", {}) or {}
|
|
148
|
+
if not meta.get("label"):
|
|
149
|
+
return Response(
|
|
150
|
+
{"error": "Product templates require a meta.label field"},
|
|
151
|
+
status=status.HTTP_400_BAD_REQUEST
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
product = (
|
|
155
|
+
CommoditySerializer.map(data=data, context=request).save().instance
|
|
156
|
+
)
|
|
157
|
+
return Response(Commodity(product).data, status=status.HTTP_201_CREATED)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ProductDetail(APIView):
|
|
161
|
+
"""
|
|
162
|
+
Retrieve, update, and delete individual product templates.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
@openapi.extend_schema(
|
|
166
|
+
tags=["Products"],
|
|
167
|
+
operation_id=f"{ENDPOINT_ID}retrieve",
|
|
168
|
+
extensions={"x-operationId": "retrieveProduct"},
|
|
169
|
+
summary="Retrieve a product",
|
|
170
|
+
description="Retrieve a product template by ID.",
|
|
171
|
+
responses={
|
|
172
|
+
200: Commodity(),
|
|
173
|
+
404: ErrorResponse(),
|
|
174
|
+
500: ErrorResponse(),
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
def get(self, request: Request, pk: str):
|
|
178
|
+
"""
|
|
179
|
+
Retrieve a product template.
|
|
180
|
+
"""
|
|
181
|
+
product = models.Commodity.access_by(request).get(pk=pk)
|
|
182
|
+
return Response(Commodity(product).data)
|
|
183
|
+
|
|
184
|
+
@openapi.extend_schema(
|
|
185
|
+
tags=["Products"],
|
|
186
|
+
operation_id=f"{ENDPOINT_ID}update",
|
|
187
|
+
extensions={"x-operationId": "updateProduct"},
|
|
188
|
+
summary="Update a product",
|
|
189
|
+
description="Update an existing product template.",
|
|
190
|
+
request=CommodityData(),
|
|
191
|
+
responses={
|
|
192
|
+
200: Commodity(),
|
|
193
|
+
400: ErrorResponse(),
|
|
194
|
+
404: ErrorResponse(),
|
|
195
|
+
409: ErrorResponse(),
|
|
196
|
+
500: ErrorResponse(),
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
def patch(self, request: Request, pk: str):
|
|
200
|
+
"""
|
|
201
|
+
Update an existing product template.
|
|
202
|
+
"""
|
|
203
|
+
product = models.Commodity.access_by(request).get(pk=pk)
|
|
204
|
+
can_mutate_commodity(product, update=True)
|
|
205
|
+
|
|
206
|
+
CommoditySerializer.map(product, data=request.data).save()
|
|
207
|
+
|
|
208
|
+
return Response(Commodity(product).data)
|
|
209
|
+
|
|
210
|
+
@openapi.extend_schema(
|
|
211
|
+
tags=["Products"],
|
|
212
|
+
operation_id=f"{ENDPOINT_ID}discard",
|
|
213
|
+
extensions={"x-operationId": "discardProduct"},
|
|
214
|
+
summary="Remove a product",
|
|
215
|
+
description="Delete a product template.",
|
|
216
|
+
responses={
|
|
217
|
+
200: Commodity(),
|
|
218
|
+
404: ErrorResponse(),
|
|
219
|
+
409: ErrorResponse(),
|
|
220
|
+
500: ErrorResponse(),
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
def delete(self, request: Request, pk: str):
|
|
224
|
+
"""
|
|
225
|
+
Remove a product template.
|
|
226
|
+
"""
|
|
227
|
+
product = models.Commodity.access_by(request).get(pk=pk)
|
|
228
|
+
can_mutate_commodity(product, update=True, delete=True)
|
|
229
|
+
|
|
230
|
+
product.delete(keep_parents=True)
|
|
231
|
+
|
|
232
|
+
return Response(Commodity(product).data)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# Register routes with the router
|
|
236
|
+
router.urls.append(path("products", ProductList.as_view(), name="product-list"))
|
|
237
|
+
router.urls.append(
|
|
238
|
+
path("products/<str:pk>", ProductDetail.as_view(), name="product-details")
|
|
239
|
+
)
|
|
@@ -42,7 +42,8 @@ class TrackerList(GenericAPIView):
|
|
|
42
42
|
carrier_name = query_params.get("carrier_name")
|
|
43
43
|
|
|
44
44
|
if carrier_name is not None:
|
|
45
|
-
|
|
45
|
+
# Filter by carrier_code in the carrier JSON snapshot
|
|
46
|
+
_filters += (Q(carrier__carrier_code=carrier_name),)
|
|
46
47
|
|
|
47
48
|
return queryset.filter(*_filters)
|
|
48
49
|
|
|
@@ -339,6 +340,65 @@ class TrackerDocs(django_downloadview.VirtualDownloadView):
|
|
|
339
340
|
return ContentFile(buffer.getvalue(), name=self.name)
|
|
340
341
|
|
|
341
342
|
|
|
343
|
+
class TrackerEventInject(APIView):
|
|
344
|
+
"""
|
|
345
|
+
Inbound tracking event API for event injection (testing purposes).
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
@openapi.extend_schema(
|
|
349
|
+
tags=["Trackers"],
|
|
350
|
+
operation_id=f"{ENDPOINT_ID}inject",
|
|
351
|
+
extensions={"x-operationId": "injectTrackingEvents"},
|
|
352
|
+
summary="Inject tracking events",
|
|
353
|
+
description="Inject tracking events into an existing tracker for testing purposes.",
|
|
354
|
+
request=serializers.TrackerEventInjectRequest(),
|
|
355
|
+
responses={
|
|
356
|
+
200: serializers.Operation(),
|
|
357
|
+
400: serializers.ErrorResponse(),
|
|
358
|
+
404: serializers.ErrorResponse(),
|
|
359
|
+
500: serializers.ErrorResponse(),
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
def post(self, request: Request, tracker_id: str):
|
|
363
|
+
"""
|
|
364
|
+
Inject tracking events into an existing tracker.
|
|
365
|
+
|
|
366
|
+
This endpoint allows injecting events for testing the tracking pipeline.
|
|
367
|
+
"""
|
|
368
|
+
import karrio.lib as lib
|
|
369
|
+
import karrio.server.manager.serializers.tracking as tracking_serializers
|
|
370
|
+
|
|
371
|
+
tracker = models.Tracking.access_by(request).get(pk=tracker_id)
|
|
372
|
+
|
|
373
|
+
serializer = serializers.TrackerEventInjectRequest(data=request.data)
|
|
374
|
+
serializer.is_valid(raise_exception=True)
|
|
375
|
+
|
|
376
|
+
data = serializer.validated_data
|
|
377
|
+
events = lib.to_dict(data.get("events", []))
|
|
378
|
+
|
|
379
|
+
# Build tracking details update payload
|
|
380
|
+
tracking_details = dict(events=events)
|
|
381
|
+
|
|
382
|
+
if data.get("status") is not None:
|
|
383
|
+
tracking_details["status"] = data["status"]
|
|
384
|
+
|
|
385
|
+
if data.get("delivered"):
|
|
386
|
+
tracking_details["delivered"] = data["delivered"]
|
|
387
|
+
|
|
388
|
+
if data.get("estimated_delivery") is not None:
|
|
389
|
+
tracking_details["estimated_delivery"] = data["estimated_delivery"]
|
|
390
|
+
|
|
391
|
+
# Use the centralized update_tracker function
|
|
392
|
+
tracking_serializers.update_tracker(tracker, tracking_details)
|
|
393
|
+
|
|
394
|
+
return Response(
|
|
395
|
+
serializers.Operation(
|
|
396
|
+
dict(operation="Inject Tracking Events", success=True)
|
|
397
|
+
).data,
|
|
398
|
+
status=status.HTTP_200_OK,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
342
402
|
router.urls.append(path("trackers", TrackerList.as_view(), name="trackers-list"))
|
|
343
403
|
router.urls.append(
|
|
344
404
|
path(
|
|
@@ -347,6 +407,14 @@ router.urls.append(
|
|
|
347
407
|
name="tracker-details",
|
|
348
408
|
)
|
|
349
409
|
)
|
|
410
|
+
# Register inject-events BEFORE shipment-tracker to avoid matching ambiguity
|
|
411
|
+
router.urls.append(
|
|
412
|
+
path(
|
|
413
|
+
"trackers/<str:tracker_id>/inject-events",
|
|
414
|
+
TrackerEventInject.as_view(),
|
|
415
|
+
name="tracker-events-inject",
|
|
416
|
+
)
|
|
417
|
+
)
|
|
350
418
|
router.urls.append(
|
|
351
419
|
path(
|
|
352
420
|
"trackers/<str:carrier_name>/<str:tracking_number>",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
karrio/server/manager/__init__.py,sha256=lDTMs_O6mQl0DEI2_TniT24TDJbjnkla-5QpnwfYlxs,64
|
|
2
2
|
karrio/server/manager/admin.py,sha256=QOl5e2m3ekU5aj0yj9Uq4nRQrNMB_FfqNae6RyIxkC0,35
|
|
3
3
|
karrio/server/manager/apps.py,sha256=WHTQ1t79uDZTbinRzvNg1NjtFwnwEvg0tP_ChrtTRwI,364
|
|
4
|
-
karrio/server/manager/models.py,sha256=
|
|
4
|
+
karrio/server/manager/models.py,sha256=M0VZOTMvy1r_mw5bl2zvnWUw9a0g0KsS3X9bWFLN-tc,42323
|
|
5
5
|
karrio/server/manager/router.py,sha256=IBUR7rfBkdEHQzWxYOPcVSM8NBp3fte9G6Q5BVTUNNw,95
|
|
6
6
|
karrio/server/manager/signals.py,sha256=3ZApZY4Ne8Gb0AT5rjC3Xneb7dnbSQyXp1OiBXt7eIA,1908
|
|
7
7
|
karrio/server/manager/urls.py,sha256=oXJlvhHNKxFkc_CmpFoyTSAMJcLp4Wt9dHbViQDkqw4,220
|
|
@@ -75,37 +75,49 @@ karrio/server/manager/migrations/0066_commodity_image_url_commodity_product_id_a
|
|
|
75
75
|
karrio/server/manager/migrations/0067_rename_customs_commodities_table.py,sha256=WPACK9Ab5QUdcX9yxBkV6_MXTAkB9F8vETDOlVqOrT4,1933
|
|
76
76
|
karrio/server/manager/migrations/0068_shipment_extra_documents.py,sha256=DL4FqB5bf5Rq3YuWVJsBMStkwMDmHIp_WN1BA6hKSTI,536
|
|
77
77
|
karrio/server/manager/migrations/0069_alter_tracking_status.py,sha256=p7rra3Iva_FT6eBmfKcpvC-E5Zll4bH69kMX9Us3Q-w,1170
|
|
78
|
+
karrio/server/manager/migrations/0070_add_meta_and_product_fields.py,sha256=R7I0nQ8US9QAdvc0taUcz1i8-p0CJM_4HMdAuAmC-a0,3168
|
|
79
|
+
karrio/server/manager/migrations/0071_product_proxy.py,sha256=-N6zIebgDOKXA-FeRZJ2Bo9YWiCzS8Kz3r7HGQdPVGE,599
|
|
80
|
+
karrio/server/manager/migrations/0072_populate_json_fields.py,sha256=SCiNZa6kL6dvOlrDWcxjGVsGQaXTloYToRnOJ__Qb3s,9418
|
|
81
|
+
karrio/server/manager/migrations/0073_make_shipment_fk_nullable.py,sha256=gSFv0VVk7kotkjAVJgCJi4e21sQZbOKorPMHmYElVEk,999
|
|
82
|
+
karrio/server/manager/migrations/0074_clean_model_refactoring.py,sha256=tpupc0RcrEqI2L-sv0m2W_AP5P0SAaKihPq8lWJT3pU,6913
|
|
83
|
+
karrio/server/manager/migrations/0075_populate_template_meta.py,sha256=dAJE3bO1we8yjeV0HQ6RVZvmoPklCuiI-JRsf9HxJjo,2319
|
|
84
|
+
karrio/server/manager/migrations/0076_remove_customs_model.py,sha256=LjH46huACsJkxgrYp79lMM5999VT_AnxIHVO-ZjV0vY,2491
|
|
85
|
+
karrio/server/manager/migrations/0077_add_carrier_snapshot_fields.py,sha256=erC-lLS88PnLXmFDEAz-_Uk5vuLkJxwdPgvLkJqaiTg,2722
|
|
86
|
+
karrio/server/manager/migrations/0078_populate_carrier_snapshots.py,sha256=KuaXeiBqKRhS3LNkiYlErR3sH2q7w-NkyenwOPiVUmk,4502
|
|
87
|
+
karrio/server/manager/migrations/0079_remove_carrier_fk_fields.py,sha256=xDOEq4aPtxw6zM1u1lqlYco_TQBeyUMq570y3uFl2j0,1652
|
|
88
|
+
karrio/server/manager/migrations/0080_add_carrier_json_indexes.py,sha256=P7FUoX5HdXfi8GkcUjFAyA-HbtXBbAM3EXiqdIElxn4,6019
|
|
89
|
+
karrio/server/manager/migrations/0081_cleanup.py,sha256=cvON11RAgUskyXAMKrBwxIiPNHP0WbhIB31DnN3rp0w,1932
|
|
90
|
+
karrio/server/manager/migrations/0082_shipment_fees.py,sha256=CWJc-ZGITLnN2ln6XZDBIgtbDHdQYYz9AyRQpq1VChc,719
|
|
78
91
|
karrio/server/manager/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
79
|
-
karrio/server/manager/serializers/__init__.py,sha256=
|
|
80
|
-
karrio/server/manager/serializers/address.py,sha256=
|
|
81
|
-
karrio/server/manager/serializers/commodity.py,sha256=
|
|
82
|
-
karrio/server/manager/serializers/
|
|
83
|
-
karrio/server/manager/serializers/
|
|
84
|
-
karrio/server/manager/serializers/
|
|
85
|
-
karrio/server/manager/serializers/
|
|
86
|
-
karrio/server/manager/serializers/pickup.py,sha256=sX0VmcQxGkXn3IEosMuFwdXh4HhdkPcuBOp79O8PoDQ,9233
|
|
92
|
+
karrio/server/manager/serializers/__init__.py,sha256=AW1RpB3rNLKbMD85rCFFiVcpngj-kPUNdyb1hkaYDrk,1513
|
|
93
|
+
karrio/server/manager/serializers/address.py,sha256=jc_D9gQb0m7ObO6RHk6SEfZipNRPCH4sPKkd_sa7T_Y,2950
|
|
94
|
+
karrio/server/manager/serializers/commodity.py,sha256=tPbGXuDd6N2xqRDWCXTKnSnl4-e1MnHIKe0KdCfQYpA,1897
|
|
95
|
+
karrio/server/manager/serializers/document.py,sha256=MFa8lqcy4Qa0RAUdn4D02AO5cY4_EgxC-aabbxqr-tI,4611
|
|
96
|
+
karrio/server/manager/serializers/manifest.py,sha256=dyEG2A9rWj59Khs5d8ltgRrUWXpk32GU4a2gZNFFU_c,3114
|
|
97
|
+
karrio/server/manager/serializers/parcel.py,sha256=Wfu2trm2pqiOWhsYG-GElhRHSS4HTW5dttkpVNdfXd4,2761
|
|
98
|
+
karrio/server/manager/serializers/pickup.py,sha256=UOIG0trWxjE12jIDyayaoEKvzepmvcSAmER7dSf8WQo,15211
|
|
87
99
|
karrio/server/manager/serializers/rate.py,sha256=7vYK_v8iWEDnswqYHG2Lir16_UhHTOxW5rdC6lw3lzA,652
|
|
88
|
-
karrio/server/manager/serializers/shipment.py,sha256=
|
|
89
|
-
karrio/server/manager/serializers/tracking.py,sha256=
|
|
90
|
-
karrio/server/manager/tests/__init__.py,sha256=
|
|
91
|
-
karrio/server/manager/tests/test_addresses.py,sha256=
|
|
92
|
-
karrio/server/manager/tests/test_custom_infos.py,sha256=iv2cLdZVoVWFZK_mDUEnrZssncAnQcn87Rn2sAk8UQI,2731
|
|
100
|
+
karrio/server/manager/serializers/shipment.py,sha256=uM775c5L0sfgW8kTbUwqCOrnVWoCQfsWy1awQ7dRt5M,41343
|
|
101
|
+
karrio/server/manager/serializers/tracking.py,sha256=HCl-WeCF-AjFaTdEd4xSb068NIKcMH-Hbo0QRMSYrvY,14319
|
|
102
|
+
karrio/server/manager/tests/__init__.py,sha256=hAN3DX8-0c442XC46yq-jDXMrtHbCV88P-sBqp8g1_M,331
|
|
103
|
+
karrio/server/manager/tests/test_addresses.py,sha256=4eiFQ_6YhLLrfBuLUbYiIfqeRiPJPcBwmykJ67i6xKw,5018
|
|
93
104
|
karrio/server/manager/tests/test_errors.py,sha256=x2-mSsXknHkE4V7TajEu8d3rpqV38T_xyAaYJU7xcGQ,3616
|
|
94
105
|
karrio/server/manager/tests/test_manifests.py,sha256=X35ZTXTFEM4Gxdjz598yiNNkOOKZGpILjHWRC0oM5U4,2764
|
|
95
|
-
karrio/server/manager/tests/test_parcels.py,sha256=
|
|
96
|
-
karrio/server/manager/tests/test_pickups.py,sha256=
|
|
97
|
-
karrio/server/manager/tests/
|
|
98
|
-
karrio/server/manager/tests/
|
|
99
|
-
karrio/server/manager/
|
|
100
|
-
karrio/server/manager/views/
|
|
101
|
-
karrio/server/manager/views/
|
|
102
|
-
karrio/server/manager/views/documents.py,sha256=
|
|
106
|
+
karrio/server/manager/tests/test_parcels.py,sha256=nB5SaTjwj_ZF2962ZIGy-DXq6XHXc15uhnV3WtKjuUU,4355
|
|
107
|
+
karrio/server/manager/tests/test_pickups.py,sha256=M8x4j12Zw3vp_L8Xw2Tw9oUzopLJK4QVEfvNn5fZyGk,19081
|
|
108
|
+
karrio/server/manager/tests/test_products.py,sha256=mK0TxjO2SBllbl_rB0foZQqTjCPR1qky_yeCVCZvzOY,20984
|
|
109
|
+
karrio/server/manager/tests/test_shipments.py,sha256=G2HdzzruZcs9M66fU6sWb763DAfbL8E7if8XVjcYS48,45545
|
|
110
|
+
karrio/server/manager/tests/test_trackers.py,sha256=pDWxYYCFxjZJTLrYbTp1YReexpIeOBXEU8xifht4-D4,9959
|
|
111
|
+
karrio/server/manager/views/__init__.py,sha256=BwQ7e9oXStfs4JN_ZztatrVH5BlQZn9Ls0mhrQb8kp0,402
|
|
112
|
+
karrio/server/manager/views/addresses.py,sha256=bwW0x75W0n1NxOfP0CdXYbnE25_YZi3xHYEbRY5cQT0,5908
|
|
113
|
+
karrio/server/manager/views/documents.py,sha256=8Lh49y9LZEDiwBJ77pq4EQVtTv04U-68hc7zzY-gwk4,4049
|
|
103
114
|
karrio/server/manager/views/manifests.py,sha256=KwDoV7GSdRv7YDAzl0SLaNoAx4R-gBguO_WiW_qwfBM,7083
|
|
104
|
-
karrio/server/manager/views/parcels.py,sha256=
|
|
115
|
+
karrio/server/manager/views/parcels.py,sha256=mF0BzP92_FLdBohnWaXecQGWWaibudoe3KsHZ3yPkR8,5322
|
|
105
116
|
karrio/server/manager/views/pickups.py,sha256=gmpxz9ot1OR-BP1qh-0MXU3kUJi1ht_74hfaLJzJ42w,5503
|
|
117
|
+
karrio/server/manager/views/products.py,sha256=1ZxODea-slOhrAZ9pazviLRuJv0I90f4NRTPhA_LxzM,7762
|
|
106
118
|
karrio/server/manager/views/shipments.py,sha256=vdJFgIKnSHEb2FVGtqJbiMoHuMqzWtDpINMH4UaEtQQ,13154
|
|
107
|
-
karrio/server/manager/views/trackers.py,sha256=
|
|
108
|
-
karrio_server_manager-2026.1.dist-info/METADATA,sha256=
|
|
109
|
-
karrio_server_manager-2026.1.dist-info/WHEEL,sha256=
|
|
110
|
-
karrio_server_manager-2026.1.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
|
|
111
|
-
karrio_server_manager-2026.1.dist-info/RECORD,,
|
|
119
|
+
karrio/server/manager/views/trackers.py,sha256=9uRjNiyqTULmKFOminGgIXj-H0RW-e0JUAF6Z3vMNCY,14744
|
|
120
|
+
karrio_server_manager-2026.1.3.dist-info/METADATA,sha256=C7RRJeRwFZdmmk4DArDcABxeHCzLp8qrLvoMvukR654,730
|
|
121
|
+
karrio_server_manager-2026.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
122
|
+
karrio_server_manager-2026.1.3.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
|
|
123
|
+
karrio_server_manager-2026.1.3.dist-info/RECORD,,
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
from django.db import transaction
|
|
2
|
-
from rest_framework import status
|
|
3
|
-
|
|
4
|
-
import karrio.server.manager.models as models
|
|
5
|
-
import karrio.server.serializers as serializers
|
|
6
|
-
import karrio.server.core.exceptions as exceptions
|
|
7
|
-
from karrio.server.core.serializers import CustomsData, ShipmentStatus
|
|
8
|
-
from karrio.server.manager.serializers.address import AddressSerializer
|
|
9
|
-
from karrio.server.manager.serializers.commodity import CommoditySerializer
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@serializers.owned_model_serializer
|
|
13
|
-
class CustomsSerializer(CustomsData):
|
|
14
|
-
def __init__(self, instance: models.Customs = None, **kwargs):
|
|
15
|
-
data = kwargs.get("data") or {}
|
|
16
|
-
|
|
17
|
-
if ("commodities" in data) and (instance is not None):
|
|
18
|
-
context = getattr(self, "__context", None) or kwargs.get("context")
|
|
19
|
-
serializers.save_many_to_many_data(
|
|
20
|
-
"commodities",
|
|
21
|
-
CommoditySerializer,
|
|
22
|
-
instance,
|
|
23
|
-
payload=data,
|
|
24
|
-
context=context,
|
|
25
|
-
partial=True,
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
super().__init__(instance, **kwargs)
|
|
29
|
-
|
|
30
|
-
@transaction.atomic
|
|
31
|
-
def create(self, validated_data: dict, context: dict, **kwargs) -> models.Customs:
|
|
32
|
-
instance = models.Customs.objects.create(
|
|
33
|
-
**{
|
|
34
|
-
**{
|
|
35
|
-
key: value
|
|
36
|
-
for key, value in validated_data.items()
|
|
37
|
-
if key in models.Customs.DIRECT_PROPS
|
|
38
|
-
},
|
|
39
|
-
"duty_billing_address": serializers.save_one_to_one_data(
|
|
40
|
-
"duty_billing_address",
|
|
41
|
-
AddressSerializer,
|
|
42
|
-
payload=validated_data,
|
|
43
|
-
context=context,
|
|
44
|
-
),
|
|
45
|
-
}
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
serializers.save_many_to_many_data(
|
|
49
|
-
"commodities",
|
|
50
|
-
CommoditySerializer,
|
|
51
|
-
instance,
|
|
52
|
-
payload=validated_data,
|
|
53
|
-
context=context,
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
return instance
|
|
57
|
-
|
|
58
|
-
@transaction.atomic
|
|
59
|
-
def update(
|
|
60
|
-
self, instance: models.Customs, validated_data: dict, **kwargs
|
|
61
|
-
) -> models.Customs:
|
|
62
|
-
data = serializers.process_dictionaries_mutations(
|
|
63
|
-
["options"], validated_data, instance
|
|
64
|
-
)
|
|
65
|
-
changes = []
|
|
66
|
-
|
|
67
|
-
for key, val in data.items():
|
|
68
|
-
if key in models.Customs.DIRECT_PROPS and getattr(instance, key) != val:
|
|
69
|
-
changes.append(key)
|
|
70
|
-
setattr(instance, key, val)
|
|
71
|
-
|
|
72
|
-
instance.save(update_fields=changes)
|
|
73
|
-
return instance
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def can_mutate_customs(customs: models.Customs, **kwargs):
|
|
77
|
-
shipment = customs.shipment
|
|
78
|
-
|
|
79
|
-
if shipment is not None and shipment.status != ShipmentStatus.create.value:
|
|
80
|
-
raise exceptions.APIException(
|
|
81
|
-
f"Operation not permitted. The related shipment is '{shipment.status}'.",
|
|
82
|
-
status_code=status.HTTP_409_CONFLICT,
|
|
83
|
-
code="state_error",
|
|
84
|
-
)
|