karrio-server-manager 2025.5rc35__py3-none-any.whl → 2025.5.1__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/serializers/__init__.py +1 -0
- karrio/server/manager/serializers/shipment.py +39 -5
- karrio/server/manager/serializers/tracking.py +134 -55
- karrio/server/manager/tests/test_errors.py +88 -0
- karrio/server/manager/tests/test_shipments.py +192 -0
- karrio/server/manager/views/manifests.py +11 -13
- karrio/server/manager/views/shipments.py +9 -3
- {karrio_server_manager-2025.5rc35.dist-info → karrio_server_manager-2025.5.1.dist-info}/METADATA +2 -2
- {karrio_server_manager-2025.5rc35.dist-info → karrio_server_manager-2025.5.1.dist-info}/RECORD +11 -10
- {karrio_server_manager-2025.5rc35.dist-info → karrio_server_manager-2025.5.1.dist-info}/WHEEL +0 -0
- {karrio_server_manager-2025.5rc35.dist-info → karrio_server_manager-2025.5.1.dist-info}/top_level.txt +0 -0
|
@@ -48,6 +48,7 @@ from karrio.server.manager.serializers.customs import CustomsSerializer
|
|
|
48
48
|
from karrio.server.manager.serializers.parcel import ParcelSerializer
|
|
49
49
|
from karrio.server.manager.serializers.rate import RateSerializer
|
|
50
50
|
import karrio.server.manager.models as models
|
|
51
|
+
|
|
51
52
|
DEFAULT_CARRIER_FILTER: typing.Any = dict(active=True, capability="shipping")
|
|
52
53
|
|
|
53
54
|
|
|
@@ -288,7 +289,16 @@ class ShipmentSerializer(ShipmentData):
|
|
|
288
289
|
|
|
289
290
|
|
|
290
291
|
class ShipmentPurchaseData(Serializer):
|
|
291
|
-
selected_rate_id = CharField(
|
|
292
|
+
selected_rate_id = CharField(
|
|
293
|
+
required=False,
|
|
294
|
+
allow_null=True,
|
|
295
|
+
help_text="The shipment selected rate.",
|
|
296
|
+
)
|
|
297
|
+
service = CharField(
|
|
298
|
+
required=False,
|
|
299
|
+
allow_null=True,
|
|
300
|
+
help_text="The carrier service to use for the shipment (alternative to selected_rate_id).",
|
|
301
|
+
)
|
|
292
302
|
label_type = ChoiceField(
|
|
293
303
|
required=False,
|
|
294
304
|
choices=LABEL_TYPES,
|
|
@@ -306,6 +316,15 @@ class ShipmentPurchaseData(Serializer):
|
|
|
306
316
|
required=False, help_text="User metadata for the shipment"
|
|
307
317
|
)
|
|
308
318
|
|
|
319
|
+
def validate(self, data):
|
|
320
|
+
if not data.get("selected_rate_id") and not data.get("service"):
|
|
321
|
+
raise exceptions.APIException(
|
|
322
|
+
"Either 'selected_rate_id' or 'service' must be provided.",
|
|
323
|
+
code="validation_error",
|
|
324
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
325
|
+
)
|
|
326
|
+
return data
|
|
327
|
+
|
|
309
328
|
|
|
310
329
|
class ShipmentUpdateData(validators.OptionDefaultSerializer):
|
|
311
330
|
label_type = ChoiceField(
|
|
@@ -792,9 +811,15 @@ def create_shipment_tracker(shipment: typing.Optional[models.Shipment], context)
|
|
|
792
811
|
)
|
|
793
812
|
tracker.save()
|
|
794
813
|
link_org(tracker, context)
|
|
795
|
-
logger.info(
|
|
814
|
+
logger.info(
|
|
815
|
+
"Successfully added a tracker to the shipment", shipment_id=shipment.id
|
|
816
|
+
)
|
|
796
817
|
except Exception as e:
|
|
797
|
-
logger.exception(
|
|
818
|
+
logger.exception(
|
|
819
|
+
"Failed to create new label tracker",
|
|
820
|
+
error=str(e),
|
|
821
|
+
shipment_id=shipment.id,
|
|
822
|
+
)
|
|
798
823
|
|
|
799
824
|
# Update shipment tracking url if different from the current one
|
|
800
825
|
try:
|
|
@@ -815,7 +840,12 @@ def create_shipment_tracker(shipment: typing.Optional[models.Shipment], context)
|
|
|
815
840
|
shipment.tracking_url = tracking_url
|
|
816
841
|
shipment.save(update_fields=["tracking_url"])
|
|
817
842
|
except Exception as e:
|
|
818
|
-
logger.exception(
|
|
843
|
+
logger.exception(
|
|
844
|
+
"Failed to update shipment tracking url",
|
|
845
|
+
error=str(e),
|
|
846
|
+
shipment_id=shipment.id,
|
|
847
|
+
tracking_number=shipment.tracking_number,
|
|
848
|
+
)
|
|
819
849
|
|
|
820
850
|
|
|
821
851
|
def generate_custom_invoice(template: str, shipment: models.Shipment, **kwargs):
|
|
@@ -843,7 +873,11 @@ def generate_custom_invoice(template: str, shipment: models.Shipment, **kwargs):
|
|
|
843
873
|
shipment.invoice = document["doc_file"]
|
|
844
874
|
shipment.save(update_fields=["invoice"])
|
|
845
875
|
|
|
846
|
-
logger.info(
|
|
876
|
+
logger.info(
|
|
877
|
+
"Custom document successfully generated",
|
|
878
|
+
shipment_id=shipment.id,
|
|
879
|
+
template=template,
|
|
880
|
+
)
|
|
847
881
|
|
|
848
882
|
return document
|
|
849
883
|
|
|
@@ -120,64 +120,26 @@ class TrackingSerializer(TrackingDetails):
|
|
|
120
120
|
).data,
|
|
121
121
|
carrier=carrier,
|
|
122
122
|
)
|
|
123
|
-
# update values only if changed; This is important for webhooks notification
|
|
124
|
-
changes = []
|
|
125
|
-
details = response.tracking
|
|
126
|
-
info = lib.to_dict(details.info or {})
|
|
127
|
-
events = utils.process_events(
|
|
128
|
-
response_events=details.events, current_events=instance.events
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
if events != instance.events:
|
|
132
|
-
instance.events = events
|
|
133
|
-
changes.append("events")
|
|
134
|
-
|
|
135
|
-
if response.messages != instance.messages:
|
|
136
|
-
instance.messages = lib.to_dict(response.messages)
|
|
137
|
-
changes.append("messages")
|
|
138
|
-
|
|
139
|
-
if details.delivered != instance.delivered:
|
|
140
|
-
instance.delivered = details.delivered
|
|
141
|
-
changes.append("delivered")
|
|
142
|
-
|
|
143
|
-
if details.status != instance.status:
|
|
144
|
-
instance.status = details.status
|
|
145
|
-
changes.append("status")
|
|
146
|
-
|
|
147
|
-
if details.estimated_delivery != instance.estimated_delivery:
|
|
148
|
-
instance.estimated_delivery = details.estimated_delivery
|
|
149
|
-
changes.append("estimated_delivery")
|
|
150
|
-
|
|
151
|
-
if details.options != instance.options:
|
|
152
|
-
instance.options = details.options
|
|
153
|
-
changes.append("options")
|
|
154
|
-
|
|
155
|
-
if any(info.keys()) and info != instance.info:
|
|
156
|
-
instance.info = serializers.process_dictionaries_mutations(
|
|
157
|
-
["info"], dict(info=info), instance
|
|
158
|
-
)["info"]
|
|
159
|
-
changes.append("info")
|
|
160
123
|
|
|
124
|
+
# Handle carrier change separately (not part of tracking_details)
|
|
161
125
|
if carrier.id != instance.tracking_carrier.id:
|
|
162
|
-
instance.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if details.images is not None and (
|
|
166
|
-
details.images.delivery_image != instance.delivery_image
|
|
167
|
-
or details.images.signature_image != instance.signature_image
|
|
168
|
-
):
|
|
169
|
-
changes.append("delivery_image")
|
|
170
|
-
changes.append("signature_image")
|
|
171
|
-
instance.delivery_image = (
|
|
172
|
-
details.images.delivery_image or instance.delivery_image
|
|
173
|
-
)
|
|
174
|
-
instance.signature_image = (
|
|
175
|
-
details.images.signature_image or instance.signature_image
|
|
176
|
-
)
|
|
126
|
+
instance.tracking_carrier = carrier
|
|
127
|
+
instance.save(update_fields=["tracking_carrier"])
|
|
177
128
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
129
|
+
# Use update_tracker for the rest of the tracking details
|
|
130
|
+
update_tracker(
|
|
131
|
+
instance,
|
|
132
|
+
dict(
|
|
133
|
+
events=response.tracking.events,
|
|
134
|
+
messages=response.messages,
|
|
135
|
+
delivered=response.tracking.delivered,
|
|
136
|
+
status=response.tracking.status,
|
|
137
|
+
estimated_delivery=response.tracking.estimated_delivery,
|
|
138
|
+
options=response.tracking.options,
|
|
139
|
+
info=lib.to_dict(response.tracking.info or {}),
|
|
140
|
+
images=response.tracking.images,
|
|
141
|
+
),
|
|
142
|
+
)
|
|
181
143
|
|
|
182
144
|
return instance
|
|
183
145
|
|
|
@@ -246,3 +208,120 @@ def update_shipment_tracker(tracker: models.Tracking):
|
|
|
246
208
|
tracker.shipment.save(update_fields=["status"])
|
|
247
209
|
except Exception as e:
|
|
248
210
|
logger.exception("Failed to update the tracked shipment", error=str(e), tracker_id=tracker.id, tracking_number=tracker.tracking_number)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def update_tracker(tracker: models.Tracking, tracking_details: dict) -> models.Tracking:
|
|
214
|
+
"""Update tracker with new tracking details from webhook or external source.
|
|
215
|
+
|
|
216
|
+
This utility function consolidates the change detection logic for updating
|
|
217
|
+
a tracker instance. It only saves fields that have changed and triggers
|
|
218
|
+
the shipment status update via update_shipment_tracker.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
tracker: The Tracking model instance to update
|
|
222
|
+
tracking_details: Dictionary containing tracking details with keys like:
|
|
223
|
+
- events: List of tracking event dictionaries
|
|
224
|
+
- messages: List of message dictionaries
|
|
225
|
+
- delivered: Boolean delivery status
|
|
226
|
+
- status: Tracker status string
|
|
227
|
+
- estimated_delivery: Estimated delivery date string
|
|
228
|
+
- options: Dictionary of tracking options
|
|
229
|
+
- meta: Dictionary of metadata
|
|
230
|
+
- info: Dictionary of tracking info
|
|
231
|
+
- images: Dictionary with delivery_image and signature_image
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
The updated Tracking model instance
|
|
235
|
+
"""
|
|
236
|
+
try:
|
|
237
|
+
changes = []
|
|
238
|
+
|
|
239
|
+
# Process events - merge with existing events
|
|
240
|
+
new_events = tracking_details.get("events") or []
|
|
241
|
+
if new_events:
|
|
242
|
+
events = utils.process_events(
|
|
243
|
+
response_events=new_events, current_events=tracker.events
|
|
244
|
+
)
|
|
245
|
+
if events != tracker.events:
|
|
246
|
+
tracker.events = events
|
|
247
|
+
changes.append("events")
|
|
248
|
+
|
|
249
|
+
# Update messages
|
|
250
|
+
messages = tracking_details.get("messages")
|
|
251
|
+
if messages is not None and messages != tracker.messages:
|
|
252
|
+
tracker.messages = lib.to_dict(messages)
|
|
253
|
+
changes.append("messages")
|
|
254
|
+
|
|
255
|
+
# Update delivered status
|
|
256
|
+
delivered = tracking_details.get("delivered")
|
|
257
|
+
if delivered is not None and delivered != tracker.delivered:
|
|
258
|
+
tracker.delivered = delivered
|
|
259
|
+
changes.append("delivered")
|
|
260
|
+
|
|
261
|
+
# Update status
|
|
262
|
+
status = tracking_details.get("status")
|
|
263
|
+
if status is not None and status != tracker.status:
|
|
264
|
+
tracker.status = status
|
|
265
|
+
changes.append("status")
|
|
266
|
+
|
|
267
|
+
# Update estimated delivery
|
|
268
|
+
estimated_delivery = tracking_details.get("estimated_delivery")
|
|
269
|
+
if estimated_delivery is not None and estimated_delivery != tracker.estimated_delivery:
|
|
270
|
+
tracker.estimated_delivery = estimated_delivery
|
|
271
|
+
changes.append("estimated_delivery")
|
|
272
|
+
|
|
273
|
+
# Update options
|
|
274
|
+
options = tracking_details.get("options")
|
|
275
|
+
if options is not None and options != tracker.options:
|
|
276
|
+
tracker.options = options
|
|
277
|
+
changes.append("options")
|
|
278
|
+
|
|
279
|
+
# Update meta
|
|
280
|
+
meta = tracking_details.get("meta")
|
|
281
|
+
if meta is not None and meta != tracker.meta:
|
|
282
|
+
tracker.meta = {**(tracker.meta or {}), **meta}
|
|
283
|
+
changes.append("meta")
|
|
284
|
+
|
|
285
|
+
# Update info - merge with existing info
|
|
286
|
+
info = tracking_details.get("info") or {}
|
|
287
|
+
if any(info.keys()) and info != tracker.info:
|
|
288
|
+
tracker.info = serializers.process_dictionaries_mutations(
|
|
289
|
+
["info"], dict(info=info), tracker
|
|
290
|
+
)["info"]
|
|
291
|
+
changes.append("info")
|
|
292
|
+
|
|
293
|
+
# Update images
|
|
294
|
+
images = tracking_details.get("images") or {}
|
|
295
|
+
delivery_image = images.get("delivery_image") if isinstance(images, dict) else getattr(images, "delivery_image", None)
|
|
296
|
+
signature_image = images.get("signature_image") if isinstance(images, dict) else getattr(images, "signature_image", None)
|
|
297
|
+
|
|
298
|
+
if delivery_image is not None or signature_image is not None:
|
|
299
|
+
if delivery_image != tracker.delivery_image or signature_image != tracker.signature_image:
|
|
300
|
+
if delivery_image is not None:
|
|
301
|
+
tracker.delivery_image = delivery_image
|
|
302
|
+
changes.append("delivery_image")
|
|
303
|
+
if signature_image is not None:
|
|
304
|
+
tracker.signature_image = signature_image
|
|
305
|
+
changes.append("signature_image")
|
|
306
|
+
|
|
307
|
+
# Save changes and update associated shipment
|
|
308
|
+
if any(changes):
|
|
309
|
+
tracker.save(update_fields=changes)
|
|
310
|
+
update_shipment_tracker(tracker)
|
|
311
|
+
logger.info(
|
|
312
|
+
"Tracker updated via webhook",
|
|
313
|
+
tracker_id=tracker.id,
|
|
314
|
+
tracking_number=tracker.tracking_number,
|
|
315
|
+
changes=changes,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return tracker
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
logger.exception(
|
|
322
|
+
"Failed to update tracker",
|
|
323
|
+
error=str(e),
|
|
324
|
+
tracker_id=tracker.id,
|
|
325
|
+
tracking_number=tracker.tracking_number,
|
|
326
|
+
)
|
|
327
|
+
return tracker
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from django.urls import reverse
|
|
3
|
+
from rest_framework import status
|
|
4
|
+
from karrio.server.core.tests import APITestCase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestNotFoundErrors(APITestCase):
|
|
8
|
+
def test_address_not_found_returns_resource_name(self):
|
|
9
|
+
url = reverse(
|
|
10
|
+
"karrio.server.manager:address-details",
|
|
11
|
+
kwargs=dict(pk="nonexistent_id"),
|
|
12
|
+
)
|
|
13
|
+
response = self.client.get(url)
|
|
14
|
+
response_data = json.loads(response.content)
|
|
15
|
+
|
|
16
|
+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
17
|
+
self.assertDictEqual(
|
|
18
|
+
response_data,
|
|
19
|
+
{"errors": [{"code": "not_found", "message": "Address not found"}]},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def test_parcel_not_found_returns_resource_name(self):
|
|
23
|
+
url = reverse(
|
|
24
|
+
"karrio.server.manager:parcel-details",
|
|
25
|
+
kwargs=dict(pk="nonexistent_id"),
|
|
26
|
+
)
|
|
27
|
+
response = self.client.get(url)
|
|
28
|
+
response_data = json.loads(response.content)
|
|
29
|
+
|
|
30
|
+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
31
|
+
self.assertDictEqual(
|
|
32
|
+
response_data,
|
|
33
|
+
{"errors": [{"code": "not_found", "message": "Parcel not found"}]},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def test_shipment_not_found_returns_resource_name(self):
|
|
37
|
+
url = reverse(
|
|
38
|
+
"karrio.server.manager:shipment-details",
|
|
39
|
+
kwargs=dict(pk="nonexistent_id"),
|
|
40
|
+
)
|
|
41
|
+
response = self.client.get(url)
|
|
42
|
+
response_data = json.loads(response.content)
|
|
43
|
+
|
|
44
|
+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
45
|
+
self.assertDictEqual(
|
|
46
|
+
response_data,
|
|
47
|
+
{"errors": [{"code": "not_found", "message": "Shipment not found"}]},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def test_customs_not_found_returns_resource_name(self):
|
|
51
|
+
url = reverse(
|
|
52
|
+
"karrio.server.manager:customs-details",
|
|
53
|
+
kwargs=dict(pk="nonexistent_id"),
|
|
54
|
+
)
|
|
55
|
+
response = self.client.get(url)
|
|
56
|
+
response_data = json.loads(response.content)
|
|
57
|
+
|
|
58
|
+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
59
|
+
self.assertDictEqual(
|
|
60
|
+
response_data,
|
|
61
|
+
{"errors": [{"code": "not_found", "message": "Customs not found"}]},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestValidationErrors(APITestCase):
|
|
66
|
+
def test_shipment_validation_error_format(self):
|
|
67
|
+
url = reverse("karrio.server.manager:shipment-list")
|
|
68
|
+
data = {
|
|
69
|
+
"shipper": {},
|
|
70
|
+
"recipient": {},
|
|
71
|
+
"parcels": [],
|
|
72
|
+
}
|
|
73
|
+
response = self.client.post(url, data, format="json")
|
|
74
|
+
response_data = json.loads(response.content)
|
|
75
|
+
|
|
76
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
77
|
+
self.assertIn("errors", response_data)
|
|
78
|
+
self.assertTrue(len(response_data["errors"]) > 0)
|
|
79
|
+
|
|
80
|
+
def test_address_validation_error_format(self):
|
|
81
|
+
url = reverse("karrio.server.manager:address-list")
|
|
82
|
+
data = {"country_code": "INVALID"}
|
|
83
|
+
response = self.client.post(url, data, format="json")
|
|
84
|
+
response_data = json.loads(response.content)
|
|
85
|
+
|
|
86
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
87
|
+
self.assertIn("errors", response_data)
|
|
88
|
+
self.assertTrue(len(response_data["errors"]) > 0)
|
|
@@ -831,3 +831,195 @@ CANCEL_PURCHASED_RESPONSE = {
|
|
|
831
831
|
"label_url": None,
|
|
832
832
|
"invoice_url": None,
|
|
833
833
|
}
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
class TestShipmentPurchaseWithAlternativeServices(TestShipmentFixture):
|
|
837
|
+
def setUp(self) -> None:
|
|
838
|
+
super().setUp()
|
|
839
|
+
carrier = providers.Carrier.objects.get(carrier_id="canadapost")
|
|
840
|
+
# Rates have "canadapost_regular_parcel" but we'll request "canadapost_priority"
|
|
841
|
+
self.shipment.rates = [
|
|
842
|
+
{
|
|
843
|
+
"id": "rat_alt_service_test",
|
|
844
|
+
"carrier_id": "canadapost",
|
|
845
|
+
"carrier_name": "canadapost",
|
|
846
|
+
"currency": "CAD",
|
|
847
|
+
"estimated_delivery": None,
|
|
848
|
+
"extra_charges": [
|
|
849
|
+
{"amount": 50.00, "currency": "CAD", "name": "Base charge"},
|
|
850
|
+
],
|
|
851
|
+
"service": "canadapost_regular_parcel",
|
|
852
|
+
"total_charge": 50.00,
|
|
853
|
+
"transit_days": 5,
|
|
854
|
+
"test_mode": True,
|
|
855
|
+
"meta": {
|
|
856
|
+
"rate_provider": "canadapost",
|
|
857
|
+
"service_name": "CANADAPOST REGULAR PARCEL",
|
|
858
|
+
"carrier_connection_id": carrier.pk,
|
|
859
|
+
},
|
|
860
|
+
}
|
|
861
|
+
]
|
|
862
|
+
self.shipment.options = {"has_alternative_services": True}
|
|
863
|
+
self.shipment.save()
|
|
864
|
+
|
|
865
|
+
def test_purchase_with_alternative_service(self):
|
|
866
|
+
"""
|
|
867
|
+
Test that when canadapost_priority is requested but only canadapost_regular_parcel
|
|
868
|
+
is in rates, the purchase proceeds with has_alternative_services=True,
|
|
869
|
+
delegating service resolution to the carrier.
|
|
870
|
+
"""
|
|
871
|
+
url = reverse(
|
|
872
|
+
"karrio.server.manager:shipment-purchase",
|
|
873
|
+
kwargs=dict(pk=self.shipment.pk),
|
|
874
|
+
)
|
|
875
|
+
data = {"service": "canadapost_priority"}
|
|
876
|
+
|
|
877
|
+
with patch("karrio.server.core.gateway.utils.identity") as mock:
|
|
878
|
+
mock.return_value = CREATED_SHIPMENT_RESPONSE
|
|
879
|
+
response = self.client.post(url, data)
|
|
880
|
+
response_data = json.loads(response.content)
|
|
881
|
+
|
|
882
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
883
|
+
self.assertDictEqual(response_data, ALTERNATIVE_SERVICE_PURCHASED_SHIPMENT)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
ALTERNATIVE_SERVICE_PURCHASED_SHIPMENT = {
|
|
887
|
+
"id": ANY,
|
|
888
|
+
"object_type": "shipment",
|
|
889
|
+
"tracking_url": "/v1/trackers/canadapost/123456789012",
|
|
890
|
+
"shipper": {
|
|
891
|
+
"id": ANY,
|
|
892
|
+
"postal_code": "E1C4Z8",
|
|
893
|
+
"city": "Moncton",
|
|
894
|
+
"federal_tax_id": None,
|
|
895
|
+
"state_tax_id": None,
|
|
896
|
+
"person_name": "John Poop",
|
|
897
|
+
"company_name": "A corp.",
|
|
898
|
+
"country_code": "CA",
|
|
899
|
+
"email": None,
|
|
900
|
+
"phone_number": "514 000 0000",
|
|
901
|
+
"state_code": "NB",
|
|
902
|
+
"street_number": None,
|
|
903
|
+
"residential": False,
|
|
904
|
+
"address_line1": "125 Church St",
|
|
905
|
+
"address_line2": None,
|
|
906
|
+
"validate_location": False,
|
|
907
|
+
"object_type": "address",
|
|
908
|
+
"validation": None,
|
|
909
|
+
},
|
|
910
|
+
"recipient": {
|
|
911
|
+
"id": ANY,
|
|
912
|
+
"postal_code": "V6M2V9",
|
|
913
|
+
"city": "Vancouver",
|
|
914
|
+
"federal_tax_id": None,
|
|
915
|
+
"state_tax_id": None,
|
|
916
|
+
"person_name": "Jane Doe",
|
|
917
|
+
"company_name": "B corp.",
|
|
918
|
+
"country_code": "CA",
|
|
919
|
+
"email": None,
|
|
920
|
+
"phone_number": "514 000 9999",
|
|
921
|
+
"state_code": "BC",
|
|
922
|
+
"street_number": None,
|
|
923
|
+
"residential": False,
|
|
924
|
+
"address_line1": "5840 Oak St",
|
|
925
|
+
"address_line2": None,
|
|
926
|
+
"validate_location": False,
|
|
927
|
+
"object_type": "address",
|
|
928
|
+
"validation": None,
|
|
929
|
+
},
|
|
930
|
+
"parcels": [
|
|
931
|
+
{
|
|
932
|
+
"id": ANY,
|
|
933
|
+
"weight": 1.0,
|
|
934
|
+
"width": None,
|
|
935
|
+
"height": None,
|
|
936
|
+
"length": None,
|
|
937
|
+
"packaging_type": None,
|
|
938
|
+
"package_preset": "canadapost_corrugated_small_box",
|
|
939
|
+
"description": None,
|
|
940
|
+
"content": None,
|
|
941
|
+
"is_document": False,
|
|
942
|
+
"weight_unit": "KG",
|
|
943
|
+
"dimension_unit": None,
|
|
944
|
+
"items": [],
|
|
945
|
+
"freight_class": None,
|
|
946
|
+
"reference_number": ANY,
|
|
947
|
+
"object_type": "parcel",
|
|
948
|
+
"options": {},
|
|
949
|
+
}
|
|
950
|
+
],
|
|
951
|
+
"services": [],
|
|
952
|
+
"options": {"has_alternative_services": True, "shipping_date": ANY, "shipment_date": ANY},
|
|
953
|
+
"payment": {"paid_by": "sender", "currency": "CAD", "account_number": None},
|
|
954
|
+
"return_address": None,
|
|
955
|
+
"billing_address": None,
|
|
956
|
+
"customs": None,
|
|
957
|
+
"rates": [
|
|
958
|
+
{
|
|
959
|
+
"id": ANY,
|
|
960
|
+
"object_type": "rate",
|
|
961
|
+
"carrier_name": "canadapost",
|
|
962
|
+
"carrier_id": "canadapost",
|
|
963
|
+
"currency": "CAD",
|
|
964
|
+
"estimated_delivery": ANY,
|
|
965
|
+
"service": "canadapost_regular_parcel",
|
|
966
|
+
"total_charge": 50.00,
|
|
967
|
+
"transit_days": 5,
|
|
968
|
+
"extra_charges": [
|
|
969
|
+
{"name": "Base charge", "amount": 50.00, "currency": "CAD", "id": None},
|
|
970
|
+
],
|
|
971
|
+
"meta": {
|
|
972
|
+
"service_name": "CANADAPOST REGULAR PARCEL",
|
|
973
|
+
"rate_provider": "canadapost",
|
|
974
|
+
"carrier_connection_id": ANY,
|
|
975
|
+
},
|
|
976
|
+
"test_mode": True,
|
|
977
|
+
}
|
|
978
|
+
],
|
|
979
|
+
"reference": None,
|
|
980
|
+
"label_type": "PDF",
|
|
981
|
+
"carrier_ids": [],
|
|
982
|
+
"tracker_id": ANY,
|
|
983
|
+
"created_at": ANY,
|
|
984
|
+
"metadata": {},
|
|
985
|
+
"messages": [],
|
|
986
|
+
"status": "purchased",
|
|
987
|
+
"carrier_name": "canadapost",
|
|
988
|
+
"carrier_id": "canadapost",
|
|
989
|
+
"tracking_number": "123456789012",
|
|
990
|
+
"shipment_identifier": "123456789012",
|
|
991
|
+
"selected_rate": {
|
|
992
|
+
"id": ANY,
|
|
993
|
+
"object_type": "rate",
|
|
994
|
+
"carrier_name": "canadapost",
|
|
995
|
+
"carrier_id": "canadapost",
|
|
996
|
+
"currency": "CAD",
|
|
997
|
+
"estimated_delivery": ANY,
|
|
998
|
+
"service": "canadapost_priority",
|
|
999
|
+
"total_charge": 50.00,
|
|
1000
|
+
"transit_days": 5,
|
|
1001
|
+
"extra_charges": [
|
|
1002
|
+
{"name": "Base charge", "amount": 50.00, "currency": "CAD", "id": None},
|
|
1003
|
+
],
|
|
1004
|
+
"meta": {
|
|
1005
|
+
"ext": "canadapost",
|
|
1006
|
+
"carrier": "canadapost",
|
|
1007
|
+
"service_name": "CANADAPOST REGULAR PARCEL",
|
|
1008
|
+
"rate_provider": "canadapost",
|
|
1009
|
+
"carrier_connection_id": ANY,
|
|
1010
|
+
"has_alternative_services": True,
|
|
1011
|
+
},
|
|
1012
|
+
"test_mode": True,
|
|
1013
|
+
},
|
|
1014
|
+
"meta": {
|
|
1015
|
+
"ext": "canadapost",
|
|
1016
|
+
"carrier": "canadapost",
|
|
1017
|
+
"rate_provider": "canadapost",
|
|
1018
|
+
"service_name": "CANADAPOST PRIORITY",
|
|
1019
|
+
},
|
|
1020
|
+
"service": "canadapost_priority",
|
|
1021
|
+
"selected_rate_id": ANY,
|
|
1022
|
+
"test_mode": True,
|
|
1023
|
+
"label_url": ANY,
|
|
1024
|
+
"invoice_url": None,
|
|
1025
|
+
}
|
|
@@ -10,7 +10,7 @@ import rest_framework.pagination as pagination
|
|
|
10
10
|
import rest_framework.throttling as throttling
|
|
11
11
|
import django_filters.rest_framework as django_filters
|
|
12
12
|
|
|
13
|
-
from karrio.server.core.
|
|
13
|
+
from karrio.server.core.utils import validate_resource_token
|
|
14
14
|
import karrio.server.openapi as openapi
|
|
15
15
|
import karrio.server.core.views.api as api
|
|
16
16
|
import karrio.server.core.filters as filters
|
|
@@ -107,26 +107,24 @@ class ManifestDetails(api.APIView):
|
|
|
107
107
|
|
|
108
108
|
class ManifestDoc(django_downloadview.VirtualDownloadView):
|
|
109
109
|
@openapi.extend_schema(exclude=True)
|
|
110
|
-
def get(
|
|
111
|
-
self,
|
|
112
|
-
request: request.Request,
|
|
113
|
-
pk: str,
|
|
114
|
-
doc: str = "manifest",
|
|
115
|
-
format: str = "pdf",
|
|
116
|
-
**kwargs,
|
|
117
|
-
):
|
|
110
|
+
def get(self, req: request.Request, pk: str, doc: str = "manifest", format: str = "pdf", **kwargs):
|
|
118
111
|
"""Retrieve a manifest file."""
|
|
112
|
+
error = validate_resource_token(req, "manifest", [pk], "manifest")
|
|
113
|
+
if error:
|
|
114
|
+
return error
|
|
115
|
+
|
|
116
|
+
query_params = req.GET.dict()
|
|
117
|
+
|
|
119
118
|
self.manifest = models.Manifest.objects.get(pk=pk, manifest__isnull=False)
|
|
120
119
|
self.document = getattr(self.manifest, doc, None)
|
|
121
120
|
self.name = f"{doc}_{self.manifest.id}.{format}"
|
|
122
121
|
|
|
123
|
-
query_params = request.GET.dict()
|
|
124
122
|
self.preview = "preview" in query_params
|
|
125
123
|
self.attachment = "download" in query_params
|
|
126
124
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return
|
|
125
|
+
resp = super(ManifestDoc, self).get(req, pk, doc, format, **kwargs)
|
|
126
|
+
resp["X-Frame-Options"] = "ALLOWALL"
|
|
127
|
+
return resp
|
|
130
128
|
|
|
131
129
|
def get_file(self):
|
|
132
130
|
content = base64.b64decode(self.document or "")
|
|
@@ -14,7 +14,6 @@ import karrio.lib as lib
|
|
|
14
14
|
import karrio.server.openapi as openapi
|
|
15
15
|
import karrio.server.core.filters as filters
|
|
16
16
|
import karrio.server.manager.models as models
|
|
17
|
-
from karrio.server.core.logging import logger
|
|
18
17
|
from karrio.server.core.views.api import GenericAPIView, APIView
|
|
19
18
|
from karrio.server.core.filters import ShipmentFilters
|
|
20
19
|
from karrio.server.manager.router import router
|
|
@@ -267,12 +266,19 @@ class ShipmentDocs(VirtualDownloadView):
|
|
|
267
266
|
format: str = "pdf",
|
|
268
267
|
**kwargs,
|
|
269
268
|
):
|
|
270
|
-
"""Retrieve a shipment label."""
|
|
269
|
+
"""Retrieve a shipment label or invoice."""
|
|
270
|
+
from karrio.server.core.utils import validate_resource_token
|
|
271
|
+
|
|
272
|
+
error = validate_resource_token(request, "shipment", [pk], doc)
|
|
273
|
+
if error:
|
|
274
|
+
return error
|
|
275
|
+
|
|
276
|
+
query_params = request.GET.dict()
|
|
277
|
+
|
|
271
278
|
self.shipment = models.Shipment.objects.get(pk=pk, label__isnull=False)
|
|
272
279
|
self.document = getattr(self.shipment, doc, None)
|
|
273
280
|
self.name = f"{doc}_{self.shipment.tracking_number}.{format}"
|
|
274
281
|
|
|
275
|
-
query_params = request.GET.dict()
|
|
276
282
|
self.preview = "preview" in query_params
|
|
277
283
|
self.attachment = "download" in query_params
|
|
278
284
|
|
{karrio_server_manager-2025.5rc35.dist-info → karrio_server_manager-2025.5.1.dist-info}/METADATA
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: karrio_server_manager
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.5.1
|
|
4
4
|
Summary: Multi-carrier shipping API Shipments manager module
|
|
5
5
|
Author-email: karrio <hello@karrio.io>
|
|
6
|
-
License-Expression:
|
|
6
|
+
License-Expression: LGPL-3.0
|
|
7
7
|
Project-URL: Homepage, https://github.com/karrioapi/karrio
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Requires-Python: >=3.11
|
{karrio_server_manager-2025.5rc35.dist-info → karrio_server_manager-2025.5.1.dist-info}/RECORD
RENAMED
|
@@ -73,7 +73,7 @@ karrio/server/manager/migrations/0064_shipment_shipment_created_at_idx_and_more.
|
|
|
73
73
|
karrio/server/manager/migrations/0065_alter_address_city_alter_address_company_name_and_more.py,sha256=m0KQSz8cnJzaXUlaAKdqU0duZ8wSQeTYQCIeam3Wivw,6279
|
|
74
74
|
karrio/server/manager/migrations/0066_commodity_image_url_commodity_product_id_and_more.py,sha256=Jw7bEBmqzDSp6zw0xcAiKc9aDCsa3Wrw-oTM8r9Pqfc,997
|
|
75
75
|
karrio/server/manager/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
76
|
-
karrio/server/manager/serializers/__init__.py,sha256=
|
|
76
|
+
karrio/server/manager/serializers/__init__.py,sha256=YVwGfjpb-_PezzGc8N8oLTkORan0hZ2muGj2PYG3n0g,1431
|
|
77
77
|
karrio/server/manager/serializers/address.py,sha256=yGKvZiNukeI15LEDdbo1ycqqK8QW77ak_vyLMIKyglI,2779
|
|
78
78
|
karrio/server/manager/serializers/commodity.py,sha256=PKrW-xAYkiNByk5RF8up_Bt1Z8mJV_BGW0mPWT9w4ME,1658
|
|
79
79
|
karrio/server/manager/serializers/customs.py,sha256=gMGC4TJMykjgELBLFHL6v7kPQ5YQKe7cQcMnOGBLL84,2872
|
|
@@ -82,25 +82,26 @@ karrio/server/manager/serializers/manifest.py,sha256=mSneCk_7HMXpi64_7hggWvkR7Ma
|
|
|
82
82
|
karrio/server/manager/serializers/parcel.py,sha256=733Bg26lVbEkoWtAVM5Qt2IRBS2QDuVxhG40Hiqh3bw,2621
|
|
83
83
|
karrio/server/manager/serializers/pickup.py,sha256=sX0VmcQxGkXn3IEosMuFwdXh4HhdkPcuBOp79O8PoDQ,9233
|
|
84
84
|
karrio/server/manager/serializers/rate.py,sha256=7vYK_v8iWEDnswqYHG2Lir16_UhHTOxW5rdC6lw3lzA,652
|
|
85
|
-
karrio/server/manager/serializers/shipment.py,sha256=
|
|
86
|
-
karrio/server/manager/serializers/tracking.py,sha256=
|
|
85
|
+
karrio/server/manager/serializers/shipment.py,sha256=gkDFTDaKXA8Iz3yjS6fxRi46AzZPw40RhJEOlnBZy6o,31143
|
|
86
|
+
karrio/server/manager/serializers/tracking.py,sha256=ixrAjIiZQsvSt4y0qtisGkt6TFOJ3ORNkJAQVt6YQrA,12483
|
|
87
87
|
karrio/server/manager/tests/__init__.py,sha256=Y1UNteEE60vWdUAkjbldu_r_-h4u0He8-UoiBgTjKcU,391
|
|
88
88
|
karrio/server/manager/tests/test_addresses.py,sha256=pNkZC_yJyb29ZlEOtOAs4blcEYiOarw0zhZIZC5uj1w,3111
|
|
89
89
|
karrio/server/manager/tests/test_custom_infos.py,sha256=iv2cLdZVoVWFZK_mDUEnrZssncAnQcn87Rn2sAk8UQI,2731
|
|
90
|
+
karrio/server/manager/tests/test_errors.py,sha256=QYsGLUtwMvrHeX1XSCpdteTKbug7-y1-Xgvbl96aN9g,3220
|
|
90
91
|
karrio/server/manager/tests/test_parcels.py,sha256=lVLBOsHzXgXQvYjHIUy5oiPvrMfxYpueVvvhtuhstWk,2559
|
|
91
92
|
karrio/server/manager/tests/test_pickups.py,sha256=8jxddwTnBvBM9FOyWxW9TtZ-GOVYUje7HQ2EZjsbtD8,10681
|
|
92
|
-
karrio/server/manager/tests/test_shipments.py,sha256=
|
|
93
|
+
karrio/server/manager/tests/test_shipments.py,sha256=N1eVH-n0BTANmzehKL0cp917goskU5X8l_ftXzcVSmw,33349
|
|
93
94
|
karrio/server/manager/tests/test_trackers.py,sha256=KvmWkplokNDZ0dzB16mFl0WcMJ0OYp_ErZeWJPGW_NA,7151
|
|
94
95
|
karrio/server/manager/views/__init__.py,sha256=kDFUaORRQ3Xh0ZPm-Jk88Ss8dgGYM57iUFXb9TPMzh0,401
|
|
95
96
|
karrio/server/manager/views/addresses.py,sha256=7YCAs2ZYgd1icYwMcGGWfX7A7vZEL4BEAbU4eIxhiMY,4620
|
|
96
97
|
karrio/server/manager/views/customs.py,sha256=-ZreiKyJ1xeLeNVG53nMfRQFeURduWr1QkDItdLPnE8,4875
|
|
97
98
|
karrio/server/manager/views/documents.py,sha256=znW54qJ_k7WInIut5FBZFDT93CioozXTOYFKRSUTBhA,4005
|
|
98
|
-
karrio/server/manager/views/manifests.py,sha256=
|
|
99
|
+
karrio/server/manager/views/manifests.py,sha256=_Dd83YxVJOgWhAhD745Kr4tcLWnKaU1dxnT5xB8opvk,5227
|
|
99
100
|
karrio/server/manager/views/parcels.py,sha256=hZY45rg6SrTWfQqyJ38MGKSor1yqgPUEVHtu16aG37g,4594
|
|
100
101
|
karrio/server/manager/views/pickups.py,sha256=gmpxz9ot1OR-BP1qh-0MXU3kUJi1ht_74hfaLJzJ42w,5503
|
|
101
|
-
karrio/server/manager/views/shipments.py,sha256=
|
|
102
|
+
karrio/server/manager/views/shipments.py,sha256=TqLpBH5Jf-rI3enJwvNptRwGzfo7co9R1VSP_oqhB3o,10419
|
|
102
103
|
karrio/server/manager/views/trackers.py,sha256=3oGn2qDpHgk8GZvuz-Cb93Fc0j_h_HbXQR692Zhfiok,12363
|
|
103
|
-
karrio_server_manager-2025.
|
|
104
|
-
karrio_server_manager-2025.
|
|
105
|
-
karrio_server_manager-2025.
|
|
106
|
-
karrio_server_manager-2025.
|
|
104
|
+
karrio_server_manager-2025.5.1.dist-info/METADATA,sha256=7G3zIzLe_6ux6VkjCj1ub1xQc4b9Cz-uiyzwvBpSDDs,730
|
|
105
|
+
karrio_server_manager-2025.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
106
|
+
karrio_server_manager-2025.5.1.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
|
|
107
|
+
karrio_server_manager-2025.5.1.dist-info/RECORD,,
|
{karrio_server_manager-2025.5rc35.dist-info → karrio_server_manager-2025.5.1.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|