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.
Files changed (102) hide show
  1. karrio/server/manager/__init__.py +1 -0
  2. karrio/server/manager/admin.py +1 -0
  3. karrio/server/manager/apps.py +13 -0
  4. karrio/server/manager/migrations/0001_initial.py +1358 -0
  5. karrio/server/manager/migrations/0002_auto_20201127_0721.py +61 -0
  6. karrio/server/manager/migrations/0003_auto_20201230_0820.py +34 -0
  7. karrio/server/manager/migrations/0004_auto_20210125_2125.py +18 -0
  8. karrio/server/manager/migrations/0005_auto_20210216_0758.py +27 -0
  9. karrio/server/manager/migrations/0006_auto_20210307_0438.py +24 -0
  10. karrio/server/manager/migrations/0006_auto_20210308_0302.py +53 -0
  11. karrio/server/manager/migrations/0007_merge_20210311_1428.py +14 -0
  12. karrio/server/manager/migrations/0008_remove_shipment_doc_images.py +17 -0
  13. karrio/server/manager/migrations/0009_auto_20210326_1425.py +28 -0
  14. karrio/server/manager/migrations/0010_auto_20210403_1404.py +28 -0
  15. karrio/server/manager/migrations/0011_auto_20210426_1924.py +48 -0
  16. karrio/server/manager/migrations/0012_auto_20210427_1319.py +24 -0
  17. karrio/server/manager/migrations/0013_customs_invoice_date.py +18 -0
  18. karrio/server/manager/migrations/0014_auto_20210515_0928.py +24 -0
  19. karrio/server/manager/migrations/0015_auto_20210601_0340.py +182 -0
  20. karrio/server/manager/migrations/0016_shipment_archived.py +18 -0
  21. karrio/server/manager/migrations/0017_auto_20210629_1650.py +22 -0
  22. karrio/server/manager/migrations/0018_auto_20210705_1049.py +23 -0
  23. karrio/server/manager/migrations/0019_auto_20210722_1131.py +43 -0
  24. karrio/server/manager/migrations/0020_tracking_messages.py +20 -0
  25. karrio/server/manager/migrations/0021_tracking_estimated_delivery.py +18 -0
  26. karrio/server/manager/migrations/0022_auto_20211122_2100.py +53 -0
  27. karrio/server/manager/migrations/0023_auto_20211227_2141.py +118 -0
  28. karrio/server/manager/migrations/0024_alter_parcel_items.py +18 -0
  29. karrio/server/manager/migrations/0025_auto_20220113_1158.py +25 -0
  30. karrio/server/manager/migrations/0026_parcel_reference_number.py +18 -0
  31. karrio/server/manager/migrations/0027_custom_migration_2021_1.py +47 -0
  32. karrio/server/manager/migrations/0028_auto_20220303_1153.py +39 -0
  33. karrio/server/manager/migrations/0029_auto_20220303_1249.py +55 -0
  34. karrio/server/manager/migrations/0030_alter_shipment_status.py +44 -0
  35. karrio/server/manager/migrations/0031_shipment_invoice.py +34 -0
  36. karrio/server/manager/migrations/0032_custom_migration_2022_3.py +26 -0
  37. karrio/server/manager/migrations/0033_auto_20220504_1335.py +57 -0
  38. karrio/server/manager/migrations/0034_commodity_hs_code.py +18 -0
  39. karrio/server/manager/migrations/0035_parcel_options.py +26 -0
  40. karrio/server/manager/migrations/0036_alter_tracking_shipment.py +24 -0
  41. karrio/server/manager/migrations/0037_auto_20220710_1350.py +28 -0
  42. karrio/server/manager/migrations/0038_alter_tracking_status.py +18 -0
  43. karrio/server/manager/migrations/0039_documentuploadrecord.py +43 -0
  44. karrio/server/manager/migrations/0040_parcel_freight_class.py +18 -0
  45. karrio/server/manager/migrations/0041_alter_commodity_options_alter_parcel_options.py +29 -0
  46. karrio/server/manager/migrations/0042_remove_shipment_shipment_tracking_number_idx_and_more.py +658 -0
  47. karrio/server/manager/migrations/0043_customs_duty_billing_address_and_more.py +62 -0
  48. karrio/server/manager/migrations/0044_address_address_line1_temp_and_more.py +326 -0
  49. karrio/server/manager/migrations/0045_alter_customs_duty_billing_address_and_more.py +45 -0
  50. karrio/server/manager/migrations/0046_auto_20230114_0930.py +78 -0
  51. karrio/server/manager/migrations/0047_remove_shipment_shipment_tracking_number_idx_and_more.py +595 -0
  52. karrio/server/manager/migrations/0048_commodity_title_alter_commodity_description_and_more.py +53 -0
  53. karrio/server/manager/migrations/0049_auto_20230318_0708.py +39 -0
  54. karrio/server/manager/migrations/0050_address_street_number_tracking_account_number_and_more.py +60 -0
  55. karrio/server/manager/migrations/0051_auto_20230330_0556.py +56 -0
  56. karrio/server/manager/migrations/0052_auto_20230520_0811.py +35 -0
  57. karrio/server/manager/migrations/0053_alter_commodity_weight_unit_alter_parcel_weight_unit.py +32 -0
  58. karrio/server/manager/migrations/0054_alter_address_company_name_alter_address_person_name.py +22 -0
  59. karrio/server/manager/migrations/0055_alter_tracking_status.py +32 -0
  60. karrio/server/manager/migrations/0056_tracking_delivery_image_tracking_signature_image.py +22 -0
  61. karrio/server/manager/migrations/0057_alter_customs_invoice_date.py +18 -0
  62. karrio/server/manager/migrations/0058_manifest_shipment_manifest.py +124 -0
  63. karrio/server/manager/migrations/0059_shipment_return_address.py +24 -0
  64. karrio/server/manager/migrations/0060_pickup_meta_alter_address_country_code_and_more.py +527 -0
  65. karrio/server/manager/migrations/0061_alter_customs_incoterm.py +37 -0
  66. karrio/server/manager/migrations/0062_alter_tracking_status.py +35 -0
  67. karrio/server/manager/migrations/__init__.py +0 -0
  68. karrio/server/manager/models.py +984 -0
  69. karrio/server/manager/router.py +3 -0
  70. karrio/server/manager/serializers/__init__.py +50 -0
  71. karrio/server/manager/serializers/address.py +82 -0
  72. karrio/server/manager/serializers/commodity.py +51 -0
  73. karrio/server/manager/serializers/customs.py +84 -0
  74. karrio/server/manager/serializers/document.py +113 -0
  75. karrio/server/manager/serializers/manifest.py +85 -0
  76. karrio/server/manager/serializers/parcel.py +84 -0
  77. karrio/server/manager/serializers/pickup.py +285 -0
  78. karrio/server/manager/serializers/rate.py +19 -0
  79. karrio/server/manager/serializers/shipment.py +869 -0
  80. karrio/server/manager/serializers/tracking.py +250 -0
  81. karrio/server/manager/signals.py +70 -0
  82. karrio/server/manager/tests/__init__.py +10 -0
  83. karrio/server/manager/tests/test_addresses.py +110 -0
  84. karrio/server/manager/tests/test_custom_infos.py +97 -0
  85. karrio/server/manager/tests/test_parcels.py +104 -0
  86. karrio/server/manager/tests/test_pickups.py +345 -0
  87. karrio/server/manager/tests/test_shipments.py +833 -0
  88. karrio/server/manager/tests/test_trackers.py +215 -0
  89. karrio/server/manager/urls.py +10 -0
  90. karrio/server/manager/views/__init__.py +9 -0
  91. karrio/server/manager/views/addresses.py +154 -0
  92. karrio/server/manager/views/customs.py +159 -0
  93. karrio/server/manager/views/documents.py +131 -0
  94. karrio/server/manager/views/manifests.py +160 -0
  95. karrio/server/manager/views/parcels.py +155 -0
  96. karrio/server/manager/views/pickups.py +182 -0
  97. karrio/server/manager/views/shipments.py +335 -0
  98. karrio/server/manager/views/trackers.py +364 -0
  99. karrio_server_manager-2025.5rc1.dist-info/METADATA +28 -0
  100. karrio_server_manager-2025.5rc1.dist-info/RECORD +102 -0
  101. karrio_server_manager-2025.5rc1.dist-info/WHEEL +5 -0
  102. karrio_server_manager-2025.5rc1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,984 @@
