karrio-server-manager 2026.1.1__py3-none-any.whl → 2026.1.4__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.
Files changed (44) hide show
  1. karrio/server/manager/migrations/0070_add_meta_and_product_fields.py +98 -0
  2. karrio/server/manager/migrations/0071_product_proxy.py +25 -0
  3. karrio/server/manager/migrations/0072_populate_json_fields.py +267 -0
  4. karrio/server/manager/migrations/0073_make_shipment_fk_nullable.py +36 -0
  5. karrio/server/manager/migrations/0074_clean_model_refactoring.py +207 -0
  6. karrio/server/manager/migrations/0075_populate_template_meta.py +69 -0
  7. karrio/server/manager/migrations/0076_remove_customs_model.py +66 -0
  8. karrio/server/manager/migrations/0077_add_carrier_snapshot_fields.py +83 -0
  9. karrio/server/manager/migrations/0078_populate_carrier_snapshots.py +112 -0
  10. karrio/server/manager/migrations/0079_remove_carrier_fk_fields.py +56 -0
  11. karrio/server/manager/migrations/0080_add_carrier_json_indexes.py +137 -0
  12. karrio/server/manager/migrations/0081_cleanup.py +62 -0
  13. karrio/server/manager/migrations/0082_shipment_fees.py +26 -0
  14. karrio/server/manager/models.py +421 -321
  15. karrio/server/manager/serializers/__init__.py +5 -4
  16. karrio/server/manager/serializers/address.py +8 -2
  17. karrio/server/manager/serializers/commodity.py +11 -4
  18. karrio/server/manager/serializers/document.py +29 -15
  19. karrio/server/manager/serializers/manifest.py +6 -3
  20. karrio/server/manager/serializers/parcel.py +5 -2
  21. karrio/server/manager/serializers/pickup.py +194 -67
  22. karrio/server/manager/serializers/shipment.py +226 -171
  23. karrio/server/manager/serializers/tracking.py +45 -12
  24. karrio/server/manager/tests/__init__.py +0 -1
  25. karrio/server/manager/tests/test_addresses.py +53 -0
  26. karrio/server/manager/tests/test_parcels.py +50 -0
  27. karrio/server/manager/tests/test_pickups.py +286 -50
  28. karrio/server/manager/tests/test_products.py +597 -0
  29. karrio/server/manager/tests/test_shipments.py +237 -92
  30. karrio/server/manager/tests/test_trackers.py +4 -3
  31. karrio/server/manager/views/__init__.py +1 -1
  32. karrio/server/manager/views/addresses.py +38 -2
  33. karrio/server/manager/views/documents.py +1 -1
  34. karrio/server/manager/views/parcels.py +25 -2
  35. karrio/server/manager/views/pickups.py +6 -6
  36. karrio/server/manager/views/products.py +239 -0
  37. karrio/server/manager/views/trackers.py +69 -1
  38. {karrio_server_manager-2026.1.1.dist-info → karrio_server_manager-2026.1.4.dist-info}/METADATA +1 -1
  39. {karrio_server_manager-2026.1.1.dist-info → karrio_server_manager-2026.1.4.dist-info}/RECORD +41 -29
  40. {karrio_server_manager-2026.1.1.dist-info → karrio_server_manager-2026.1.4.dist-info}/WHEEL +1 -1
  41. karrio/server/manager/serializers/customs.py +0 -84
  42. karrio/server/manager/tests/test_custom_infos.py +0 -101
  43. karrio/server/manager/views/customs.py +0 -159
  44. {karrio_server_manager-2026.1.1.dist-info → karrio_server_manager-2026.1.4.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,7 @@ from django.urls import reverse
5
5
  from rest_framework import status
6
6
  from karrio.core.models import PickupDetails, ConfirmationDetails, ChargeDetails
7
7
  from karrio.server.manager.tests.test_shipments import TestShipmentFixture
8
+ from karrio.server.core.utils import create_carrier_snapshot
8
9
  import karrio.server.manager.models as models
9
10
 
10
11
 
@@ -12,29 +13,34 @@ class TestFixture(TestShipmentFixture):
12
13
  def setUp(self) -> None:
13
14
  super().setUp()
14
15
 
15
- self.address: models.Address = models.Address.objects.create(
16
- **{
17
- "postal_code": "E1C4Z8",
18
- "city": "Moncton",
19
- "federal_tax_id": None,
20
- "state_tax_id": None,
21
- "person_name": "John Poop",
22
- "company_name": "A corp.",
23
- "country_code": "CA",
24
- "email": "john@a.com",
25
- "phone_number": "514 000 0000",
26
- "state_code": "NB",
27
- "street_number": None,
28
- "residential": False,
29
- "address_line1": "125 Church St",
30
- "address_line2": None,
31
- "validate_location": False,
32
- "validation": None,
33
- "created_by": self.user,
34
- }
35
- )
16
+ # Address as dict data for JSON field (use proper JSON-generated ID format)
17
+ self.address_data = {
18
+ "id": "adr_001122334455",
19
+ "postal_code": "E1C4Z8",
20
+ "city": "Moncton",
21
+ "federal_tax_id": None,
22
+ "state_tax_id": None,
23
+ "person_name": "John Poop",
24
+ "company_name": "A corp.",
25
+ "country_code": "CA",
26
+ "email": "john@a.com",
27
+ "phone_number": "514 000 0000",
28
+ "state_code": "NB",
29
+ "street_number": None,
30
+ "residential": False,
31
+ "address_line1": "125 Church St",
32
+ "address_line2": None,
33
+ "validate_location": False,
34
+ "validation": None,
35
+ }
36
36
  self.shipment.tracking_number = "123456789012"
37
- self.shipment.selected_rate_carrier = self.carrier
37
+ # Set selected_rate and carrier snapshot
38
+ self.shipment.selected_rate = {
39
+ "carrier_id": "canadapost",
40
+ "carrier_name": "canadapost",
41
+ "service": "canadapost_priority",
42
+ }
43
+ self.shipment.carrier = create_carrier_snapshot(self.carrier)
38
44
  self.shipment.save()
39
45
 
40
46
 
@@ -53,13 +59,116 @@ class TestPickupSchedule(TestFixture):
53
59
  self.assertEqual(response.status_code, status.HTTP_201_CREATED)
54
60
  self.assertDictEqual(response_data, PICKUP_RESPONSE)
55
61
 
62
+ def test_schedule_pickup_with_parcels_count(self):
63
+ """Test scheduling a standalone pickup using parcels_count instead of tracking_numbers."""
64
+ url = reverse(
65
+ "karrio.server.manager:shipment-pickup-request",
66
+ kwargs=dict(carrier_name="canadapost"),
67
+ )
68
+
69
+ with patch("karrio.server.core.gateway.utils.identity") as mock:
70
+ mock.return_value = SCHEDULE_RETURNED_VALUE
71
+ response = self.client.post(f"{url}", PICKUP_DATA_STANDALONE)
72
+ response_data = json.loads(response.content)
73
+
74
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
75
+ self.assertEqual(response_data["confirmation_number"], "27241")
76
+
77
+ def test_schedule_pickup_validation_no_source(self):
78
+ """Test that validation fails when neither tracking_numbers nor parcels_count is provided."""
79
+ url = reverse(
80
+ "karrio.server.manager:shipment-pickup-request",
81
+ kwargs=dict(carrier_name="canadapost"),
82
+ )
83
+
84
+ response = self.client.post(f"{url}", PICKUP_DATA_NO_SOURCE)
85
+
86
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
87
+ response_data = json.loads(response.content)
88
+ self.assertIn("errors", response_data)
89
+
90
+ def test_schedule_pickup_validation_standalone_no_address(self):
91
+ """Test that validation fails for standalone pickup without address."""
92
+ url = reverse(
93
+ "karrio.server.manager:shipment-pickup-request",
94
+ kwargs=dict(carrier_name="canadapost"),
95
+ )
96
+
97
+ response = self.client.post(f"{url}", PICKUP_DATA_STANDALONE_NO_ADDRESS)
98
+
99
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
100
+ response_data = json.loads(response.content)
101
+ self.assertIn("errors", response_data)
102
+
103
+ def test_backward_compatibility_tracking_numbers(self):
104
+ """Test that the existing tracking_numbers flow still works."""
105
+ url = reverse(
106
+ "karrio.server.manager:shipment-pickup-request",
107
+ kwargs=dict(carrier_name="canadapost"),
108
+ )
109
+
110
+ with patch("karrio.server.core.gateway.utils.identity") as mock:
111
+ mock.return_value = SCHEDULE_RETURNED_VALUE
112
+ response = self.client.post(f"{url}", PICKUP_DATA)
113
+
114
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
115
+
116
+ def test_schedule_pickup_with_pickup_type_one_time(self):
117
+ """Test scheduling a pickup with explicit one_time pickup_type."""
118
+ url = reverse(
119
+ "karrio.server.manager:shipment-pickup-request",
120
+ kwargs=dict(carrier_name="canadapost"),
121
+ )
122
+
123
+ with patch("karrio.server.core.gateway.utils.identity") as mock:
124
+ mock.return_value = SCHEDULE_RETURNED_VALUE
125
+ response = self.client.post(f"{url}", PICKUP_DATA_ONE_TIME)
126
+ response_data = json.loads(response.content)
127
+
128
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
129
+ self.assertEqual(response_data["pickup_type"], "one_time")
130
+ self.assertIsNone(response_data["recurrence"])
131
+
132
+ def test_schedule_pickup_with_pickup_type_daily(self):
133
+ """Test scheduling a pickup with daily pickup_type."""
134
+ url = reverse(
135
+ "karrio.server.manager:shipment-pickup-request",
136
+ kwargs=dict(carrier_name="canadapost"),
137
+ )
138
+
139
+ with patch("karrio.server.core.gateway.utils.identity") as mock:
140
+ mock.return_value = SCHEDULE_RETURNED_VALUE
141
+ response = self.client.post(f"{url}", PICKUP_DATA_DAILY)
142
+ response_data = json.loads(response.content)
143
+
144
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
145
+ self.assertEqual(response_data["pickup_type"], "daily")
146
+
147
+ def test_schedule_pickup_with_pickup_type_recurring(self):
148
+ """Test scheduling a pickup with recurring pickup_type and recurrence config."""
149
+ url = reverse(
150
+ "karrio.server.manager:shipment-pickup-request",
151
+ kwargs=dict(carrier_name="canadapost"),
152
+ )
153
+
154
+ with patch("karrio.server.core.gateway.utils.identity") as mock:
155
+ mock.return_value = SCHEDULE_RETURNED_VALUE
156
+ response = self.client.post(f"{url}", PICKUP_DATA_RECURRING)
157
+ response_data = json.loads(response.content)
158
+
159
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
160
+ self.assertEqual(response_data["pickup_type"], "recurring")
161
+ self.assertIsNotNone(response_data["recurrence"])
162
+ self.assertEqual(response_data["recurrence"]["frequency"], "weekly")
163
+ self.assertIn("monday", response_data["recurrence"]["days_of_week"])
164
+
56
165
 
57
166
  class TestPickupDetails(TestFixture):
58
167
  def setUp(self) -> None:
59
168
  super().setUp()
60
169
  self.pickup: models.Pickup = models.Pickup.objects.create(
61
- address=self.address,
62
- pickup_carrier=self.carrier,
170
+ address=self.address_data,
171
+ carrier=create_carrier_snapshot(self.carrier),
63
172
  created_by=self.user,
64
173
  test_mode=True,
65
174
  pickup_date="2020-10-25",
@@ -108,6 +217,7 @@ PICKUP_DATA = {
108
217
  "instruction": "Should not be folded",
109
218
  "package_location": "At the main entrance hall",
110
219
  "address": {
220
+ "id": "adr_aabbccddeeff", # JSON-generated ID format
111
221
  "address_line1": "125 Church St",
112
222
  "person_name": "John Doe",
113
223
  "company_name": "A corp.",
@@ -124,6 +234,123 @@ PICKUP_DATA = {
124
234
  "tracking_numbers": ["123456789012"],
125
235
  }
126
236
 
237
+ # Test data for standalone pickup with parcels_count
238
+ PICKUP_DATA_STANDALONE = {
239
+ "pickup_date": "2020-10-25",
240
+ "ready_time": "13:00",
241
+ "closing_time": "17:00",
242
+ "instruction": "Handle with care",
243
+ "package_location": "Front desk",
244
+ "address": {
245
+ "address_line1": "125 Church St",
246
+ "person_name": "John Doe",
247
+ "company_name": "A corp.",
248
+ "phone_number": "514 000 0000",
249
+ "city": "Moncton",
250
+ "country_code": "CA",
251
+ "postal_code": "E1C4Z8",
252
+ "residential": False,
253
+ "state_code": "NB",
254
+ "email": "john@a.com",
255
+ },
256
+ "parcels_count": 3,
257
+ }
258
+
259
+ # Test data with neither tracking_numbers nor parcels_count (should fail validation)
260
+ PICKUP_DATA_NO_SOURCE = {
261
+ "pickup_date": "2020-10-25",
262
+ "ready_time": "13:00",
263
+ "closing_time": "17:00",
264
+ "address": {
265
+ "address_line1": "125 Church St",
266
+ "person_name": "John Doe",
267
+ "city": "Moncton",
268
+ "country_code": "CA",
269
+ "postal_code": "E1C4Z8",
270
+ },
271
+ }
272
+
273
+ # Test data for standalone pickup without address (should fail validation)
274
+ PICKUP_DATA_STANDALONE_NO_ADDRESS = {
275
+ "pickup_date": "2020-10-25",
276
+ "ready_time": "13:00",
277
+ "closing_time": "17:00",
278
+ "parcels_count": 2,
279
+ }
280
+
281
+ # Test data for pickup with explicit one_time pickup_type
282
+ PICKUP_DATA_ONE_TIME = {
283
+ "pickup_date": "2020-10-25",
284
+ "ready_time": "13:00",
285
+ "closing_time": "17:00",
286
+ "instruction": "One-time pickup",
287
+ "package_location": "Front door",
288
+ "address": {
289
+ "address_line1": "125 Church St",
290
+ "person_name": "John Doe",
291
+ "company_name": "A corp.",
292
+ "phone_number": "514 000 0000",
293
+ "city": "Moncton",
294
+ "country_code": "CA",
295
+ "postal_code": "E1C4Z8",
296
+ "residential": False,
297
+ "state_code": "NB",
298
+ "email": "john@a.com",
299
+ },
300
+ "tracking_numbers": ["123456789012"],
301
+ "pickup_type": "one_time",
302
+ }
303
+
304
+ # Test data for daily pickup
305
+ PICKUP_DATA_DAILY = {
306
+ "pickup_date": "2020-10-25",
307
+ "ready_time": "09:00",
308
+ "closing_time": "17:00",
309
+ "instruction": "Daily pickup",
310
+ "package_location": "Loading dock",
311
+ "address": {
312
+ "address_line1": "125 Church St",
313
+ "person_name": "John Doe",
314
+ "company_name": "A corp.",
315
+ "phone_number": "514 000 0000",
316
+ "city": "Moncton",
317
+ "country_code": "CA",
318
+ "postal_code": "E1C4Z8",
319
+ "residential": False,
320
+ "state_code": "NB",
321
+ "email": "john@a.com",
322
+ },
323
+ "tracking_numbers": ["123456789012"],
324
+ "pickup_type": "daily",
325
+ }
326
+
327
+ # Test data for recurring pickup
328
+ PICKUP_DATA_RECURRING = {
329
+ "pickup_date": "2020-10-25",
330
+ "ready_time": "10:00",
331
+ "closing_time": "16:00",
332
+ "instruction": "Weekly recurring pickup",
333
+ "package_location": "Warehouse bay 3",
334
+ "address": {
335
+ "address_line1": "125 Church St",
336
+ "person_name": "John Doe",
337
+ "company_name": "A corp.",
338
+ "phone_number": "514 000 0000",
339
+ "city": "Moncton",
340
+ "country_code": "CA",
341
+ "postal_code": "E1C4Z8",
342
+ "residential": False,
343
+ "state_code": "NB",
344
+ "email": "john@a.com",
345
+ },
346
+ "tracking_numbers": ["123456789012"],
347
+ "pickup_type": "recurring",
348
+ "recurrence": {
349
+ "frequency": "weekly",
350
+ "days_of_week": ["monday", "wednesday", "friday"],
351
+ },
352
+ }
353
+
127
354
  PICKUP_UPDATE_DATA = {
128
355
  "ready_time": "14:00",
129
356
  "package_location": "At the main entrance hall next to the distributor",
@@ -174,7 +401,12 @@ PICKUP_RESPONSE = {
174
401
  "carrier_id": "canadapost",
175
402
  "confirmation_number": "27241",
176
403
  "pickup_date": "2020-10-25",
177
- "pickup_charge": {"name": "Pickup fees", "amount": 0.0, "currency": "CAD", "id": ANY},
404
+ "pickup_charge": {
405
+ "name": "Pickup fees",
406
+ "amount": 0.0,
407
+ "currency": "CAD",
408
+ "id": ANY,
409
+ },
178
410
  "ready_time": "13:00",
179
411
  "closing_time": "17:00",
180
412
  "test_mode": True,
@@ -197,6 +429,7 @@ PICKUP_RESPONSE = {
197
429
  "address_line2": None,
198
430
  "validate_location": False,
199
431
  "validation": None,
432
+ "meta": {},
200
433
  },
201
434
  "parcels": [
202
435
  {
@@ -217,10 +450,14 @@ PICKUP_RESPONSE = {
217
450
  "freight_class": None,
218
451
  "reference_number": ANY,
219
452
  "options": {},
453
+ "meta": {},
220
454
  }
221
455
  ],
456
+ "parcels_count": None,
222
457
  "instruction": "Should not be folded",
223
458
  "package_location": "At the main entrance hall",
459
+ "pickup_type": "one_time",
460
+ "recurrence": None,
224
461
  "options": {},
225
462
  "metadata": {},
226
463
  "meta": ANY,
@@ -233,7 +470,12 @@ PICKUP_UPDATE_RESPONSE = {
233
470
  "carrier_id": "canadapost",
234
471
  "confirmation_number": "00110215",
235
472
  "pickup_date": "2020-10-25",
236
- "pickup_charge": {"name": "Pickup fees", "amount": 0.0, "currency": "CAD", "id": ANY},
473
+ "pickup_charge": {
474
+ "name": "Pickup fees",
475
+ "amount": 0.0,
476
+ "currency": "CAD",
477
+ "id": ANY,
478
+ },
237
479
  "ready_time": "14:00",
238
480
  "closing_time": "17:00",
239
481
  "test_mode": True,
@@ -256,6 +498,7 @@ PICKUP_UPDATE_RESPONSE = {
256
498
  "address_line2": None,
257
499
  "validate_location": False,
258
500
  "validation": None,
501
+ "meta": {},
259
502
  },
260
503
  "parcels": [
261
504
  {
@@ -276,27 +519,36 @@ PICKUP_UPDATE_RESPONSE = {
276
519
  "freight_class": None,
277
520
  "reference_number": ANY,
278
521
  "options": {},
522
+ "meta": {},
279
523
  }
280
524
  ],
525
+ "parcels_count": None,
281
526
  "instruction": "Should not be folded",
282
527
  "package_location": "At the main entrance hall next to the distributor",
528
+ "pickup_type": "one_time",
529
+ "recurrence": None,
283
530
  "options": {},
284
531
  "metadata": {},
285
532
  "meta": ANY,
286
533
  }
287
534
 
288
535
  PICKUP_CANCEL_RESPONSE = {
289
- "id": ANY,
536
+ "id": None, # Deleted pickup has no id
290
537
  "object_type": "pickup",
291
538
  "carrier_name": "canadapost",
292
539
  "carrier_id": "canadapost",
293
540
  "confirmation_number": "00110215",
294
541
  "pickup_date": "2020-10-25",
295
- "pickup_charge": {"name": "Pickup fees", "amount": 0.0, "currency": "CAD", "id": ANY},
542
+ "pickup_charge": {
543
+ "name": "Pickup fees",
544
+ "amount": 0.0,
545
+ "currency": "CAD",
546
+ "id": None,
547
+ },
296
548
  "ready_time": "13:00",
297
549
  "closing_time": "17:00",
298
550
  "address": {
299
- "id": ANY,
551
+ "id": "adr_001122334455", # JSON address retains its id
300
552
  "postal_code": "E1C4Z8",
301
553
  "city": "Moncton",
302
554
  "federal_tax_id": None,
@@ -314,30 +566,14 @@ PICKUP_CANCEL_RESPONSE = {
314
566
  "validate_location": False,
315
567
  "object_type": "address",
316
568
  "validation": None,
569
+ "meta": {},
317
570
  },
318
- "parcels": [
319
- {
320
- "id": ANY,
321
- "weight": 1.0,
322
- "width": None,
323
- "height": None,
324
- "length": None,
325
- "packaging_type": None,
326
- "package_preset": "canadapost_corrugated_small_box",
327
- "description": None,
328
- "content": None,
329
- "is_document": False,
330
- "weight_unit": "KG",
331
- "dimension_unit": None,
332
- "items": [],
333
- "freight_class": None,
334
- "reference_number": "0000000002",
335
- "options": {},
336
- "object_type": "parcel",
337
- }
338
- ],
571
+ "parcels": [], # Deleted pickup has no parcels
572
+ "parcels_count": None,
339
573
  "instruction": "Should not be folded",
340
574
  "package_location": "At the main entrance hall",
575
+ "pickup_type": "one_time",
576
+ "recurrence": None,
341
577
  "options": {},
342
578
  "metadata": {},
343
579
  "test_mode": True,