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.
- karrio/server/manager/migrations/0070_add_meta_and_product_fields.py +98 -0
- karrio/server/manager/migrations/0071_product_proxy.py +25 -0
- karrio/server/manager/migrations/0072_populate_json_fields.py +267 -0
- karrio/server/manager/migrations/0073_make_shipment_fk_nullable.py +36 -0
- karrio/server/manager/migrations/0074_clean_model_refactoring.py +207 -0
- karrio/server/manager/migrations/0075_populate_template_meta.py +69 -0
- karrio/server/manager/migrations/0076_remove_customs_model.py +66 -0
- karrio/server/manager/migrations/0077_add_carrier_snapshot_fields.py +83 -0
- karrio/server/manager/migrations/0078_populate_carrier_snapshots.py +112 -0
- karrio/server/manager/migrations/0079_remove_carrier_fk_fields.py +56 -0
- karrio/server/manager/migrations/0080_add_carrier_json_indexes.py +137 -0
- karrio/server/manager/migrations/0081_cleanup.py +62 -0
- karrio/server/manager/migrations/0082_shipment_fees.py +26 -0
- karrio/server/manager/models.py +421 -321
- karrio/server/manager/serializers/__init__.py +5 -4
- karrio/server/manager/serializers/address.py +8 -2
- karrio/server/manager/serializers/commodity.py +11 -4
- karrio/server/manager/serializers/document.py +29 -15
- karrio/server/manager/serializers/manifest.py +6 -3
- karrio/server/manager/serializers/parcel.py +5 -2
- karrio/server/manager/serializers/pickup.py +194 -67
- karrio/server/manager/serializers/shipment.py +226 -171
- karrio/server/manager/serializers/tracking.py +45 -12
- karrio/server/manager/tests/__init__.py +0 -1
- karrio/server/manager/tests/test_addresses.py +53 -0
- karrio/server/manager/tests/test_parcels.py +50 -0
- karrio/server/manager/tests/test_pickups.py +286 -50
- karrio/server/manager/tests/test_products.py +597 -0
- karrio/server/manager/tests/test_shipments.py +237 -92
- karrio/server/manager/tests/test_trackers.py +4 -3
- karrio/server/manager/views/__init__.py +1 -1
- karrio/server/manager/views/addresses.py +38 -2
- karrio/server/manager/views/documents.py +1 -1
- karrio/server/manager/views/parcels.py +25 -2
- karrio/server/manager/views/pickups.py +6 -6
- karrio/server/manager/views/products.py +239 -0
- karrio/server/manager/views/trackers.py +69 -1
- {karrio_server_manager-2026.1.1.dist-info → karrio_server_manager-2026.1.4.dist-info}/METADATA +1 -1
- {karrio_server_manager-2026.1.1.dist-info → karrio_server_manager-2026.1.4.dist-info}/RECORD +41 -29
- {karrio_server_manager-2026.1.1.dist-info → karrio_server_manager-2026.1.4.dist-info}/WHEEL +1 -1
- karrio/server/manager/serializers/customs.py +0 -84
- karrio/server/manager/tests/test_custom_infos.py +0 -101
- karrio/server/manager/views/customs.py +0 -159
- {karrio_server_manager-2026.1.1.dist-info → karrio_server_manager-2026.1.4.dist-info}/top_level.txt +0 -0
|
@@ -10,7 +10,7 @@ import karrio.core.units as units
|
|
|
10
10
|
import karrio.server.conf as conf
|
|
11
11
|
import karrio.server.core.utils as utils
|
|
12
12
|
import karrio.server.core.gateway as gateway
|
|
13
|
-
|
|
13
|
+
from karrio.server.core.utils import create_carrier_snapshot, resolve_carrier
|
|
14
14
|
import karrio.server.core.dataunits as dataunits
|
|
15
15
|
import karrio.server.core.datatypes as datatypes
|
|
16
16
|
import karrio.server.core.exceptions as exceptions
|
|
@@ -23,12 +23,13 @@ from karrio.server.serializers import (
|
|
|
23
23
|
ChoiceField,
|
|
24
24
|
BooleanField,
|
|
25
25
|
owned_model_serializer,
|
|
26
|
-
save_one_to_one_data,
|
|
27
|
-
save_many_to_many_data,
|
|
28
26
|
link_org,
|
|
29
27
|
Context,
|
|
30
28
|
PlainDictField,
|
|
31
29
|
StringListField,
|
|
30
|
+
process_json_object_mutation,
|
|
31
|
+
process_json_array_mutation,
|
|
32
|
+
process_customs_mutation,
|
|
32
33
|
)
|
|
33
34
|
from karrio.server.core.serializers import (
|
|
34
35
|
SHIPMENT_STATUS,
|
|
@@ -44,11 +45,9 @@ from karrio.server.core.serializers import (
|
|
|
44
45
|
Payment,
|
|
45
46
|
Message,
|
|
46
47
|
Rate,
|
|
48
|
+
Parcel,
|
|
47
49
|
)
|
|
48
50
|
from karrio.server.manager.serializers.document import DocumentUploadSerializer
|
|
49
|
-
from karrio.server.manager.serializers.address import AddressSerializer
|
|
50
|
-
from karrio.server.manager.serializers.customs import CustomsSerializer
|
|
51
|
-
from karrio.server.manager.serializers.parcel import ParcelSerializer
|
|
52
51
|
from karrio.server.manager.serializers.rate import RateSerializer
|
|
53
52
|
import karrio.server.manager.models as models
|
|
54
53
|
|
|
@@ -69,64 +68,8 @@ class ShipmentSerializer(ShipmentData):
|
|
|
69
68
|
docs = Documents(required=False)
|
|
70
69
|
meta = PlainDictField(required=False, allow_null=True)
|
|
71
70
|
messages = Message(many=True, required=False, default=[])
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
data = kwargs.get("data") or {}
|
|
75
|
-
context = getattr(self, "__context", None) or kwargs.get("context")
|
|
76
|
-
is_update = instance is not None
|
|
77
|
-
|
|
78
|
-
if is_update and ("parcels" in data):
|
|
79
|
-
save_many_to_many_data(
|
|
80
|
-
"parcels",
|
|
81
|
-
ParcelSerializer,
|
|
82
|
-
instance,
|
|
83
|
-
payload=data,
|
|
84
|
-
context=context,
|
|
85
|
-
partial=True,
|
|
86
|
-
)
|
|
87
|
-
if is_update and ("customs" in data):
|
|
88
|
-
instance.customs = save_one_to_one_data(
|
|
89
|
-
"customs",
|
|
90
|
-
CustomsSerializer,
|
|
91
|
-
instance,
|
|
92
|
-
payload=data,
|
|
93
|
-
context=context,
|
|
94
|
-
partial=instance.customs is not None,
|
|
95
|
-
)
|
|
96
|
-
if is_update and ("recipient" in data):
|
|
97
|
-
instance.recipient = save_one_to_one_data(
|
|
98
|
-
"recipient",
|
|
99
|
-
AddressSerializer,
|
|
100
|
-
instance,
|
|
101
|
-
payload=data,
|
|
102
|
-
context=context,
|
|
103
|
-
)
|
|
104
|
-
if is_update and ("return_address" in data):
|
|
105
|
-
instance.return_address = save_one_to_one_data(
|
|
106
|
-
"return_address",
|
|
107
|
-
AddressSerializer,
|
|
108
|
-
instance,
|
|
109
|
-
payload=data,
|
|
110
|
-
context=context,
|
|
111
|
-
)
|
|
112
|
-
if is_update and ("billing_address" in data):
|
|
113
|
-
instance.billing_address = save_one_to_one_data(
|
|
114
|
-
"billing_address",
|
|
115
|
-
AddressSerializer,
|
|
116
|
-
instance,
|
|
117
|
-
payload=data,
|
|
118
|
-
context=context,
|
|
119
|
-
)
|
|
120
|
-
if is_update and ("shipper" in data):
|
|
121
|
-
instance.shipper = save_one_to_one_data(
|
|
122
|
-
"shipper",
|
|
123
|
-
AddressSerializer,
|
|
124
|
-
instance,
|
|
125
|
-
payload=data,
|
|
126
|
-
context=context,
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
super().__init__(instance, **kwargs)
|
|
71
|
+
# Override parcels to use Parcel (with id) instead of ParcelData (without id)
|
|
72
|
+
parcels = Parcel(many=True, allow_empty=False, help_text="The shipment's parcels")
|
|
130
73
|
|
|
131
74
|
@transaction.atomic
|
|
132
75
|
def create(
|
|
@@ -155,7 +98,7 @@ class ShipmentSerializer(ShipmentData):
|
|
|
155
98
|
)
|
|
156
99
|
)
|
|
157
100
|
|
|
158
|
-
carriers = gateway.
|
|
101
|
+
carriers = gateway.Connections.list(
|
|
159
102
|
context=context,
|
|
160
103
|
carrier_ids=carrier_ids,
|
|
161
104
|
**({"carrier_name": resolved_carrier_name} if resolved_carrier_name else {}),
|
|
@@ -196,6 +139,46 @@ class ShipmentSerializer(ShipmentData):
|
|
|
196
139
|
context=context,
|
|
197
140
|
)
|
|
198
141
|
|
|
142
|
+
# Process JSON fields for addresses, parcels, and customs
|
|
143
|
+
json_fields = {}
|
|
144
|
+
|
|
145
|
+
if "shipper" in validated_data:
|
|
146
|
+
json_fields.update(shipper=process_json_object_mutation(
|
|
147
|
+
"shipper", validated_data, None,
|
|
148
|
+
model_class=models.Address, object_type="address", id_prefix="adr",
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
if "recipient" in validated_data:
|
|
152
|
+
json_fields.update(recipient=process_json_object_mutation(
|
|
153
|
+
"recipient", validated_data, None,
|
|
154
|
+
model_class=models.Address, object_type="address", id_prefix="adr",
|
|
155
|
+
))
|
|
156
|
+
|
|
157
|
+
if "return_address" in validated_data:
|
|
158
|
+
json_fields.update(return_address=process_json_object_mutation(
|
|
159
|
+
"return_address", validated_data, None,
|
|
160
|
+
model_class=models.Address, object_type="address", id_prefix="adr",
|
|
161
|
+
))
|
|
162
|
+
|
|
163
|
+
if "billing_address" in validated_data:
|
|
164
|
+
json_fields.update(billing_address=process_json_object_mutation(
|
|
165
|
+
"billing_address", validated_data, None,
|
|
166
|
+
model_class=models.Address, object_type="address", id_prefix="adr",
|
|
167
|
+
))
|
|
168
|
+
|
|
169
|
+
json_fields.update(parcels=process_json_array_mutation(
|
|
170
|
+
"parcels", validated_data, None,
|
|
171
|
+
id_prefix="pcl", model_class=models.Parcel,
|
|
172
|
+
nested_arrays={"items": ("itm", models.Commodity)},
|
|
173
|
+
object_type="parcel", data_field_name="parcels",
|
|
174
|
+
))
|
|
175
|
+
|
|
176
|
+
if "customs" in validated_data:
|
|
177
|
+
json_fields.update(customs=process_customs_mutation(
|
|
178
|
+
validated_data, None,
|
|
179
|
+
address_model=models.Address, product_model=models.Commodity,
|
|
180
|
+
))
|
|
181
|
+
|
|
199
182
|
shipment = models.Shipment.objects.create(
|
|
200
183
|
**{
|
|
201
184
|
**{
|
|
@@ -203,36 +186,7 @@ class ShipmentSerializer(ShipmentData):
|
|
|
203
186
|
for key, value in validated_data.items()
|
|
204
187
|
if key in models.Shipment.DIRECT_PROPS and value is not None
|
|
205
188
|
},
|
|
206
|
-
|
|
207
|
-
"customs",
|
|
208
|
-
CustomsSerializer,
|
|
209
|
-
payload=validated_data,
|
|
210
|
-
context=context,
|
|
211
|
-
),
|
|
212
|
-
"shipper": save_one_to_one_data(
|
|
213
|
-
"shipper",
|
|
214
|
-
AddressSerializer,
|
|
215
|
-
payload=validated_data,
|
|
216
|
-
context=context,
|
|
217
|
-
),
|
|
218
|
-
"recipient": save_one_to_one_data(
|
|
219
|
-
"recipient",
|
|
220
|
-
AddressSerializer,
|
|
221
|
-
payload=validated_data,
|
|
222
|
-
context=context,
|
|
223
|
-
),
|
|
224
|
-
"return_address": save_one_to_one_data(
|
|
225
|
-
"return_address",
|
|
226
|
-
AddressSerializer,
|
|
227
|
-
payload=validated_data,
|
|
228
|
-
context=context,
|
|
229
|
-
),
|
|
230
|
-
"billing_address": save_one_to_one_data(
|
|
231
|
-
"billing_address",
|
|
232
|
-
AddressSerializer,
|
|
233
|
-
payload=validated_data,
|
|
234
|
-
context=context,
|
|
235
|
-
),
|
|
189
|
+
**json_fields,
|
|
236
190
|
"rates": rates,
|
|
237
191
|
"payment": payment,
|
|
238
192
|
"services": services,
|
|
@@ -241,18 +195,12 @@ class ShipmentSerializer(ShipmentData):
|
|
|
241
195
|
}
|
|
242
196
|
)
|
|
243
197
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
save_many_to_many_data(
|
|
247
|
-
"parcels",
|
|
248
|
-
ParcelSerializer,
|
|
249
|
-
shipment,
|
|
250
|
-
payload=validated_data,
|
|
251
|
-
context=context,
|
|
252
|
-
)
|
|
198
|
+
# carriers M2M removed - carrier info now in selected_rate JSON
|
|
253
199
|
|
|
254
200
|
# Buy label if preferred service is selected, shipping method applied, shipping rules applied, or skip rate fetching
|
|
255
201
|
if (service and fetch_rates) or apply_shipping_method_flag or apply_shipping_rules or skip_rate_fetching:
|
|
202
|
+
from karrio.server.tracing.utils import set_tracing_context
|
|
203
|
+
set_tracing_context(object_id=shipment.id)
|
|
256
204
|
return buy_shipment_label(
|
|
257
205
|
shipment,
|
|
258
206
|
context=context,
|
|
@@ -275,13 +223,73 @@ class ShipmentSerializer(ShipmentData):
|
|
|
275
223
|
changes.append(key)
|
|
276
224
|
validated_data.pop(key)
|
|
277
225
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
226
|
+
# Note: RELATIONAL_PROPS handling removed - FK relationships converted to JSONFields
|
|
227
|
+
|
|
228
|
+
if "shipper" in data:
|
|
229
|
+
instance.shipper = process_json_object_mutation(
|
|
230
|
+
"shipper",
|
|
231
|
+
data,
|
|
232
|
+
instance,
|
|
233
|
+
model_class=models.Address,
|
|
234
|
+
object_type="address",
|
|
235
|
+
id_prefix="adr",
|
|
236
|
+
)
|
|
237
|
+
changes.append("shipper")
|
|
238
|
+
|
|
239
|
+
if "recipient" in data:
|
|
240
|
+
instance.recipient = process_json_object_mutation(
|
|
241
|
+
"recipient",
|
|
242
|
+
data,
|
|
243
|
+
instance,
|
|
244
|
+
model_class=models.Address,
|
|
245
|
+
object_type="address",
|
|
246
|
+
id_prefix="adr",
|
|
247
|
+
)
|
|
248
|
+
changes.append("recipient")
|
|
249
|
+
|
|
250
|
+
if "return_address" in data:
|
|
251
|
+
instance.return_address = process_json_object_mutation(
|
|
252
|
+
"return_address",
|
|
253
|
+
data,
|
|
254
|
+
instance,
|
|
255
|
+
model_class=models.Address,
|
|
256
|
+
object_type="address",
|
|
257
|
+
id_prefix="adr",
|
|
258
|
+
)
|
|
259
|
+
changes.append("return_address")
|
|
260
|
+
|
|
261
|
+
if "billing_address" in data:
|
|
262
|
+
instance.billing_address = process_json_object_mutation(
|
|
263
|
+
"billing_address",
|
|
264
|
+
data,
|
|
265
|
+
instance,
|
|
266
|
+
model_class=models.Address,
|
|
267
|
+
object_type="address",
|
|
268
|
+
id_prefix="adr",
|
|
269
|
+
)
|
|
270
|
+
changes.append("billing_address")
|
|
271
|
+
|
|
272
|
+
if "parcels" in data:
|
|
273
|
+
instance.parcels = process_json_array_mutation(
|
|
274
|
+
"parcels",
|
|
275
|
+
data,
|
|
276
|
+
instance,
|
|
277
|
+
id_prefix="pcl",
|
|
278
|
+
model_class=models.Parcel,
|
|
279
|
+
nested_arrays={"items": ("itm", models.Commodity)},
|
|
280
|
+
object_type="parcel",
|
|
281
|
+
data_field_name="parcels",
|
|
282
|
+
)
|
|
283
|
+
changes.append("parcels")
|
|
284
|
+
|
|
285
|
+
if "customs" in data:
|
|
286
|
+
instance.customs = process_customs_mutation(
|
|
287
|
+
data,
|
|
288
|
+
instance,
|
|
289
|
+
address_model=models.Address,
|
|
290
|
+
product_model=models.Commodity,
|
|
291
|
+
)
|
|
292
|
+
changes.append("customs")
|
|
285
293
|
|
|
286
294
|
if "docs" in validated_data:
|
|
287
295
|
changes.append("label")
|
|
@@ -297,30 +305,24 @@ class ShipmentSerializer(ShipmentData):
|
|
|
297
305
|
|
|
298
306
|
if "selected_rate" in validated_data:
|
|
299
307
|
selected_rate = validated_data.get("selected_rate", {})
|
|
300
|
-
carrier
|
|
308
|
+
# Try to find carrier for connection metadata
|
|
309
|
+
carrier = providers.CarrierConnection.objects.filter(
|
|
301
310
|
carrier_id=selected_rate.get("carrier_id")
|
|
302
311
|
).first()
|
|
303
312
|
instance.test_mode = selected_rate.get("test_mode", instance.test_mode)
|
|
304
313
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
else {}
|
|
313
|
-
),
|
|
314
|
-
},
|
|
315
|
-
}
|
|
316
|
-
instance.selected_rate_carrier = carrier
|
|
317
|
-
changes += ["selected_rate", "selected_rate_carrier"]
|
|
314
|
+
# Store carrier snapshot in dedicated field (consistent with Tracking, Pickup, etc.)
|
|
315
|
+
if carrier:
|
|
316
|
+
instance.carrier = create_carrier_snapshot(carrier)
|
|
317
|
+
changes += ["carrier"]
|
|
318
|
+
|
|
319
|
+
instance.selected_rate = selected_rate
|
|
320
|
+
changes += ["selected_rate"]
|
|
318
321
|
|
|
319
322
|
if any(changes):
|
|
320
323
|
instance.save(update_fields=changes)
|
|
321
324
|
|
|
322
|
-
|
|
323
|
-
instance.carriers.set(carriers)
|
|
325
|
+
# carriers M2M removed - carrier info now in selected_rate JSON
|
|
324
326
|
|
|
325
327
|
return instance
|
|
326
328
|
|
|
@@ -568,9 +570,11 @@ class ShipmentPurchaseSerializer(Shipment):
|
|
|
568
570
|
|
|
569
571
|
class ShipmentCancelSerializer(Shipment):
|
|
570
572
|
def update(
|
|
571
|
-
self, instance: models.Shipment, validated_data: dict, **kwargs
|
|
573
|
+
self, instance: models.Shipment, validated_data: dict, context=None, **kwargs
|
|
572
574
|
) -> datatypes.ConfirmationResponse:
|
|
573
575
|
if instance.status == ShipmentStatus.purchased.value:
|
|
576
|
+
# Resolve carrier from carrier snapshot
|
|
577
|
+
carrier = resolve_carrier(instance.carrier or {}, context)
|
|
574
578
|
gateway.Shipments.cancel(
|
|
575
579
|
payload={
|
|
576
580
|
**ShipmentCancelRequest(instance).data,
|
|
@@ -580,7 +584,7 @@ class ShipmentCancelSerializer(Shipment):
|
|
|
580
584
|
**(validated_data.get("options") or {}),
|
|
581
585
|
},
|
|
582
586
|
},
|
|
583
|
-
carrier=
|
|
587
|
+
carrier=carrier,
|
|
584
588
|
)
|
|
585
589
|
|
|
586
590
|
instance.status = ShipmentStatus.cancelled.value
|
|
@@ -605,9 +609,10 @@ def fetch_shipment_rates(
|
|
|
605
609
|
context: typing.Any,
|
|
606
610
|
data: dict = dict(),
|
|
607
611
|
) -> models.Shipment:
|
|
608
|
-
carrier_ids
|
|
612
|
+
# carrier_ids can be passed in data, or default to empty list (query all carriers)
|
|
613
|
+
carrier_ids = data.get("carrier_ids", [])
|
|
609
614
|
|
|
610
|
-
carriers = gateway.
|
|
615
|
+
carriers = gateway.Connections.list(
|
|
611
616
|
active=True,
|
|
612
617
|
capability="shipping",
|
|
613
618
|
context=context,
|
|
@@ -653,7 +658,7 @@ def buy_shipment_label(
|
|
|
653
658
|
invoice_template = shipment.options.get("invoice_template")
|
|
654
659
|
|
|
655
660
|
payload = {**data, "selected_rate_id": selected_rate.get("id")}
|
|
656
|
-
carrier = gateway.
|
|
661
|
+
carrier = gateway.Connections.first(
|
|
657
662
|
carrier_id=selected_rate.get("carrier_id"),
|
|
658
663
|
test_mode=selected_rate.get("test_mode"),
|
|
659
664
|
context=context,
|
|
@@ -667,8 +672,9 @@ def buy_shipment_label(
|
|
|
667
672
|
|
|
668
673
|
# Generate invoice in advance if is_paperless_trade
|
|
669
674
|
if pre_purchase_generation:
|
|
675
|
+
# Set carrier snapshot on shipment (consistent with other models)
|
|
676
|
+
shipment.carrier = create_carrier_snapshot(carrier)
|
|
670
677
|
shipment.selected_rate = selected_rate
|
|
671
|
-
shipment.selected_rate_carrier = carrier
|
|
672
678
|
document = generate_custom_invoice(invoice_template, shipment)
|
|
673
679
|
invoice = dict(invoice=document["doc_file"])
|
|
674
680
|
|
|
@@ -695,11 +701,22 @@ def buy_shipment_label(
|
|
|
695
701
|
.instance
|
|
696
702
|
)
|
|
697
703
|
|
|
704
|
+
# Merge response parcel data with existing parcel data to preserve all fields (weight, etc.)
|
|
705
|
+
existing_parcels = shipment.parcels or []
|
|
706
|
+
merged_parcels = []
|
|
707
|
+
for idx, response_parcel in enumerate(response.parcels):
|
|
708
|
+
existing_parcel = existing_parcels[idx] if idx < len(existing_parcels) else {}
|
|
709
|
+
merged_parcels.append(
|
|
710
|
+
{
|
|
711
|
+
**existing_parcel, # Keep existing data (weight, weight_unit, etc.)
|
|
712
|
+
"id": response_parcel.id or existing_parcel.get("id"),
|
|
713
|
+
"reference_number": response_parcel.reference_number
|
|
714
|
+
or existing_parcel.get("reference_number"),
|
|
715
|
+
}
|
|
716
|
+
)
|
|
717
|
+
|
|
698
718
|
extra.update(
|
|
699
|
-
parcels=
|
|
700
|
-
dict(id=parcel.id, reference_number=parcel.reference_number)
|
|
701
|
-
for parcel in response.parcels
|
|
702
|
-
],
|
|
719
|
+
parcels=merged_parcels,
|
|
703
720
|
docs={**lib.to_dict(response.docs), **invoice},
|
|
704
721
|
)
|
|
705
722
|
|
|
@@ -715,6 +732,13 @@ def buy_shipment_label(
|
|
|
715
732
|
),
|
|
716
733
|
}
|
|
717
734
|
|
|
735
|
+
# Set selected_rate with carrier snapshot directly on shipment before update
|
|
736
|
+
# (This is more reliable than depending on serializer validation)
|
|
737
|
+
# Set carrier snapshot on shipment (consistent with other models)
|
|
738
|
+
shipment.carrier = create_carrier_snapshot(carrier)
|
|
739
|
+
shipment.selected_rate = selected_rate
|
|
740
|
+
shipment.save(update_fields=["carrier", "selected_rate"])
|
|
741
|
+
|
|
718
742
|
purchased_shipment = lib.identity(
|
|
719
743
|
ShipmentSerializer.map(
|
|
720
744
|
shipment,
|
|
@@ -809,6 +833,44 @@ def can_mutate_shipment(
|
|
|
809
833
|
)
|
|
810
834
|
|
|
811
835
|
|
|
836
|
+
def compute_estimated_delivery(
|
|
837
|
+
selected_rate: typing.Optional[dict],
|
|
838
|
+
options: typing.Optional[dict],
|
|
839
|
+
) -> typing.Tuple[typing.Optional[str], typing.Optional[str]]:
|
|
840
|
+
"""Compute estimated delivery date from rate and shipment options.
|
|
841
|
+
|
|
842
|
+
This function extracts the estimated delivery date from the selected rate,
|
|
843
|
+
or computes it from transit days and shipping date if not directly available.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
selected_rate: The selected shipping rate dictionary
|
|
847
|
+
options: The shipment options dictionary
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
A tuple of (estimated_delivery, shipping_date_str) where:
|
|
851
|
+
- estimated_delivery: The estimated delivery date string (YYYY-MM-DD format) or None
|
|
852
|
+
- shipping_date_str: The shipping date string from options or None
|
|
853
|
+
"""
|
|
854
|
+
_rate = selected_rate or {}
|
|
855
|
+
_options = options or {}
|
|
856
|
+
|
|
857
|
+
shipping_date_str = _options.get("shipping_date") or _options.get("shipment_date")
|
|
858
|
+
estimated_delivery = _rate.get("estimated_delivery")
|
|
859
|
+
transit_days = _rate.get("transit_days")
|
|
860
|
+
|
|
861
|
+
if not estimated_delivery and transit_days and shipping_date_str:
|
|
862
|
+
shipping_date = lib.to_date(
|
|
863
|
+
shipping_date_str,
|
|
864
|
+
current_format="%Y-%m-%dT%H:%M",
|
|
865
|
+
try_formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M", "%Y-%m-%dT%H:%M:%S"],
|
|
866
|
+
)
|
|
867
|
+
if shipping_date:
|
|
868
|
+
estimated_date = shipping_date + datetime.timedelta(days=int(transit_days))
|
|
869
|
+
estimated_delivery = lib.fdate(estimated_date)
|
|
870
|
+
|
|
871
|
+
return estimated_delivery, shipping_date_str
|
|
872
|
+
|
|
873
|
+
|
|
812
874
|
def remove_shipment_tracker(shipment: models.Shipment):
|
|
813
875
|
if hasattr(shipment, "shipment_tracker"):
|
|
814
876
|
shipment.shipment_tracker.delete()
|
|
@@ -816,21 +878,27 @@ def remove_shipment_tracker(shipment: models.Shipment):
|
|
|
816
878
|
|
|
817
879
|
def create_shipment_tracker(shipment: typing.Optional[models.Shipment], context):
|
|
818
880
|
rate_provider = (shipment.meta or {}).get("rate_provider") or shipment.carrier_name
|
|
819
|
-
carrier
|
|
881
|
+
# Resolve carrier from carrier snapshot
|
|
882
|
+
carrier_snapshot = shipment.carrier or {}
|
|
883
|
+
carrier = resolve_carrier(carrier_snapshot, context)
|
|
820
884
|
|
|
821
885
|
# Get rate provider carrier if supported instead of carrier account
|
|
822
886
|
if (
|
|
823
887
|
rate_provider != shipment.carrier_name
|
|
824
888
|
) and rate_provider in dataunits.CARRIER_NAMES:
|
|
825
889
|
carrier = (
|
|
826
|
-
providers.
|
|
890
|
+
providers.CarrierConnection.access_by(context)
|
|
827
891
|
.filter(carrier_code=rate_provider)
|
|
828
892
|
.first()
|
|
829
893
|
)
|
|
830
894
|
|
|
831
|
-
# Handle hub extension tracking
|
|
832
|
-
if
|
|
833
|
-
|
|
895
|
+
# Handle hub extension tracking - resolve from snapshot if carrier is None
|
|
896
|
+
if carrier and carrier.gateway.is_hub:
|
|
897
|
+
# Keep the hub carrier
|
|
898
|
+
pass
|
|
899
|
+
elif carrier is None and carrier_snapshot:
|
|
900
|
+
# Try to resolve again if carrier is None
|
|
901
|
+
carrier = resolve_carrier(carrier_snapshot, context)
|
|
834
902
|
|
|
835
903
|
# Get dhl universal account if a dhl integration doesn't support tracking API
|
|
836
904
|
if (
|
|
@@ -838,7 +906,7 @@ def create_shipment_tracker(shipment: typing.Optional[models.Shipment], context)
|
|
|
838
906
|
and "dhl" in carrier.carrier_name
|
|
839
907
|
and "get_tracking" not in carrier.gateway.proxy_methods
|
|
840
908
|
):
|
|
841
|
-
carrier = gateway.
|
|
909
|
+
carrier = gateway.Connections.first(
|
|
842
910
|
carrier_name="dhl_universal",
|
|
843
911
|
context=context,
|
|
844
912
|
)
|
|
@@ -846,34 +914,21 @@ def create_shipment_tracker(shipment: typing.Optional[models.Shipment], context)
|
|
|
846
914
|
if carrier is not None and "get_tracking" in carrier.gateway.proxy_methods:
|
|
847
915
|
# Create shipment tracker
|
|
848
916
|
try:
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
917
|
+
# Use JSON fields for data access
|
|
918
|
+
parcels = shipment.parcels or []
|
|
919
|
+
shipper = shipment.shipper or {}
|
|
920
|
+
recipient = shipment.recipient or {}
|
|
921
|
+
|
|
922
|
+
pkg_weight = sum([p.get("weight") or 0.0 for p in parcels], 0.0)
|
|
923
|
+
estimated_delivery, shipping_date_str = compute_estimated_delivery(
|
|
924
|
+
shipment.selected_rate, shipment.options
|
|
854
925
|
)
|
|
855
926
|
|
|
856
|
-
# Get estimated_delivery from selected_rate or compute from transit_days
|
|
857
|
-
estimated_delivery = selected_rate.get("estimated_delivery")
|
|
858
|
-
transit_days = selected_rate.get("transit_days")
|
|
859
|
-
|
|
860
|
-
if not estimated_delivery and transit_days and shipping_date_str:
|
|
861
|
-
shipping_date = lib.to_date(
|
|
862
|
-
shipping_date_str,
|
|
863
|
-
current_format="%Y-%m-%dT%H:%M",
|
|
864
|
-
try_formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M", "%Y-%m-%dT%H:%M:%S"],
|
|
865
|
-
)
|
|
866
|
-
if shipping_date:
|
|
867
|
-
estimated_date = shipping_date + datetime.timedelta(
|
|
868
|
-
days=int(transit_days)
|
|
869
|
-
)
|
|
870
|
-
estimated_delivery = lib.fdate(estimated_date)
|
|
871
|
-
|
|
872
927
|
tracker = models.Tracking.objects.create(
|
|
873
928
|
tracking_number=shipment.tracking_number,
|
|
874
929
|
delivered=False,
|
|
875
930
|
shipment=shipment,
|
|
876
|
-
|
|
931
|
+
carrier=create_carrier_snapshot(carrier),
|
|
877
932
|
test_mode=carrier.test_mode,
|
|
878
933
|
created_by=shipment.created_by,
|
|
879
934
|
status=TrackerStatus.pending.value,
|
|
@@ -884,12 +939,12 @@ def create_shipment_tracker(shipment: typing.Optional[models.Shipment], context)
|
|
|
884
939
|
info=dict(
|
|
885
940
|
source="api",
|
|
886
941
|
shipment_weight=str(pkg_weight),
|
|
887
|
-
shipment_package_count=str(
|
|
888
|
-
customer_name=
|
|
889
|
-
shipment_origin_country=
|
|
890
|
-
shipment_origin_postal_code=
|
|
891
|
-
shipment_destination_country=
|
|
892
|
-
shipment_destination_postal_code=
|
|
942
|
+
shipment_package_count=str(len(parcels)),
|
|
943
|
+
customer_name=recipient.get("person_name"),
|
|
944
|
+
shipment_origin_country=shipper.get("country_code"),
|
|
945
|
+
shipment_origin_postal_code=shipper.get("postal_code"),
|
|
946
|
+
shipment_destination_country=recipient.get("country_code"),
|
|
947
|
+
shipment_destination_postal_code=recipient.get("postal_code"),
|
|
893
948
|
shipment_service=shipment.meta.get("service_name"),
|
|
894
949
|
shipping_date=shipping_date_str,
|
|
895
950
|
expected_delivery=estimated_delivery,
|