1
+ import typing
2
+ import functools
3
+ import django.conf as conf
4
+ import django.urls as urls
5
+ import django.db.models as models
6
+ import django.db.models.fields as fields
7
+
8
+ import karrio.server.core.utils as utils
9
+ import karrio.server.core.models as core
10
+ import karrio.server.providers.models as providers
11
+ import karrio.server.core.serializers as serializers
12
+
13
+
14
+ # -----------------------------------------------------------
15
+ # Model Managers
16
+ # -----------------------------------------------------------
17
+ # region
18
+
19
+
20
+ class AddressManager(models.Manager):
21
+ def get_queryset(self):
22
+ return super().get_queryset().defer("validation")
23
+
24
+
25
+ class ParcelManager(models.Manager):
26
+ def get_queryset(self):
27
+ return super().get_queryset().prefetch_related("items")
28
+
29
+
30
+ class CommodityManager(models.Manager):
31
+ def get_queryset(self):
32
+ return (
33
+ super().get_queryset().select_related("parent").prefetch_related("children")
34
+ )
35
+
36
+
37
+ class CustomsManager(models.Manager):
38
+ def get_queryset(self):
39
+ return (
40
+ super()
41
+ .get_queryset()
42
+ .select_related("duty_billing_address")
43
+ .prefetch_related("commodities")
44
+ )
45
+
46
+
47
+ class PickupManager(models.Manager):
48
+ def get_queryset(self):
49
+ return (
50
+ super()
51
+ .get_queryset()
52
+ .select_related("pickup_carrier")
53
+ .prefetch_related("shipments")
54
+ )
55
+
56
+
57
+ class ShipmentManager(models.Manager):
58
+ def get_queryset(self):
59
+ return (
60
+ super()
61
+ .get_queryset()
62
+ .select_related(
63
+ "created_by",
64
+ "recipient",
65
+ "shipper",
66
+ "customs",
67
+ "manifest",
68
+ "return_address",
69
+ "billing_address",
70
+ "shipment_tracker",
71
+ "selected_rate_carrier",
72
+ "shipment_upload_record",
73
+ )
74
+ .prefetch_related(
75
+ "parcels",
76
+ "carriers",
77
+ )
78
+ )
79
+
80
+
81
+ class TrackingManager(models.Manager):
82
+ def get_queryset(self):
83
+ return (
84
+ super()
85
+ .get_queryset()
86
+ .defer("shipment")
87
+ .prefetch_related(
88
+ "tracking_carrier",
89
+ )
90
+ .select_related(
91
+ "created_by",
92
+ )
93
+ )
94
+
95
+
96
+ class DocumentUploadRecordManager(models.Manager):
97
+ def get_queryset(self):
98
+ return super().get_queryset().select_related("upload_carrier")
99
+
100
+
101
+ class ManifestManager(models.Manager):
102
+ def get_queryset(self):
103
+ # Load manifest details and associated carrier data efficiently
104
+ return (
105
+ super().get_queryset().select_related("manifest_carrier").defer("manifest")
106
+ )
107
+
108
+
109
+ # endregion
110
+
111
+ # -----------------------------------------------------------
112
+ # Shipping Management Models
113
+ # -----------------------------------------------------------
114
+ # region
115
+
116
+
117
+ @core.register_model
118
+ class Address(core.OwnedEntity):
119
+ HIDDEN_PROPS = (
120
+ "shipper_shipment",
121
+ "recipient_shipment",
122
+ "billing_address_shipment",
123
+ *(("org",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
124
+ *(
125
+ ("shipper_order", "recipient_order")
126
+ if conf.settings.ORDERS_MANAGEMENT
127
+ else tuple()
128
+ ),
129
+ )
130
+ objects = AddressManager()
131
+
132
+ class Meta:
133
+ db_table = "address"
134
+ verbose_name = "Address"
135
+ verbose_name_plural = "Addresses"
136
+ ordering = ["-created_at"]
137
+
138
+ id = models.CharField(
139
+ max_length=50,
140
+ primary_key=True,
141
+ default=functools.partial(core.uuid, prefix="adr_"),
142
+ editable=False,
143
+ )
144
+
145
+ postal_code = models.CharField(max_length=10, null=True, blank=True, db_index=True)
146
+ country_code = models.CharField(
147
+ max_length=20, choices=serializers.COUNTRIES, db_index=True
148
+ )
149
+ email = models.EmailField(null=True, blank=True, db_index=True)
150
+ city = models.CharField(max_length=30, null=True, blank=True, db_index=True)
151
+ federal_tax_id = models.CharField(max_length=20, null=True, blank=True)
152
+ state_tax_id = models.CharField(max_length=20, null=True, blank=True)
153
+ person_name = models.CharField(max_length=50, null=True, blank=True, db_index=True)
154
+ company_name = models.CharField(max_length=50, null=True, blank=True, db_index=True)
155
+ phone_number = models.CharField(max_length=50, null=True, blank=True)
156
+ street_number = models.CharField(
157
+ max_length=20, null=True, blank=True, db_index=True
158
+ )
159
+ address_line1 = models.CharField(
160
+ max_length=100, null=True, blank=True, db_index=True
161
+ )
162
+ address_line2 = models.CharField(
163
+ max_length=100, null=True, blank=True, db_index=True
164
+ )
165
+ state_code = models.CharField(max_length=20, null=True, blank=True, db_index=True)
166
+ suburb = models.CharField(max_length=20, null=True, blank=True)
167
+ residential = models.BooleanField(null=True)
168
+
169
+ validate_location = models.BooleanField(null=True)
170
+ validation = models.JSONField(blank=True, null=True)
171
+
172
+ @property
173
+ def object_type(self):
174
+ return "address"
175
+
176
+ @property
177
+ def shipment(self):
178
+ if hasattr(self, "shipper_shipment"):
179
+ return self.shipper_shipment
180
+ if hasattr(self, "recipient_shipment"):
181
+ return self.recipient_shipment
182
+ if hasattr(self, "billing_address_shipment"):
183
+ return self.billing_address_shipment
184
+
185
+ return None
186
+
187
+ @property
188
+ def customs(self):
189
+ if hasattr(self, "duty_billing_address_customs"):
190
+ return self.duty_billing_address_customs
191
+
192
+ return None
193
+
194
+ @property
195
+ def order(self):
196
+ if hasattr(self, "shipper_order"):
197
+ return self.shipper_order
198
+ if hasattr(self, "recipient_order"):
199
+ return self.recipient_order
200
+ if hasattr(self, "bill_to_order"):
201
+ return self.bill_to_order
202
+
203
+ return None
204
+
205
+
206
+ @core.register_model
207
+ class Parcel(core.OwnedEntity):
208
+ HIDDEN_PROPS = (
209
+ "parcel_shipment",
210
+ *(("org",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
211
+ )
212
+ objects = ParcelManager()
213
+
214
+ class Meta:
215
+ db_table = "parcel"
216
+ verbose_name = "Parcel"
217
+ verbose_name_plural = "Parcels"
218
+ ordering = ["created_at"]
219
+
220
+ id = models.CharField(
221
+ max_length=50,
222
+ primary_key=True,
223
+ default=functools.partial(core.uuid, prefix="pcl_"),
224
+ editable=False,
225
+ )
226
+
227
+ weight = models.FloatField(blank=True, null=True)
228
+ width = models.FloatField(blank=True, null=True)
229
+ height = models.FloatField(blank=True, null=True)
230
+ length = models.FloatField(blank=True, null=True)
231
+ packaging_type = models.CharField(max_length=50, null=True, blank=True)
232
+ package_preset = models.CharField(max_length=50, null=True, blank=True)
233
+ is_document = models.BooleanField(default=False, blank=True, null=True)
234
+ description = models.CharField(max_length=35, null=True, blank=True)
235
+ content = models.CharField(max_length=35, null=True, blank=True)
236
+ reference_number = models.CharField(
237
+ max_length=50, null=True, blank=True, db_index=True
238
+ )
239
+ weight_unit = models.CharField(
240
+ max_length=2, choices=serializers.WEIGHT_UNIT, null=True, blank=True
241
+ )
242
+ dimension_unit = models.CharField(
243
+ max_length=2, choices=serializers.DIMENSION_UNIT, null=True, blank=True
244
+ )
245
+ items = models.ManyToManyField(
246
+ "Commodity", blank=True, related_name="commodity_parcel"
247
+ )
248
+ freight_class = models.CharField(max_length=10, null=True, blank=True)
249
+ options = models.JSONField(
250
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
251
+ )
252
+
253
+ def delete(self, *args, **kwargs):
254
+ self.items.all().delete()
255
+ return super().delete(*args, **kwargs)
256
+
257
+ @property
258
+ def object_type(self):
259
+ return "parcel"
260
+
261
+ @property
262
+ def shipment(self):
263
+ return self.parcel_shipment.first()
264
+
265
+
266
+ @core.register_model
267
+ class Commodity(core.OwnedEntity):
268
+ HIDDEN_PROPS = (
269
+ "children",
270
+ "commodity_parcel",
271
+ "commodity_customs",
272
+ *(("org",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
273
+ )
274
+ objects = CommodityManager()
275
+
276
+ class Meta:
277
+ db_table = "commodity"
278
+ verbose_name = "Commodity"
279
+ verbose_name_plural = "Commodities"
280
+ ordering = ["created_at"]
281
+
282
+ id = models.CharField(
283
+ max_length=50,
284
+ primary_key=True,
285
+ default=functools.partial(core.uuid, prefix="cdt_"),
286
+ editable=False,
287
+ )
288
+
289
+ weight = models.FloatField(blank=True, null=True)
290
+ quantity = models.IntegerField(blank=True, null=True)
291
+ title = models.CharField(max_length=35, null=True, blank=True)
292
+ description = models.CharField(max_length=100, null=True, blank=True)
293
+ sku = models.CharField(max_length=35, null=True, blank=True, db_index=True)
294
+ hs_code = models.CharField(max_length=35, null=True, blank=True, db_index=True)
295
+ value_amount = models.FloatField(blank=True, null=True)
296
+ weight_unit = models.CharField(
297
+ max_length=2, choices=serializers.WEIGHT_UNIT, null=True, blank=True
298
+ )
299
+ value_currency = models.CharField(
300
+ max_length=3, choices=serializers.CURRENCIES, null=True, blank=True
301
+ )
302
+ origin_country = models.CharField(
303
+ max_length=3,
304
+ choices=serializers.COUNTRIES,
305
+ null=True,
306
+ blank=True,
307
+ db_index=True,
308
+ )
309
+ parent = models.ForeignKey(
310
+ "self",
311
+ on_delete=models.SET_NULL,
312
+ null=True,
313
+ blank=True,
314
+ related_name="children",
315
+ )
316
+ metadata = models.JSONField(
317
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
318
+ )
319
+
320
+ def delete(self, *args, **kwargs):
321
+ self.children.all().delete()
322
+ return super().delete(*args, **kwargs)
323
+
324
+ @property
325
+ def object_type(self):
326
+ return "commodity"
327
+
328
+ @property
329
+ def parcel(self):
330
+ return self.commodity_parcel.first()
331
+
332
+ @property
333
+ def customs(self):
334
+ return self.commodity_customs.first()
335
+
336
+ @property
337
+ def shipment(self):
338
+ related = self.customs or self.parcel
339
+
340
+ return getattr(related, "shipment", None)
341
+
342
+ @property
343
+ def order(self):
344
+ if hasattr(self, "order_link"):
345
+ return self.order_link.order
346
+ if self.parent is not None:
347
+ return self.parent.order
348
+
349
+ return None
350
+
351
+
352
+ @core.register_model
353
+ class Customs(core.OwnedEntity):
354
+ DIRECT_PROPS = [
355
+ "content_description",
356
+ "content_type",
357
+ "incoterm",
358
+ "commercial_invoice",
359
+ "certify",
360
+ "duty",
361
+ "created_by",
362
+ "signer",
363
+ "invoice",
364
+ "invoice_date",
365
+ "options",
366
+ ]
367
+ RELATIONAL_PROPS = ["commodities", "duty_billing_address"]
368
+ HIDDEN_PROPS = (
369
+ "customs_shipment",
370
+ *(("org",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
371
+ )
372
+ objects = CustomsManager()
373
+
374
+ class Meta:
375
+ db_table = "customs"
376
+ verbose_name = "Customs Info"
377
+ verbose_name_plural = "Customs Info"
378
+ ordering = ["-created_at"]
379
+
380
+ id = models.CharField(
381
+ max_length=50,
382
+ primary_key=True,
383
+ default=functools.partial(core.uuid, prefix="cst_"),
384
+ editable=False,
385
+ )
386
+
387
+ certify = models.BooleanField(null=True)
388
+ commercial_invoice = models.BooleanField(null=True)
389
+ content_type = models.CharField(max_length=50, null=True, blank=True, db_index=True)
390
+ content_description = models.CharField(max_length=250, null=True, blank=True)
391
+ incoterm = models.CharField(
392
+ max_length=20, choices=serializers.INCOTERMS, db_index=True
393
+ )
394
+ invoice = models.CharField(max_length=50, null=True, blank=True)
395
+ invoice_date = models.CharField(max_length=50, null=True, blank=True)
396
+ signer = models.CharField(max_length=50, null=True, blank=True)
397
+
398
+ duty = models.JSONField(
399
+ blank=True, null=True, default=functools.partial(utils.identity, value=None)
400
+ )
401
+ options = models.JSONField(
402
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
403
+ )
404
+
405
+ # System Reference fields
406
+
407
+ commodities = models.ManyToManyField(
408
+ "Commodity", blank=True, related_name="commodity_customs"
409
+ )
410
+ duty_billing_address = models.OneToOneField(
411
+ "Address",
412
+ null=True,
413
+ on_delete=models.SET_NULL,
414
+ related_name="duty_billing_address_customs",
415
+ )
416
+
417
+ def delete(self, *args, **kwargs):
418
+ self.commodities.all().delete()
419
+ return super().delete(*args, **kwargs)
420
+
421
+ @property
422
+ def object_type(self):
423
+ return "customs_info"
424
+
425
+ @property
426
+ def shipment(self):
427
+ if hasattr(self, "customs_shipment"):
428
+ return self.customs_shipment
429
+
430
+ return None
431
+
432
+
433
+ @core.register_model
434
+ class Pickup(core.OwnedEntity):
435
+ DIRECT_PROPS = [
436
+ "confirmation_number",
437
+ "pickup_date",
438
+ "instruction",
439
+ "package_location",
440
+ "ready_time",
441
+ "closing_time",
442
+ "test_mode",
443
+ "pickup_charge",
444
+ "created_by",
445
+ "metadata",
446
+ "meta",
447
+ ]
448
+ objects = PickupManager()
449
+
450
+ class Meta:
451
+ db_table = "pickup"
452
+ verbose_name = "Pickup"
453
+ verbose_name_plural = "Pickups"
454
+ ordering = ["-created_at"]
455
+
456
+ id = models.CharField(
457
+ max_length=50,
458
+ primary_key=True,
459
+ default=functools.partial(core.uuid, prefix="pck_"),
460
+ editable=False,
461
+ )
462
+ confirmation_number = models.CharField(max_length=50, blank=False, db_index=True)
463
+ test_mode = models.BooleanField(null=False)
464
+ pickup_date = models.DateField(blank=False)
465
+ ready_time = models.CharField(max_length=5, blank=False)
466
+ closing_time = models.CharField(max_length=5, blank=False)
467
+ instruction = models.CharField(max_length=200, null=True, blank=True)
468
+ package_location = models.CharField(max_length=200, null=True, blank=True)
469
+
470
+ options = models.JSONField(
471
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
472
+ )
473
+ pickup_charge = models.JSONField(blank=True, null=True)
474
+ address = models.ForeignKey(
475
+ "Address",
476
+ on_delete=models.CASCADE,
477
+ related_name="address_pickup",
478
+ blank=True,
479
+ null=True,
480
+ )
481
+ metadata = models.JSONField(
482
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
483
+ )
484
+ meta = models.JSONField(
485
+ blank=True, default=functools.partial(utils.identity, value={})
486
+ )
487
+
488
+ # System Reference fields
489
+
490
+ pickup_carrier = models.ForeignKey(providers.Carrier, on_delete=models.CASCADE)
491
+ shipments = models.ManyToManyField("Shipment", related_name="shipment_pickup")
492
+
493
+ def delete(self, *args, **kwargs):
494
+ handle = self.address or super()
495
+ return handle.delete(*args, **kwargs)
496
+
497
+ @property
498
+ def object_type(self):
499
+ return "pickup"
500
+
501
+ # Computed properties
502
+
503
+ @property
504
+ def carrier_id(self) -> str:
505
+ return typing.cast(providers.Carrier, self.pickup_carrier).carrier_id
506
+
507
+ @property
508
+ def carrier_name(self) -> str:
509
+ return typing.cast(providers.Carrier, self.pickup_carrier).data.carrier_name
510
+
511
+ @property
512
+ def parcels(self) -> typing.List[Parcel]:
513
+ return sum(
514
+ [list(shipment.parcels.all()) for shipment in self.shipments.all()], []
515
+ )
516
+
517
+ @property
518
+ def tracking_numbers(self) -> typing.List[str]:
519
+ return [shipment.tracking_number for shipment in self.shipments.all()]
520
+
521
+
522
+ @core.register_model
523
+ class Tracking(core.OwnedEntity):
524
+ DIRECT_PROPS = [
525
+ "metadata",
526
+ "info",
527
+ ]
528
+ HIDDEN_PROPS = (
529
+ "tracking_carrier",
530
+ *(("org",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
531
+ )
532
+ objects = TrackingManager()
533
+
534
+ class Meta:
535
+ db_table = "tracking-status"
536
+ verbose_name = "Tracking Status"
537
+ verbose_name_plural = "Tracking Statuses"
538
+ ordering = ["-created_at"]
539
+
540
+ id = models.CharField(
541
+ max_length=50,
542
+ primary_key=True,
543
+ default=functools.partial(core.uuid, prefix="trk_"),
544
+ editable=False,
545
+ )
546
+
547
+ status = models.CharField(
548
+ max_length=25,
549
+ choices=serializers.TRACKER_STATUS,
550
+ default=serializers.TRACKER_STATUS[0][0],
551
+ db_index=True,
552
+ )
553
+ tracking_number = models.CharField(max_length=50, db_index=True)
554
+ account_number = models.CharField(max_length=35, null=True, blank=True)
555
+ reference = models.CharField(max_length=35, null=True, blank=True)
556
+ info = models.JSONField(
557
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
558
+ )
559
+ events = models.JSONField(
560
+ blank=True, null=True, default=functools.partial(utils.identity, value=[])
561
+ )
562
+ delivered = models.BooleanField(blank=True, null=True, default=False)
563
+ estimated_delivery = models.DateField(null=True, blank=True)
564
+ test_mode = models.BooleanField(null=False)
565
+ messages = models.JSONField(
566
+ blank=True, null=True, default=functools.partial(utils.identity, value=[])
567
+ )
568
+ options = models.JSONField(
569
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
570
+ )
571
+ meta = models.JSONField(
572
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
573
+ )
574
+ metadata = models.JSONField(
575
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
576
+ )
577
+
578
+ delivery_image = models.TextField(max_length=None, null=True, blank=True)
579
+ signature_image = models.TextField(max_length=None, null=True, blank=True)
580
+
581
+ # System Reference fields
582
+
583
+ tracking_carrier = models.ForeignKey(providers.Carrier, on_delete=models.CASCADE)
584
+ shipment = models.OneToOneField(
585
+ "Shipment", on_delete=models.CASCADE, related_name="shipment_tracker", null=True
586
+ )
587
+
588
+ @property
589
+ def object_type(self):
590
+ return "tracker"
591
+
592
+ # Computed properties
593
+
594
+ @property
595
+ def carrier_id(self) -> str:
596
+ return typing.cast(providers.Carrier, self.tracking_carrier).carrier_id
597
+
598
+ @property
599
+ def carrier_name(self) -> str:
600
+ return typing.cast(providers.Carrier, self.tracking_carrier).data.carrier_name
601
+
602
+ @property
603
+ def pending(self) -> bool:
604
+ return len(self.events) == 0 or (
605
+ len(self.events) == 1 and self.events[0].get("code") == "CREATED"
606
+ )
607
+
608
+ @property
609
+ def delivery_image_url(self) -> str:
610
+ if self.delivery_image is None:
611
+ return None
612
+
613
+ return urls.reverse(
614
+ "karrio.server.manager:tracker-docs",
615
+ kwargs=dict(pk=self.pk, doc="delivery_image"),
616
+ )
617
+
618
+ @property
619
+ def signature_image_url(self) -> str:
620
+ if self.signature_image is None:
621
+ return None
622
+
623
+ return urls.reverse(
624
+ "karrio.server.manager:tracker-docs",
625
+ kwargs=dict(pk=self.pk, doc="signature_image"),
626
+ )
627
+
628
+
629
+ @core.register_model
630
+ class Shipment(core.OwnedEntity):
631
+ DIRECT_PROPS = [
632
+ "options",
633
+ "services",
634
+ "status",
635
+ "meta",
636
+ "label_type",
637
+ "tracking_number",
638
+ "tracking_url",
639
+ "shipment_identifier",
640
+ "test_mode",
641
+ "messages",
642
+ "rates",
643
+ "payment",
644
+ "metadata",
645
+ "created_by",
646
+ "reference",
647
+ ]
648
+ RELATIONAL_PROPS = [
649
+ "shipper",
650
+ "recipient",
651
+ "parcels",
652
+ "customs",
653
+ "selected_rate",
654
+ "return_address",
655
+ "billing_address",
656
+ ]
657
+ HIDDEN_PROPS = (
658
+ "carriers",
659
+ "label",
660
+ "invoice",
661
+ "shipment_pickup",
662
+ "shipment_tracker",
663
+ "selected_rate_carrier",
664
+ *(("org",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
665
+ )
666
+ objects = ShipmentManager()
667
+
668
+ class Meta:
669
+ db_table = "shipment"
670
+ verbose_name = "Shipment"
671
+ verbose_name_plural = "Shipments"
672
+ ordering = ["-created_at"]
673
+ indexes = [
674
+ models.Index(
675
+ fields.json.KeyTextTransform("service", "selected_rate"),
676
+ condition=models.Q(meta__object_id__isnull=False),
677
+ name="shipment_service_idx",
678
+ ),
679
+ ]
680
+
681
+ id = models.CharField(
682
+ max_length=50,
683
+ primary_key=True,
684
+ default=functools.partial(core.uuid, prefix="shp_"),
685
+ editable=False,
686
+ )
687
+ status = models.CharField(
688
+ max_length=50,
689
+ choices=serializers.SHIPMENT_STATUS,
690
+ default=serializers.SHIPMENT_STATUS[0][0],
691
+ db_index=True,
692
+ )
693
+
694
+ recipient = models.OneToOneField(
695
+ "Address", on_delete=models.CASCADE, related_name="recipient_shipment"
696
+ )
697
+ shipper = models.OneToOneField(
698
+ "Address", on_delete=models.CASCADE, related_name="shipper_shipment"
699
+ )
700
+ return_address = models.OneToOneField(
701
+ "Address",
702
+ null=True,
703
+ on_delete=models.SET_NULL,
704
+ related_name="return_address_shipment",
705
+ )
706
+ billing_address = models.OneToOneField(
707
+ "Address",
708
+ null=True,
709
+ on_delete=models.SET_NULL,
710
+ related_name="billing_address_shipment",
711
+ )
712
+ label_type = models.CharField(max_length=25, null=True, blank=True)
713
+ tracking_number = models.CharField(
714
+ max_length=50, null=True, blank=True, db_index=True
715
+ )
716
+ shipment_identifier = models.CharField(max_length=50, null=True, blank=True)
717
+ tracking_url = models.TextField(max_length=None, null=True, blank=True)
718
+ test_mode = models.BooleanField(null=False)
719
+ customs = models.OneToOneField(
720
+ "Customs",
721
+ on_delete=models.SET_NULL,
722
+ blank=True,
723
+ null=True,
724
+ related_name="customs_shipment",
725
+ )
726
+
727
+ label = models.TextField(max_length=None, null=True, blank=True)
728
+ invoice = models.TextField(max_length=None, null=True, blank=True)
729
+ reference = models.CharField(max_length=35, null=True, blank=True)
730
+ selected_rate = models.JSONField(blank=True, null=True)
731
+ payment = models.JSONField(
732
+ blank=True, null=True, default=functools.partial(utils.identity, value=None)
733
+ )
734
+ options = models.JSONField(
735
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
736
+ )
737
+ services = models.JSONField(
738
+ blank=True, null=True, default=functools.partial(utils.identity, value=[])
739
+ )
740
+ messages = models.JSONField(
741
+ blank=True, null=True, default=functools.partial(utils.identity, value=[])
742
+ )
743
+ meta = models.JSONField(
744
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
745
+ )
746
+ metadata = models.JSONField(
747
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
748
+ )
749
+
750
+ # System Reference fields
751
+
752
+ rates = models.JSONField(
753
+ blank=True, null=True, default=functools.partial(utils.identity, value=[])
754
+ )
755
+ parcels = models.ManyToManyField("Parcel", related_name="parcel_shipment")
756
+ carriers = models.ManyToManyField(
757
+ providers.Carrier, blank=True, related_name="related_shipments"
758
+ )
759
+ selected_rate_carrier = models.ForeignKey(
760
+ providers.Carrier,
761
+ on_delete=models.CASCADE,
762
+ related_name="carrier_shipments",
763
+ blank=True,
764
+ null=True,
765
+ )
766
+ manifest = models.ForeignKey(
767
+ "Manifest",
768
+ on_delete=models.SET_NULL,
769
+ related_name="shipments",
770
+ blank=True,
771
+ null=True,
772
+ )
773
+
774
+ def delete(self, *args, **kwargs):
775
+ self.parcels.all().delete()
776
+ self.customs and self.customs.delete()
777
+ return super().delete(*args, **kwargs)
778
+
779
+ @property
780
+ def object_type(self):
781
+ return "shipment"
782
+
783
+ # Computed properties
784
+
785
+ @property
786
+ def carrier_id(self) -> str:
787
+ return typing.cast(providers.Carrier, self.selected_rate_carrier).carrier_id
788
+
789
+ @property
790
+ def carrier_name(self) -> str:
791
+ return typing.cast(providers.Carrier, self.selected_rate_carrier).carrier_name
792
+
793
+ @property
794
+ def tracker_id(self) -> typing.Optional[str]:
795
+ return getattr(self.tracker, "id", None)
796
+
797
+ @property
798
+ def carrier_ids(self) -> typing.List[str]:
799
+ return [carrier.carrier_id for carrier in self.carriers.all()]
800
+
801
+ @property
802
+ def selected_rate_id(self) -> str:
803
+ return (
804
+ typing.cast(dict, self.selected_rate).get("id")
805
+ if self.selected_rate is not None
806
+ else None
807
+ )
808
+
809
+ @property
810
+ def service(self) -> str:
811
+ return (
812
+ typing.cast(dict, self.selected_rate).get("service")
813
+ if self.selected_rate is not None
814
+ else None
815
+ )
816
+
817
+ @property
818
+ def tracker(self):
819
+ if hasattr(self, "shipment_tracker"):
820
+ return self.shipment_tracker
821
+
822
+ return None
823
+
824
+ @property
825
+ def label_url(self) -> str:
826
+ if self.label is None:
827
+ return None
828
+
829
+ return urls.reverse(
830
+ "karrio.server.manager:shipment-docs",
831
+ kwargs=dict(
832
+ pk=self.pk, doc="label", format=(self.label_type or "PDF").lower()
833
+ ),
834
+ )
835
+
836
+ @property
837
+ def invoice_url(self) -> str:
838
+ if self.invoice is None:
839
+ return None
840
+
841
+ return urls.reverse(
842
+ "karrio.server.manager:shipment-docs",
843
+ kwargs=dict(pk=self.pk, doc="invoice", format="pdf"),
844
+ )
845
+
846
+
847
+ @core.register_model
848
+ class DocumentUploadRecord(core.OwnedEntity):
849
+ HIDDEN_PROPS = (
850
+ "upload_carrier",
851
+ *(("org",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
852
+ )
853
+ objects = DocumentUploadRecordManager()
854
+
855
+ class Meta:
856
+ db_table = "document-upload-record"
857
+ verbose_name = "Document Upload Record"
858
+ verbose_name_plural = "Document Upload Records"
859
+ ordering = ["-created_at"]
860
+
861
+ id = models.CharField(
862
+ max_length=50,
863
+ primary_key=True,
864
+ default=functools.partial(core.uuid, prefix="uprec_"),
865
+ editable=False,
866
+ )
867
+ documents = models.JSONField(
868
+ blank=True, null=True, default=functools.partial(utils.identity, value=[])
869
+ )
870
+ messages = models.JSONField(
871
+ blank=True, null=True, default=functools.partial(utils.identity, value=[])
872
+ )
873
+ meta = models.JSONField(
874
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
875
+ )
876
+ options = models.JSONField(
877
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
878
+ )
879
+ reference = models.CharField(max_length=100, null=True, blank=True)
880
+
881
+ # System Reference fields
882
+
883
+ upload_carrier = models.ForeignKey(providers.Carrier, on_delete=models.CASCADE)
884
+ shipment = models.OneToOneField(
885
+ "Shipment",
886
+ on_delete=models.CASCADE,
887
+ related_name="shipment_upload_record",
888
+ )
889
+
890
+ # Computed properties
891
+
892
+ @property
893
+ def carrier_id(self) -> str:
894
+ return typing.cast(providers.Carrier, self.upload_carrier).carrier_id
895
+
896
+ @property
897
+ def carrier_name(self) -> str:
898
+ return typing.cast(providers.Carrier, self.upload_carrier).data.carrier_name
899
+
900
+
901
+ @core.register_model
902
+ class Manifest(core.OwnedEntity):
903
+ DIRECT_PROPS = [
904
+ "meta",
905
+ "options",
906
+ "metadata",
907
+ "messages",
908
+ "created_by",
909
+ "reference",
910
+ ]
911
+ RELATIONAL_PROPS = [
912
+ "address",
913
+ ]
914
+ HIDDEN_PROPS = (
915
+ "manifest_carrier",
916
+ *(("org",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
917
+ )
918
+ objects = ManifestManager()
919
+
920
+ class Meta:
921
+ db_table = "manifest"
922
+ verbose_name = "Manifest"
923
+ verbose_name_plural = "Manifests"
924
+ ordering = ["-created_at"]
925
+
926
+ id = models.CharField(
927
+ max_length=50,
928
+ primary_key=True,
929
+ default=functools.partial(core.uuid, prefix="manf_"),
930
+ editable=False,
931
+ )
932
+ address = models.OneToOneField(
933
+ "Address", on_delete=models.CASCADE, related_name="manifest"
934
+ )
935
+ metadata = models.JSONField(
936
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
937
+ )
938
+ meta = models.JSONField(
939
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
940
+ )
941
+ options = models.JSONField(
942
+ blank=True, null=True, default=functools.partial(utils.identity, value={})
943
+ )
944
+ messages = models.JSONField(
945
+ blank=True, null=True, default=functools.partial(utils.identity, value=[])
946
+ )
947
+ reference = models.CharField(max_length=100, null=True, blank=True)
948
+ manifest = models.TextField(max_length=None, null=True, blank=True)
949
+ test_mode = models.BooleanField(null=False)
950
+
951
+ # System Reference fields
952
+
953
+ manifest_carrier = models.ForeignKey(providers.Carrier, on_delete=models.CASCADE)
954
+
955
+ # Computed properties
956
+
957
+ @property
958
+ def object_type(self):
959
+ return "manifest"
960
+
961
+ @property
962
+ def carrier_id(self) -> str:
963
+ return self.manifest_carrier.carrier_id
964
+
965
+ @property
966
+ def carrier_name(self) -> str:
967
+ return self.manifest_carrier.data.carrier_name
968
+
969
+ @property
970
+ def manifest_url(self) -> str:
971
+ if self.manifest is None:
972
+ return None
973
+
974
+ return urls.reverse(
975
+ "karrio.server.manager:manifest-docs",
976
+ kwargs=dict(pk=self.pk, doc="manifest", format="pdf"),
977
+ )
978
+
979
+ @property
980
+ def shipment_identifiers(self) -> typing.List[str]:
981
+ return [shipment.shipment_identifier for shipment in self.shipments.all()]
982
+
983
+
984
+ # endregion