karrio-server-data 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 (43) hide show
  1. karrio/server/data/__init__.py +0 -0
  2. karrio/server/data/admin.py +1 -0
  3. karrio/server/data/apps.py +13 -0
  4. karrio/server/data/filters.py +43 -0
  5. karrio/server/data/migrations/0001_initial.py +62 -0
  6. karrio/server/data/migrations/0002_alter_batchoperation_resource_type_and_more.py +28 -0
  7. karrio/server/data/migrations/0003_datatemplate_metadata_alter_batchoperation_resources.py +36 -0
  8. karrio/server/data/migrations/__init__.py +0 -0
  9. karrio/server/data/models.py +97 -0
  10. karrio/server/data/resources/__init__.py +53 -0
  11. karrio/server/data/resources/orders.py +523 -0
  12. karrio/server/data/resources/shipments.py +473 -0
  13. karrio/server/data/resources/trackers.py +212 -0
  14. karrio/server/data/serializers/__init__.py +26 -0
  15. karrio/server/data/serializers/base.py +107 -0
  16. karrio/server/data/serializers/batch.py +9 -0
  17. karrio/server/data/serializers/batch_orders.py +99 -0
  18. karrio/server/data/serializers/batch_shipments.py +102 -0
  19. karrio/server/data/serializers/batch_trackers.py +131 -0
  20. karrio/server/data/serializers/data.py +109 -0
  21. karrio/server/data/signals.py +52 -0
  22. karrio/server/data/tests.py +3 -0
  23. karrio/server/data/urls.py +13 -0
  24. karrio/server/data/views/__init__.py +0 -0
  25. karrio/server/data/views/batch.py +72 -0
  26. karrio/server/data/views/batch_order.py +40 -0
  27. karrio/server/data/views/batch_shipment.py +40 -0
  28. karrio/server/data/views/batch_tracking.py +40 -0
  29. karrio/server/data/views/data.py +171 -0
  30. karrio/server/events/task_definitions/__init__.py +1 -0
  31. karrio/server/events/task_definitions/data/__init__.py +136 -0
  32. karrio/server/events/task_definitions/data/batch.py +130 -0
  33. karrio/server/events/task_definitions/data/shipments.py +51 -0
  34. karrio/server/graph/schemas/__init__.py +1 -0
  35. karrio/server/graph/schemas/data/__init__.py +51 -0
  36. karrio/server/graph/schemas/data/inputs.py +39 -0
  37. karrio/server/graph/schemas/data/mutations.py +53 -0
  38. karrio/server/graph/schemas/data/types.py +78 -0
  39. karrio/server/settings/data.py +15 -0
  40. karrio_server_data-2025.5rc1.dist-info/METADATA +18 -0
  41. karrio_server_data-2025.5rc1.dist-info/RECORD +43 -0
  42. karrio_server_data-2025.5rc1.dist-info/WHEEL +5 -0
  43. karrio_server_data-2025.5rc1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,473 @@
