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,3 @@
1
+ from rest_framework.routers import DefaultRouter
2
+
3
+ router = DefaultRouter(trailing_slash=False)
@@ -0,0 +1,50 @@
1
+ from karrio.server.serializers import *
2
+ from karrio.server.core.serializers import *
3
+ from karrio.server.manager.serializers.address import (
4
+ AddressSerializer,
5
+ can_mutate_address,
6
+ )
7
+ from karrio.server.manager.serializers.parcel import (
8
+ ParcelSerializer,
9
+ can_mutate_parcel,
10
+ )
11
+ from karrio.server.manager.serializers.customs import (
12
+ CustomsSerializer,
13
+ can_mutate_customs,
14
+ )
15
+ from karrio.server.manager.serializers.commodity import (
16
+ CommoditySerializer,
17
+ can_mutate_commodity,
18
+ )
19
+ from karrio.server.manager.serializers.rate import RateSerializer
20
+ from karrio.server.manager.serializers.tracking import (
21
+ TrackingSerializer,
22
+ TrackerUpdateData,
23
+ update_shipment_tracker,
24
+ can_mutate_tracker,
25
+ )
26
+ from karrio.server.manager.serializers.shipment import (
27
+ ShipmentRateData,
28
+ ShipmentSerializer,
29
+ ShipmentUpdateData,
30
+ ShipmentPurchaseData,
31
+ ShipmentPurchaseSerializer,
32
+ ShipmentCancelSerializer,
33
+ create_shipment_tracker,
34
+ reset_related_shipment_rates,
35
+ can_mutate_shipment,
36
+ buy_shipment_label,
37
+ fetch_shipment_rates,
38
+ )
39
+ from karrio.server.manager.serializers.pickup import (
40
+ PickupData,
41
+ PickupUpdateData,
42
+ PickupCancelData,
43
+ )
44
+ from karrio.server.manager.serializers.document import (
45
+ DocumentUploadSerializer,
46
+ can_upload_shipment_document,
47
+ )
48
+ from karrio.server.manager.serializers.manifest import (
49
+ ManifestSerializer,
50
+ )
@@ -0,0 +1,82 @@
1
+ from rest_framework import status
2
+
3
+ from karrio.server.core.exceptions import APIException
4
+ from karrio.server.serializers import owned_model_serializer
5
+ from karrio.core import utils
6
+ from karrio.server.core import gateway
7
+ from karrio.server.core.serializers import AddressData, ShipmentStatus
8
+ from karrio.server.manager import models
9
+
10
+
11
+ @owned_model_serializer
12
+ class AddressSerializer(AddressData):
13
+ def validate(self, data):
14
+ validated_data = super().validate(data)
15
+ should_validate = validated_data.get("validate_location") is True or (
16
+ self.instance is not None and self.instance.validate_location
17
+ )
18
+
19
+ if should_validate:
20
+ address = {
21
+ **(
22
+ AddressData(self.instance).data if self.instance is not None else {}
23
+ ),
24
+ **validated_data,
25
+ }
26
+ validation = gateway.Address.validate(address)
27
+ validated_data.update(dict(validation=utils.DP.to_dict(validation)))
28
+
29
+ return validated_data
30
+
31
+ def create(self, validated_data: dict, **kwargs) -> models.Address:
32
+ return models.Address.objects.create(**validated_data)
33
+
34
+ def update(
35
+ self, instance: models.Address, validated_data: dict, **kwargs
36
+ ) -> models.Address:
37
+ changes = []
38
+ for key, val in validated_data.items():
39
+ if getattr(instance, key) != val:
40
+ changes.append(key)
41
+ setattr(instance, key, val)
42
+
43
+ instance.save(update_fields=changes)
44
+ return instance
45
+
46
+
47
+ def can_mutate_address(
48
+ address: models.Address, update: bool = False, delete: bool = False
49
+ ):
50
+ shipment = address.shipment
51
+ order = address.order
52
+
53
+ if shipment is None and order is None:
54
+ return
55
+
56
+ if update and shipment and shipment.status != ShipmentStatus.draft.value:
57
+ raise APIException(
58
+ f"Operation not permitted. The related shipment is '{shipment.status}'.",
59
+ status_code=status.HTTP_409_CONFLICT,
60
+ code="state_error",
61
+ )
62
+
63
+ if delete and shipment is not None:
64
+ raise APIException(
65
+ "This address is linked to a shipment and cannot be removed",
66
+ status_code=status.HTTP_409_CONFLICT,
67
+ code="state_error",
68
+ )
69
+
70
+ if update and order and order.status != "unfulfilled":
71
+ raise APIException(
72
+ f"Operation not permitted. The related order is '{order.status}'.",
73
+ status_code=status.HTTP_409_CONFLICT,
74
+ code="state_error",
75
+ )
76
+
77
+ if delete and order is not None:
78
+ raise APIException(
79
+ "This address is linked to an order and cannot be removed",
80
+ status_code=status.HTTP_409_CONFLICT,
81
+ code="state_error",
82
+ )
@@ -0,0 +1,51 @@
1
+ from rest_framework import status
2
+
3
+ from karrio.server.core.exceptions import APIException
4
+ from karrio.server.core.serializers import CommodityData, ShipmentStatus
5
+ from karrio.server.serializers import owned_model_serializer
6
+ import karrio.server.manager.models as models
7
+
8
+
9
+ @owned_model_serializer
10
+ class CommoditySerializer(CommodityData):
11
+ object_type = None
12
+
13
+ def create(self, validated_data: dict, **kwargs) -> models.Commodity:
14
+ return models.Commodity.objects.create(**validated_data)
15
+
16
+ def update(
17
+ self, instance: models.Commodity, validated_data: dict, **kwargs
18
+ ) -> models.Commodity:
19
+ changes = []
20
+
21
+ for key, val in validated_data.items():
22
+ if getattr(instance, key) != val:
23
+ changes.append(key)
24
+ setattr(instance, key, val)
25
+
26
+ instance.save()
27
+ return instance
28
+
29
+
30
+ def can_mutate_commodity(
31
+ commodity: models.Commodity, update: bool = False, delete: bool = False, **kwargs
32
+ ):
33
+ shipment = commodity.shipment
34
+ order = commodity.order
35
+
36
+ if shipment is None and order is None:
37
+ return
38
+
39
+ if update and shipment and shipment.status != ShipmentStatus.draft.value:
40
+ raise APIException(
41
+ f"Operation not permitted. The related shipment is '{shipment.status}'.",
42
+ status_code=status.HTTP_409_CONFLICT,
43
+ code="state_error",
44
+ )
45
+
46
+ if delete and order and len(order.line_items.all()) == 1:
47
+ raise APIException(
48
+ f"Operation not permitted. The related order needs at least one line_item.",
49
+ status_code=status.HTTP_409_CONFLICT,
50
+ code="state_error",
51
+ )
@@ -0,0 +1,84 @@
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
+ )
@@ -0,0 +1,113 @@
1
+ import rest_framework.status as status
2
+
3
+ import karrio.lib as lib
4
+ import karrio.server.serializers as serialiazers
5
+ import karrio.server.core.exceptions as exceptions
6
+ import karrio.server.core.serializers as core
7
+ import karrio.server.core.gateway as gateway
8
+ import karrio.server.manager.models as models
9
+
10
+
11
+ @serialiazers.owned_model_serializer
12
+ class DocumentUploadSerializer(core.DocumentUploadData):
13
+ def create(
14
+ self,
15
+ validated_data: dict,
16
+ context: serialiazers.Context,
17
+ **kwargs,
18
+ ) -> models.DocumentUploadRecord:
19
+ shipment = validated_data.get("shipment")
20
+ carrier = validated_data.get("carrier") or getattr(
21
+ shipment, "selected_rate_carrier", None
22
+ )
23
+ tracking_number = getattr(shipment, "tracking_number", None)
24
+ reference = validated_data.get("reference") or tracking_number
25
+
26
+ payload = core.DocumentUploadData(validated_data).data
27
+ options = ({
28
+ "origin_country_code": shipment.shipper.country_code,
29
+ "origin_postal_code": shipment.shipper.postal_code,
30
+ "destination_country_code": shipment.recipient.country_code,
31
+ "destination_postal_code": shipment.recipient.postal_code,
32
+ **(payload.get("options") or {})
33
+ } if shipment else payload.get("options"))
34
+
35
+ response = gateway.Documents.upload(
36
+ {
37
+ **payload,
38
+ "options": options,
39
+ "reference": reference,
40
+ "tracking_number": tracking_number,
41
+ },
42
+ carrier=carrier,
43
+ context=context,
44
+ )
45
+
46
+ upload_record = models.DocumentUploadRecord.objects.create(
47
+ documents=lib.to_dict(response.documents),
48
+ messages=lib.to_dict(response.messages),
49
+ options=response.options,
50
+ meta=response.meta,
51
+ shipment=shipment,
52
+ upload_carrier=carrier,
53
+ created_by=context.user,
54
+ )
55
+
56
+ return upload_record
57
+
58
+ def update(
59
+ self,
60
+ instance: models.DocumentUploadRecord,
61
+ validated_data: dict,
62
+ context: serialiazers.Context,
63
+ **kwargs,
64
+ ) -> models.DocumentUploadRecord:
65
+ changes = []
66
+
67
+ response = gateway.Documents.upload(
68
+ {
69
+ "reference": getattr(instance.shipment, "tracking_number", None),
70
+ **core.DocumentUploadData(validated_data).data,
71
+ },
72
+ carrier=instance.upload_carrier,
73
+ context=context,
74
+ )
75
+
76
+ if any(response.documents):
77
+ changes.append("documents")
78
+ instance.documents = [*instance.documents, *lib.to_dict(response.documents)]
79
+
80
+ if any(response.messages):
81
+ changes.append("messages")
82
+ instance.messages = lib.to_dict(response.messages)
83
+
84
+ instance.save(update_fields=changes)
85
+
86
+ return instance
87
+
88
+
89
+ def can_upload_shipment_document(shipment: models.Shipment):
90
+ carrier = getattr(shipment, "selected_rate_carrier", None)
91
+ capabilities = getattr(carrier, "capabilities", [])
92
+
93
+ if shipment is None:
94
+ raise exceptions.APIException(
95
+ detail=f"No purchased shipment found for trade document upload.",
96
+ status_code=status.HTTP_400_BAD_REQUEST,
97
+ )
98
+
99
+ if shipment.status not in [
100
+ core.ShipmentStatus.shipped.value,
101
+ core.ShipmentStatus.purchased.value,
102
+ core.ShipmentStatus.in_transit.value,
103
+ ]:
104
+ raise exceptions.APIException(
105
+ detail=f"The trade document upload is not enabled for shipment status: '{shipment.status}'.",
106
+ status_code=status.HTTP_409_CONFLICT,
107
+ )
108
+
109
+ if "paperless" not in capabilities:
110
+ raise exceptions.APIException(
111
+ detail=f"trade document upload is not supported by carrier: '{carrier.carrier_id}'",
112
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
113
+ )
@@ -0,0 +1,85 @@
1
+ import typing
2
+
3
+ import karrio.server.core.gateway as gateway
4
+ import karrio.server.manager.models as models
5
+ import karrio.server.core.serializers as core
6
+ import karrio.server.serializers as serializers
7
+ import karrio.server.manager.serializers as manager
8
+
9
+ DEFAULT_CARRIER_FILTER: typing.Any = dict(active=True, capability="manifest")
10
+
11
+
12
+ @serializers.owned_model_serializer
13
+ class ManifestSerializer(core.ManifestData):
14
+ def create(
15
+ self, validated_data: dict, context: serializers.Context, **kwargs
16
+ ) -> models.Manifest:
17
+ data = validated_data.copy()
18
+ shipment_ids = list(set(data.pop("shipment_ids")))
19
+ carrier_name = data["carrier_name"]
20
+ carrier = gateway.Carriers.first(
21
+ context=context,
22
+ carrier_name=carrier_name,
23
+ **{"raise_not_found": True, **DEFAULT_CARRIER_FILTER},
24
+ )
25
+
26
+ shipments = models.Shipment.access_by(context).filter(
27
+ id__in=shipment_ids,
28
+ manifest__isnull=True,
29
+ selected_rate_carrier__carrier_code=carrier_name,
30
+ )
31
+ shipment_identifiers = [_.shipment_identifier for _ in shipments]
32
+
33
+ if (
34
+ len(shipment_identifiers) > len(shipment_ids)
35
+ or len(shipment_identifiers) == 0
36
+ ):
37
+ raise serializers.ValidationError(
38
+ {
39
+ "shipment_ids": (
40
+ "One or more shipment ids are invalid or not found. "
41
+ "Please make sure that the shipments referenced exist and have been purchased with the same carrier."
42
+ )
43
+ }
44
+ )
45
+
46
+ response = gateway.Manifests.create(
47
+ payload=core.ManifestRequest.map(
48
+ data={
49
+ **data,
50
+ "shipment_identifiers": shipment_identifiers,
51
+ "options": {
52
+ **data.get("options", {}),
53
+ "shipments": core.Shipment(shipments, many=True).data,
54
+ },
55
+ }
56
+ ).data,
57
+ carrier=carrier,
58
+ )
59
+
60
+ payload = {
61
+ key: value
62
+ for key, value in core.Manifest(response.manifest).data.items()
63
+ if key in models.Manifest.DIRECT_PROPS
64
+ }
65
+ address = serializers.save_one_to_one_data(
66
+ "address",
67
+ manager.AddressSerializer,
68
+ payload=validated_data,
69
+ context=context,
70
+ )
71
+
72
+ manifest = models.Manifest.objects.create(
73
+ **{
74
+ **payload,
75
+ "address": address,
76
+ "created_by": context.user,
77
+ "manifest_carrier": carrier,
78
+ "options": data.get("options", {}),
79
+ "test_mode": response.manifest.test_mode,
80
+ "manifest": response.manifest.doc.manifest,
81
+ }
82
+ )
83
+ manifest.shipments.set(shipments)
84
+
85
+ return manifest
@@ -0,0 +1,84 @@
1
+ from rest_framework import status
2
+
3
+ from karrio.server.core.exceptions import APIException
4
+ from karrio.server.core.serializers import ParcelData, ShipmentStatus
5
+ from karrio.server.serializers import (
6
+ owned_model_serializer,
7
+ save_many_to_many_data,
8
+ process_dictionaries_mutations,
9
+ )
10
+
11
+ from karrio.server.manager.serializers.commodity import CommoditySerializer
12
+ import karrio.server.manager.models as models
13
+
14
+
15
+ @owned_model_serializer
16
+ class ParcelSerializer(ParcelData):
17
+ object_type = None
18
+
19
+ def __init__(self, instance: models.Address = None, **kwargs):
20
+ data = kwargs.get("data") or {}
21
+
22
+ if ("items" in data) and (instance is not None):
23
+ context = getattr(self, "__context", None) or kwargs.get("context")
24
+ save_many_to_many_data(
25
+ "items",
26
+ CommoditySerializer,
27
+ instance,
28
+ payload=data,
29
+ context=context,
30
+ )
31
+
32
+ super().__init__(instance, **kwargs)
33
+
34
+ def create(self, validated_data: dict, context: dict, **kwargs) -> models.Parcel:
35
+ instance = models.Parcel.objects.create(
36
+ **{key: value for key, value in validated_data.items() if key != "items"}
37
+ )
38
+
39
+ save_many_to_many_data(
40
+ "items",
41
+ CommoditySerializer,
42
+ instance,
43
+ payload=validated_data,
44
+ context=context,
45
+ )
46
+
47
+ return instance
48
+
49
+ def update(
50
+ self, instance: models.Parcel, validated_data: dict, **kwargs
51
+ ) -> models.Parcel:
52
+ data = process_dictionaries_mutations(["options"], validated_data, instance)
53
+ changes = []
54
+
55
+ for key, val in data.items():
56
+ if getattr(instance, key) != val and key != "items":
57
+ changes.append(key)
58
+ setattr(instance, key, val)
59
+
60
+ instance.save(update_fields=changes)
61
+ return instance
62
+
63
+
64
+ def can_mutate_parcel(
65
+ parcel: models.Parcel, update: bool = False, delete: bool = False, **kwargs
66
+ ):
67
+ shipment = parcel.shipment
68
+
69
+ if shipment is None:
70
+ return
71
+
72
+ if update and shipment.status != ShipmentStatus.draft.value:
73
+ raise APIException(
74
+ f"Operation not permitted. The related shipment is '{shipment.status}'.",
75
+ status_code=status.HTTP_409_CONFLICT,
76
+ code="state_error",
77
+ )
78
+
79
+ if delete and len(shipment.parcels.all()) == 1:
80
+ raise APIException(
81
+ f"Operation not permitted. The related shipment needs at least one parcel.",
82
+ status_code=status.HTTP_409_CONFLICT,
83
+ code="state_error",
84
+ )