1
+ import json
2
+ from django.db.models import Q
3
+ from import_export import resources
4
+
5
+ import karrio.lib as lib
6
+ from karrio.core.units import Packages
7
+ from karrio.server.manager import models
8
+ from karrio.server.core import serializers as core
9
+ from karrio.server.core.filters import ShipmentFilters
10
+ from karrio.server.manager.serializers import ShipmentSerializer
11
+ from karrio.server.core import datatypes as types, dataunits as units
12
+
13
+ DEFAULT_HEADERS = {
14
+ "id": "ID",
15
+ "created_at": "Created at",
16
+ "tracking_number": "Tracking number",
17
+ "status": "Status",
18
+ "shipper_id": "Shipper ID",
19
+ "shipper_name": "Shipper name",
20
+ "shipper_company": "Shipper Company",
21
+ "shipper_phone": "Shipper phone",
22
+ "shipper_email": "Shipper email",
23
+ "shipper_address1": "Shipper address 1",
24
+ "shipper_address2": "Shipper address 2",
25
+ "shipper_city": "Shipper city",
26
+ "shipper_state": "Shipper state",
27
+ "shipper_postal_code": "Shipper postal code",
28
+ "shipper_country": "Shipper country",
29
+ "shipper_residential": "Shipper residential",
30
+ "recipient_id": "Recipient ID",
31
+ "recipient_name": "Recipient name",
32
+ "recipient_company": "Recipient Company",
33
+ "recipient_phone": "Recipient phone",
34
+ "recipient_email": "Recipient email",
35
+ "recipient_address1": "Recipient address 1",
36
+ "recipient_address2": "Recipient address 2",
37
+ "recipient_city": "Recipient city",
38
+ "recipient_state": "Recipient state",
39
+ "recipient_postal_code": "Recipient postal code",
40
+ "recipient_country": "Recipient country",
41
+ "recipient_residential": "Recipient residential",
42
+ "parcel_width": "Parcel width",
43
+ "parcel_height": "Parcel height",
44
+ "parcel_length": "Parcel length",
45
+ "parcel_dimension_unit": "Parcel dimension unit",
46
+ "parcel_weight": "Parcel weight",
47
+ "parcel_weight_unit": "Parcel weight unit",
48
+ "parcel_package_preset": "Parcel package preset",
49
+ "pieces": "Number of pieces",
50
+ "service": "Service",
51
+ "carrier": "Carrier",
52
+ "rate": "Rate",
53
+ "currency": "Currency",
54
+ "paid_by": "Payor",
55
+ "reference": "Reference",
56
+ "options": "Options",
57
+ }
58
+
59
+
60
+ def shipment_export_resource(query_params: dict, context, **kwargs):
61
+ queryset = models.Shipment.access_by(context)
62
+ _exclude = query_params.get("exclude", "").split(",")
63
+ _fields = (
64
+ "id",
65
+ "tracking_number",
66
+ "created_at",
67
+ "status",
68
+ "reference",
69
+ )
70
+
71
+ if "status" not in query_params:
72
+ queryset = queryset.filter(
73
+ Q(status__in=["purchased", "delivered", "shipped", "in_transit"]),
74
+ )
75
+
76
+ class Resource(resources.ModelResource):
77
+ class Meta:
78
+ model = models.Shipment
79
+ fields = _fields
80
+ exclude = _exclude
81
+ export_order = [k for k in DEFAULT_HEADERS.keys() if k not in _exclude]
82
+
83
+ def get_queryset(self):
84
+ return ShipmentFilters(query_params, queryset).qs
85
+
86
+ def get_export_headers(self):
87
+ headers = super().get_export_headers()
88
+ return [DEFAULT_HEADERS.get(k, k) for k in headers]
89
+
90
+ @staticmethod
91
+ def packages(row):
92
+ parcels = core.Parcel(row.parcels, many=True).data
93
+ return Packages([lib.to_object(types.Parcel, p) for p in parcels])
94
+
95
+ if "service" not in _exclude:
96
+ service = resources.Field()
97
+
98
+ def dehydrate_service(self, row):
99
+ rate = getattr(row, "selected_rate") or {}
100
+ return (rate.get("meta") or {}).get("service_name") or rate.get(
101
+ "service"
102
+ )
103
+
104
+ if "carrier" not in _exclude:
105
+ carrier = resources.Field()
106
+
107
+ def dehydrate_carrier(self, row):
108
+ carrier = getattr(row, "selected_rate_carrier", None)
109
+ settings = getattr(carrier, "settings", None)
110
+ return getattr(
111
+ settings, "display_name", None
112
+ ) or units.REFERENCE_MODELS["carriers"].get(carrier.carrier_name)
113
+
114
+ if "pieces" not in _exclude:
115
+ pieces = resources.Field()
116
+
117
+ def dehydrate_pieces(self, row):
118
+ return len(row.parcels.all())
119
+
120
+ if "rate" not in _exclude:
121
+ rate = resources.Field()
122
+
123
+ def dehydrate_rate(self, row):
124
+ return (getattr(row, "selected_rate") or {}).get("total_charge", None)
125
+
126
+ if "currency" not in _exclude:
127
+ currency = resources.Field()
128
+
129
+ def dehydrate_currency(self, row):
130
+ return (getattr(row, "selected_rate") or {}).get("currency", None)
131
+
132
+ if "paid_by" not in _exclude:
133
+ paid_by = resources.Field()
134
+
135
+ def dehydrate_paid_by(self, row):
136
+ return (getattr(row, "payment") or {}).get("paid_by", None)
137
+
138
+ if "parcel_weight" not in _exclude:
139
+ parcel_weight = resources.Field()
140
+
141
+ def dehydrate_parcel_weight(self, row):
142
+ return self.packages(row).weight.value
143
+
144
+ if "parcel_weight_unit" not in _exclude:
145
+ parcel_weight_unit = resources.Field()
146
+
147
+ def dehydrate_parcel_weight_unit(self, row):
148
+ return self.packages(row).weight_unit
149
+
150
+ if "parcel_length" not in _exclude:
151
+ parcel_length = resources.Field()
152
+
153
+ def dehydrate_parcel_length(self, row):
154
+ return self.packages(row)[0].parcel.length
155
+
156
+ if "parcel_width" not in _exclude:
157
+ parcel_width = resources.Field()
158
+
159
+ def dehydrate_parcel_width(self, row):
160
+ return self.packages(row)[0].parcel.width
161
+
162
+ if "parcel_height" not in _exclude:
163
+ parcel_height = resources.Field()
164
+
165
+ def dehydrate_parcel_height(self, row):
166
+ return self.packages(row)[0].parcel.height
167
+
168
+ if "parcel_dimension_unit" not in _exclude:
169
+ parcel_dimension_unit = resources.Field()
170
+
171
+ def dehydrate_parcel_dimension_unit(self, row):
172
+ _, unit = self.packages(row).compatible_units
173
+ return unit.value
174
+
175
+ if "parcel_package_preset" not in _exclude:
176
+ parcel_package_preset = resources.Field()
177
+
178
+ def dehydrate_parcel_package_preset(self, row):
179
+ return self.packages(row)[0].parcel.package_preset
180
+
181
+ if "shipment_date" not in _exclude:
182
+ shipment_date = resources.Field()
183
+
184
+ def dehydrate_shipment_date(self, row):
185
+ return (getattr(row, "options") or {}).get("shipment_date", None) or (
186
+ lib.fdate(row.created_at, "%Y-%m-%m %H:%M:%M")
187
+ )
188
+
189
+ if "shipper_id" not in _exclude:
190
+ shipper_id = resources.Field()
191
+
192
+ def dehydrate_shipper_id(self, row):
193
+ return row.shipper.id
194
+
195
+ if "shipper_name" not in _exclude:
196
+ shipper_name = resources.Field()
197
+
198
+ def dehydrate_shipper_name(self, row):
199
+ return row.shipper.person_name
200
+
201
+ if "shipper_company" not in _exclude:
202
+ shipper_company = resources.Field()
203
+
204
+ def dehydrate_shipper_company(self, row):
205
+ return row.shipper.company_name
206
+
207
+ if "shipper_phone" not in _exclude:
208
+ shipper_phone = resources.Field()
209
+
210
+ def dehydrate_shipper_phone(self, row):
211
+ return row.shipper.phone_number
212
+
213
+ if "shipper_email" not in _exclude:
214
+ shipper_email = resources.Field()
215
+
216
+ def dehydrate_shipper_email(self, row):
217
+ return row.shipper.email
218
+
219
+ if "shipper_address1" not in _exclude:
220
+ shipper_address1 = resources.Field()
221
+
222
+ def dehydrate_shipper_address1(self, row):
223
+ return row.shipper.address_line1
224
+
225
+ if "shipper_address2" not in _exclude:
226
+ shipper_address2 = resources.Field()
227
+
228
+ def dehydrate_shipper_address2(self, row):
229
+ return row.shipper.address_line2
230
+
231
+ if "shipper_city" not in _exclude:
232
+ shipper_city = resources.Field()
233
+
234
+ def dehydrate_shipper_city(self, row):
235
+ return row.shipper.city
236
+
237
+ if "shipper_state" not in _exclude:
238
+ shipper_state = resources.Field()
239
+
240
+ def dehydrate_shipper_state(self, row):
241
+ return row.shipper.state_code
242
+
243
+ if "shipper_postal_code" not in _exclude:
244
+ shipper_postal_code = resources.Field()
245
+
246
+ def dehydrate_shipper_postal_code(self, row):
247
+ return row.shipper.postal_code
248
+
249
+ if "shipper_country" not in _exclude:
250
+ shipper_country = resources.Field()
251
+
252
+ def dehydrate_shipper_country(self, row):
253
+ return row.shipper.country_code
254
+
255
+ if "shipper_residential" not in _exclude:
256
+ shipper_residential = resources.Field()
257
+
258
+ def dehydrate_shipper_residential(self, row):
259
+ return row.shipper.residential
260
+
261
+ if "recipient_id" not in _exclude:
262
+ recipient_id = resources.Field()
263
+
264
+ def dehydrate_recipient_id(self, row):
265
+ return row.recipient.id
266
+
267
+ if "recipient_name" not in _exclude:
268
+ recipient_name = resources.Field()
269
+
270
+ def dehydrate_recipient_name(self, row):
271
+ return row.recipient.person_name
272
+
273
+ if "recipient_company" not in _exclude:
274
+ recipient_company = resources.Field()
275
+
276
+ def dehydrate_recipient_company(self, row):
277
+ return row.recipient.company_name
278
+
279
+ if "recipient_phone" not in _exclude:
280
+ recipient_phone = resources.Field()
281
+
282
+ def dehydrate_recipient_phone(self, row):
283
+ return row.recipient.phone_number
284
+
285
+ if "recipient_email" not in _exclude:
286
+ recipient_email = resources.Field()
287
+
288
+ def dehydrate_recipient_email(self, row):
289
+ return row.recipient.email
290
+
291
+ if "recipient_address1" not in _exclude:
292
+ recipient_address1 = resources.Field()
293
+
294
+ def dehydrate_recipient_address1(self, row):
295
+ return row.recipient.address_line1
296
+
297
+ if "recipient_address2" not in _exclude:
298
+ recipient_address2 = resources.Field()
299
+
300
+ def dehydrate_recipient_address2(self, row):
301
+ return row.recipient.address_line2
302
+
303
+ if "recipient_city" not in _exclude:
304
+ recipient_city = resources.Field()
305
+
306
+ def dehydrate_recipient_city(self, row):
307
+ return row.recipient.city
308
+
309
+ if "recipient_state" not in _exclude:
310
+ recipient_state = resources.Field()
311
+
312
+ def dehydrate_recipient_state(self, row):
313
+ return row.recipient.state_code
314
+
315
+ if "recipient_postal_code" not in _exclude:
316
+ recipient_postal_code = resources.Field()
317
+
318
+ def dehydrate_recipient_postal_code(self, row):
319
+ return row.recipient.postal_code
320
+
321
+ if "recipient_country" not in _exclude:
322
+ recipient_country = resources.Field()
323
+
324
+ def dehydrate_recipient_country(self, row):
325
+ return row.recipient.country_code
326
+
327
+ if "recipient_residential" not in _exclude:
328
+ recipient_residential = resources.Field()
329
+
330
+ def dehydrate_recipient_residential(self, row):
331
+ return row.recipient.residential
332
+
333
+ if "options" not in _exclude:
334
+ options = resources.Field()
335
+
336
+ def dehydrate_options(self, row):
337
+ return json.loads(json.dumps(row.options))
338
+
339
+ return Resource()
340
+
341
+
342
+ def shipment_import_resource(
343
+ query_params: dict,
344
+ context,
345
+ data_fields: dict = None,
346
+ batch_id: str = None,
347
+ **kwargs
348
+ ):
349
+ queryset = models.Shipment.access_by(context)
350
+ field_headers = data_fields if data_fields is not None else DEFAULT_HEADERS
351
+ _exclude = query_params.get("exclude", "").split(",")
352
+ _fields = (
353
+ "shipper_name",
354
+ "shipper_company",
355
+ "shipper_address_line1",
356
+ "shipper_address_line2",
357
+ "shipper_city",
358
+ "shipper_state",
359
+ "shipper_postal_code",
360
+ "shipper_country",
361
+ "shipper_residential",
362
+ "recipient_name",
363
+ "recipient_company",
364
+ "recipient_address_line1",
365
+ "recipient_address_line2",
366
+ "recipient_city",
367
+ "recipient_state",
368
+ "recipient_postal_code",
369
+ "recipient_country",
370
+ "recipient_residential",
371
+ "parcel_width",
372
+ "parcel_height",
373
+ "parcel_length",
374
+ "parcel_dimension_unit",
375
+ "parcel_weight",
376
+ "parcel_weight_unit",
377
+ "parcel_package_preset",
378
+ "service",
379
+ "reference",
380
+ "options",
381
+ )
382
+
383
+ _Base = type(
384
+ "ResourceFields",
385
+ (resources.ModelResource,),
386
+ {
387
+ k: resources.Field(readonly=(k not in models.Shipment.__dict__))
388
+ for k in field_headers.keys()
389
+ if k not in _exclude
390
+ },
391
+ )
392
+
393
+ class Resource(_Base, resources.ModelResource):
394
+ class Meta:
395
+ model = models.Shipment
396
+ fields = _fields
397
+ exclude = _exclude
398
+ export_order = [k for k in field_headers.keys() if k not in _exclude]
399
+
400
+ def get_queryset(self):
401
+ return queryset
402
+
403
+ def get_export_headers(self):
404
+ headers = super().get_export_headers()
405
+ return [field_headers.get(k, k) for k in headers]
406
+
407
+ def init_instance(self, row=None):
408
+ service = row.get(field_headers["service"])
409
+ svc = {} if service is None else dict(perferred_service=service)
410
+ batch = {} if batch_id is None else dict(batch_id=batch_id)
411
+ options = {
412
+ **lib.to_dict(row.get(field_headers["options"]) or "{}"),
413
+ **svc,
414
+ }
415
+
416
+ data = lib.to_dict(
417
+ dict(
418
+ status="draft",
419
+ test_mode=context.test_mode,
420
+ created_by_id=context.user.id,
421
+ meta={**svc, **batch},
422
+ options=options,
423
+ shipper=dict(
424
+ person_name=row.get(field_headers["shipper_name"]),
425
+ company_name=row.get(field_headers["shipper_company"]),
426
+ address_line1=row.get(field_headers["shipper_address1"]),
427
+ address_line2=row.get(field_headers["shipper_address2"]),
428
+ city=row.get(field_headers["shipper_city"]),
429
+ state_code=row.get(field_headers["shipper_state"]),
430
+ postal_code=row.get(field_headers["shipper_postal_code"]),
431
+ country_code=row.get(field_headers["shipper_country"]),
432
+ residential=row.get(field_headers["shipper_residential"]),
433
+ ),
434
+ recipient=dict(
435
+ person_name=row.get(field_headers["recipient_name"]),
436
+ company_name=row.get(field_headers["recipient_company"]),
437
+ address_line1=row.get(field_headers["recipient_address1"]),
438
+ address_line2=row.get(field_headers["recipient_address2"]),
439
+ city=row.get(field_headers["recipient_city"]),
440
+ state_code=row.get(field_headers["recipient_state"]),
441
+ postal_code=row.get(field_headers["recipient_postal_code"]),
442
+ country_code=row.get(field_headers["recipient_country"]),
443
+ residential=row.get(field_headers["recipient_residential"]),
444
+ ),
445
+ parcels=[
446
+ dict(
447
+ weight=row.get(field_headers["parcel_weight"]),
448
+ weight_unit=row.get(field_headers["parcel_weight_unit"])
449
+ or "KG",
450
+ width=row.get(field_headers["parcel_width"]),
451
+ height=row.get(field_headers["parcel_height"]),
452
+ length=row.get(field_headers["parcel_length"]),
453
+ dimension_unit=row.get(
454
+ field_headers["parcel_dimension_unit"]
455
+ )
456
+ or "CM",
457
+ package_preset=row.get(
458
+ field_headers["parcel_package_preset"]
459
+ ),
460
+ )
461
+ ],
462
+ )
463
+ )
464
+
465
+ instance = (
466
+ ShipmentSerializer.map(data=data, context=context)
467
+ .save(fetch_rates=False)
468
+ .instance
469
+ )
470
+
471
+ return instance
472
+
473
+ return Resource()
@@ -0,0 +1,212 @@
1
+ import json
2
+ from import_export import resources
3
+
4
+ import karrio.server.serializers as serializers
5
+ import karrio.server.manager.models as manager
6
+ import karrio.server.providers.models as providers
7
+ from karrio.server.core import utils, gateway, exceptions
8
+
9
+
10
+ DEFAULT_HEADERS = {
11
+ "id": "ID",
12
+ "tracking_number": "Tracking Number",
13
+ "status": "Status",
14
+ "tracking_carrier": "Carrier",
15
+ "options": "Options",
16
+ }
17
+
18
+
19
+ def tracker_export_resource(query_params: dict, context, data_fields: dict = None):
20
+ queryset = manager.Tracking.access_by(context)
21
+ field_headers = data_fields if data_fields is not None else DEFAULT_HEADERS
22
+ _exclude = query_params.get("exclude", "").split(",")
23
+ _fields = (
24
+ "id",
25
+ "tracking_number",
26
+ "status",
27
+ "tracking_carrier",
28
+ "options",
29
+ )
30
+
31
+ class Resource(resources.ModelResource):
32
+ class Meta:
33
+ model = manager.Tracking
34
+ fields = _fields
35
+ exclude = _exclude
36
+ export_order = [k for k in field_headers.keys() if k not in _exclude]
37
+
38
+ def get_queryset(self):
39
+ return queryset
40
+
41
+ def get_export_headers(self):
42
+ headers = super().get_export_headers()
43
+ return [field_headers.get(k, k) for k in headers]
44
+
45
+ if "tracking_carrier" not in _exclude:
46
+ tracking_carrier = resources.Field()
47
+
48
+ def dehydrate_tracking_carrier(self, row):
49
+ carrier = getattr(row, "tracking_carrier", None)
50
+
51
+ return getattr(carrier, "carrier_name", None)
52
+
53
+ if "options" not in _exclude:
54
+ options = resources.Field(default=None)
55
+
56
+ def dehydrate_options(self, row):
57
+ return json.loads(json.dumps(row.options))
58
+
59
+ return Resource()
60
+
61
+
62
+ def tracker_import_resource(
63
+ query_params: dict,
64
+ context,
65
+ data_fields: dict = None,
66
+ batch_id: str = None,
67
+ **kwargs,
68
+ ):
69
+ queryset = manager.Tracking.access_by(context)
70
+ field_headers = data_fields if data_fields is not None else DEFAULT_HEADERS
71
+ _exclude = query_params.get("exclude", "").split(",")
72
+ _fields = (
73
+ "id",
74
+ "tracking_number",
75
+ "status",
76
+ "tracking_carrier",
77
+ "options",
78
+ )
79
+
80
+ class Resource(resources.ModelResource):
81
+ class Meta:
82
+ model = manager.Tracking
83
+ fields = _fields
84
+ exclude = _exclude
85
+ export_order = [k for k in field_headers.keys() if k not in _exclude]
86
+
87
+ def get_queryset(self):
88
+ return queryset
89
+
90
+ def get_export_headers(self):
91
+ headers = super().get_export_headers()
92
+ return [field_headers.get(k, k) for k in headers]
93
+
94
+ def init_instance(self, row=None):
95
+ carrier_id = row.get("tracking_carrier")
96
+ tracking_number = row.get("tracking_number")
97
+
98
+ errors = []
99
+ if len(tracking_number or "") == 0:
100
+ errors.append(
101
+ exceptions.APIException(
102
+ f"A tracking number is required.",
103
+ code="tracking_number_required",
104
+ )
105
+ )
106
+ if carrier_id is None:
107
+ errors.append(
108
+ exceptions.APIException(
109
+ f"No carrier connection found.",
110
+ code="invalid_carrier",
111
+ )
112
+ )
113
+ if any(errors):
114
+ raise exceptions.APIExceptions(
115
+ errors,
116
+ code="invalid_row",
117
+ )
118
+
119
+ tracker = (
120
+ manager.Tracking.access_by(context)
121
+ .filter(tracking_number=tracking_number)
122
+ .first()
123
+ )
124
+ carrier = providers.Carrier.objects.get(id=carrier_id).settings
125
+ exists = getattr(tracker, "carrier_name", None) == carrier.carrier_name
126
+ meta = {} if batch_id is None else dict(meta=dict(batch_id=batch_id))
127
+
128
+ instance = (
129
+ tracker
130
+ if exists
131
+ else manager.Tracking(
132
+ status="unknown",
133
+ test_mode=context.test_mode,
134
+ created_by_id=context.user.id,
135
+ tracking_number=tracking_number,
136
+ events=utils.default_tracking_event(
137
+ description="Awaiting update from carrier...",
138
+ code="UNKNOWN",
139
+ ),
140
+ **meta,
141
+ )
142
+ )
143
+ return instance
144
+
145
+ def save_instance(
146
+ self, instance, is_create, using_transactions=True, dry_run=False
147
+ ):
148
+ is_create = instance.pk is None
149
+
150
+ result = super().save_instance(
151
+ instance,
152
+ is_create,
153
+ using_transactions,
154
+ dry_run,
155
+ )
156
+
157
+ if is_create:
158
+ serializers.link_org(instance, context)
159
+
160
+ return result
161
+
162
+ def before_import(self, dataset, using_transactions, dry_run, **kwargs):
163
+ if dry_run:
164
+ # Retrieve requested carriers from the database to set their id in the tracking_carrier column
165
+ carrier_col = dataset.get_col(
166
+ dataset.headers.index(data_fields.get("tracking_carrier"))
167
+ )
168
+ carriers = {
169
+ carrier_name: utils.failsafe(
170
+ lambda: gateway.Carriers.first(
171
+ context=context,
172
+ capability="tracking",
173
+ carrier_name=carrier_name,
174
+ raise_not_found=False,
175
+ )
176
+ )
177
+ for carrier_name in set(carrier_col)
178
+ }
179
+
180
+ del dataset[data_fields.get("tracking_carrier")]
181
+ dataset.append_col(
182
+ [
183
+ getattr(carriers.get(carrier_name), "id", None)
184
+ for carrier_name in carrier_col
185
+ ],
186
+ header="tracking_carrier",
187
+ )
188
+
189
+ # set actual fields name to headers
190
+ dataset.headers = [
191
+ next(
192
+ (key for key, value in data_fields.items() if value == header),
193
+ header,
194
+ )
195
+ for header in dataset.headers
196
+ ]
197
+
198
+ unknown_headers = [
199
+ header
200
+ for header in dataset.headers
201
+ if header not in manager.Tracking.__dict__
202
+ ]
203
+
204
+ if any(unknown_headers):
205
+ raise exceptions.APIException(
206
+ code="unknown_headers",
207
+ detail=unknown_headers,
208
+ )
209
+
210
+ return super().before_import(dataset, using_transactions, dry_run, **kwargs)
211
+
212
+ return Resource()