karrio 2023.5.1__py3-none-any.whl → 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.
karrio/core/units.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Karrio universal data types and units definitions"""
2
2
 
3
+ from ctypes import util
3
4
  import attr
4
5
  import typing
5
6
  import numbers
@@ -24,20 +25,20 @@ class PackagePreset:
24
25
  packaging_type: str = None
25
26
 
26
27
 
27
- class LabelType(utils.Enum):
28
+ class LabelType(utils.StrEnum):
28
29
  PDF = "PDF"
29
30
  ZPL = "ZPL"
30
31
  PNG = "PNG"
31
32
 
32
33
 
33
- class DocFormat(utils.Enum):
34
+ class DocFormat(utils.StrEnum):
34
35
  gif = "GIF"
35
36
  jpg = "JPG"
36
37
  pdf = "PDF"
37
38
  png = "PNG"
38
39
 
39
40
 
40
- class PackagingUnit(utils.Enum):
41
+ class PackagingUnit(utils.StrEnum):
41
42
  envelope = "Small Envelope"
42
43
  pak = "Pak"
43
44
  tube = "Tube"
@@ -47,19 +48,19 @@ class PackagingUnit(utils.Enum):
47
48
  your_packaging = "Your Packaging"
48
49
 
49
50
 
50
- class PaymentType(utils.Enum):
51
+ class PaymentType(utils.StrEnum):
51
52
  sender = "SENDER"
52
53
  recipient = "RECIPIENT"
53
54
  third_party = "THIRD_PARTY"
54
55
 
55
56
 
56
- class CreditCardType(utils.Enum):
57
+ class CreditCardType(utils.StrEnum):
57
58
  visa = "Visa"
58
59
  mastercard = "Mastercard"
59
60
  american_express = "AmericanExpress"
60
61
 
61
62
 
62
- class CustomsContentType(utils.Enum):
63
+ class CustomsContentType(utils.StrEnum):
63
64
  documents = "DOCUMENTS"
64
65
  gift = "GIFT"
65
66
  sample = "SAMPLE"
@@ -68,13 +69,14 @@ class CustomsContentType(utils.Enum):
68
69
  other = "OTHER"
69
70
 
70
71
 
71
- class Incoterm(utils.Enum):
72
+ class Incoterm(utils.StrEnum):
72
73
  """universal international shipment incoterm (term of trades)"""
73
74
 
74
75
  CFR = "Cost and Freight"
75
76
  CIF = "Cost Insurance and Freight"
76
77
  CIP = "Carriage and Insurance Paid"
77
78
  CPT = "Carriage Paid To"
79
+ DAP = "Delivery at Place"
78
80
  DAF = "Delivered at Frontier"
79
81
  DDP = "Delivery Duty Paid"
80
82
  DDU = "Delivery Duty Unpaid"
@@ -86,20 +88,36 @@ class Incoterm(utils.Enum):
86
88
  FOB = "Free On Board"
87
89
 
88
90
 
89
- class WeightUnit(utils.Enum):
91
+ class WeightUnit(utils.StrEnum):
90
92
  """universal weight units"""
91
93
 
92
94
  KG = "KG"
93
95
  LB = "LB"
96
+ OZ = "OZ"
97
+ G = "G"
94
98
 
95
99
 
96
- class DimensionUnit(utils.Enum):
100
+ class DimensionUnit(utils.StrEnum):
97
101
  """universal dimension units"""
98
102
 
99
103
  CM = "CM"
100
104
  IN = "IN"
101
105
 
102
106
 
107
+ class VolumeUnit(utils.StrEnum):
108
+ """universal dimension units"""
109
+
110
+ l = "l"
111
+ m3 = "m3"
112
+ i3 = "i3"
113
+ ft3 = "ft3"
114
+ cm3 = "cm3"
115
+
116
+ """ mapping from dimension units to volume units """
117
+ CM = "cm3"
118
+ IN = "i3"
119
+
120
+
103
121
  class FreightClass(utils.Enum):
104
122
  """universal freight_class units"""
105
123
 
@@ -123,7 +141,7 @@ class FreightClass(utils.Enum):
123
141
  freight_class_400 = 400
124
142
 
125
143
 
126
- class UploadDocumentType(utils.Enum):
144
+ class UploadDocumentType(utils.StrEnum):
127
145
  """universal upload document types"""
128
146
 
129
147
  certificate_of_origin = "certificate_of_origin"
@@ -134,17 +152,23 @@ class UploadDocumentType(utils.Enum):
134
152
 
135
153
 
136
154
  class MeasurementOptionsType(typing.NamedTuple):
155
+ quant: typing.Optional[float] = None
156
+
137
157
  min_in: typing.Optional[float] = None
138
158
  min_cm: typing.Optional[float] = None
139
159
  min_lb: typing.Optional[float] = None
140
160
  min_kg: typing.Optional[float] = None
141
161
  min_oz: typing.Optional[float] = None
162
+ min_g: typing.Optional[float] = None
142
163
  max_in: typing.Optional[float] = None
143
164
  max_cm: typing.Optional[float] = None
144
165
  max_lb: typing.Optional[float] = None
145
166
  max_kg: typing.Optional[float] = None
146
167
  max_oz: typing.Optional[float] = None
147
- quant: typing.Optional[float] = None
168
+ max_g: typing.Optional[float] = None
169
+
170
+ min_volume: typing.Optional[float] = None
171
+ max_volume: typing.Optional[float] = None
148
172
 
149
173
 
150
174
  class CarrierCapabilities(utils.Enum):
@@ -153,6 +177,7 @@ class CarrierCapabilities(utils.Enum):
153
177
  shipping = "shipping"
154
178
  tracking = "tracking"
155
179
  paperless = "paperless"
180
+ manifest = "manifest"
156
181
 
157
182
  @classmethod
158
183
  def get_capabilities(cls):
@@ -172,6 +197,8 @@ class CarrierCapabilities(utils.Enum):
172
197
  return "shipping"
173
198
  elif "document" in method_name:
174
199
  return "paperless"
200
+ elif "manifest" in method_name:
201
+ return "manifest"
175
202
 
176
203
  return None
177
204
 
@@ -241,6 +268,13 @@ class Dimension:
241
268
  else:
242
269
  return self._compute(self.CM / 100)
243
270
 
271
+ @property
272
+ def MM(self):
273
+ if self._unit is None or self._value is None:
274
+ return None
275
+ else:
276
+ return self._compute(self.CM * 10)
277
+
244
278
  def map(self, options: MeasurementOptionsType):
245
279
  return Dimension(value=self._value, unit=self._unit, options=options)
246
280
 
@@ -249,24 +283,148 @@ class Volume:
249
283
  """The volume common processing helper"""
250
284
 
251
285
  def __init__(
252
- self, side1: Dimension = None, side2: Dimension = None, side3: Dimension = None
286
+ self,
287
+ side1: Dimension = None,
288
+ side2: Dimension = None,
289
+ side3: Dimension = None,
290
+ value: float = None,
291
+ unit: typing.Union[VolumeUnit, str] = VolumeUnit.cm3,
292
+ options: MeasurementOptionsType = MeasurementOptionsType(),
253
293
  ):
254
294
  self._side1 = side1
255
295
  self._side2 = side2
256
296
  self._side3 = side3
257
297
 
298
+ self._value = value
299
+ self._unit = VolumeUnit[unit] if isinstance(unit, str) else unit
300
+
301
+ self._quant = 0.01
302
+ self._min_volume = options.min_volume
303
+
304
+ def __getitem__(self, item):
305
+ return getattr(self, item)
306
+
307
+ def _compute(self, value: float):
308
+ below_min = self._min_volume is not None and value < self._min_volume
309
+ return utils.NF.decimal(
310
+ value=(self._min_volume if below_min else value),
311
+ quant=self._quant,
312
+ )
313
+
314
+ @property
315
+ def unit(self) -> str:
316
+ if self._unit is None:
317
+ return None
318
+
319
+ return self._unit.value
320
+
258
321
  @property
259
322
  def value(self):
260
- if not any([self._side1.value, self._side2.value, self._side3.value]):
323
+ missing_side_value = not all(
324
+ [
325
+ getattr(self._side1, "value", None),
326
+ getattr(self._side2, "value", None),
327
+ getattr(self._side3, "value", None),
328
+ ]
329
+ )
330
+ missing_value = self._unit is None or self._value is None
331
+
332
+ if missing_side_value and missing_value:
261
333
  return None
262
334
 
263
- return utils.NF.decimal(self._side1.M * self._side2.M * self._side3.M)
335
+ if not missing_value:
336
+ return self._value
337
+
338
+ return self._compute(self._side1.value * self._side2.value * self._side3.value)
264
339
 
265
340
  @property
266
- def cubic_meter(self):
341
+ def l(self):
342
+ if self.value is None:
343
+ return None
344
+ if self._unit == VolumeUnit.m3:
345
+ return self._compute(self.value * 1000)
346
+ elif self._unit == VolumeUnit.i3:
347
+ return self._compute(self.value / 61.024)
348
+ elif self._unit == VolumeUnit.ft3:
349
+ return self._compute(self.value * 28.317)
350
+ if self._unit == VolumeUnit.cm3:
351
+ return self._compute(self.value / 1000)
352
+ else:
353
+ return self.value
354
+
355
+ @property
356
+ def m3(self):
357
+ if self.value is None:
358
+ return None
359
+ if self._unit == VolumeUnit.l:
360
+ return self._compute(self.value / 1000)
361
+ if self._unit == VolumeUnit.cm3:
362
+ return self._compute(self.value / 1e6)
363
+ elif self._unit == VolumeUnit.i3:
364
+ return self._compute(self.value / 61020)
365
+ elif self._unit == VolumeUnit.ft3:
366
+ return self._compute(self.value / 35.315)
367
+ else:
368
+ return self.value
369
+
370
+ @property
371
+ def i3(self):
372
+ if self.value is None:
373
+ return None
374
+ if self._unit == VolumeUnit.l:
375
+ return self._compute(self.value * 61.024)
376
+ if self._unit == VolumeUnit.m3:
377
+ return self._compute(self.value * 1000000)
378
+ elif self._unit == VolumeUnit.cm3:
379
+ return self._compute(self.value / 16.387)
380
+ elif self._unit == VolumeUnit.ft3:
381
+ return self._compute(self.value * 1728)
382
+ else:
383
+ return self.value
384
+
385
+ @property
386
+ def ft3(self):
387
+ if self.value is None:
388
+ return None
389
+ if self._unit == VolumeUnit.l:
390
+ return self._compute(self.value / 28.317)
391
+ if self._unit == VolumeUnit.m3:
392
+ return self._compute(self.value * 35.315)
393
+ elif self._unit == VolumeUnit.i3:
394
+ return self._compute(self.value / 1728)
395
+ elif self._unit == VolumeUnit.cm3:
396
+ return self._compute(self.value / 28320)
397
+ else:
398
+ return self.value
399
+
400
+ @property
401
+ def cm3(self):
267
402
  if self.value is None:
268
403
  return None
269
- return utils.NF.decimal(self.value * 250)
404
+ if self._unit == VolumeUnit.l:
405
+ return self._compute(self.value * 1000)
406
+ if self._unit == VolumeUnit.m3:
407
+ return self._compute(self.value * 1e6)
408
+ elif self._unit == VolumeUnit.i3:
409
+ return self._compute(self.value * 16.387)
410
+ elif self._unit == VolumeUnit.ft3:
411
+ return self._compute(self.value * 28320)
412
+ else:
413
+ return self.value
414
+
415
+ @property
416
+ def cubic_meter(self):
417
+ return self.m3
418
+
419
+ def map(self, options: MeasurementOptionsType):
420
+ return Volume(
421
+ side1=self._side1,
422
+ side2=self._side2,
423
+ side3=self._side3,
424
+ value=self._value,
425
+ unit=self._unit,
426
+ options=options,
427
+ )
270
428
 
271
429
 
272
430
  class Girth:
@@ -306,6 +464,7 @@ class Weight:
306
464
  self._min_lb = options.min_lb
307
465
  self._min_kg = options.min_kg
308
466
  self._min_oz = options.min_oz
467
+ self._min_g = options.min_g
309
468
  self._quant = options.quant
310
469
 
311
470
  def __getitem__(self, item):
@@ -339,6 +498,10 @@ class Weight:
339
498
  return self._compute(self._value, self._min_kg)
340
499
  elif self._unit == WeightUnit.LB:
341
500
  return self._compute(self._value / 2.205, self._min_kg)
501
+ elif self._unit == WeightUnit.OZ:
502
+ return self._compute(self._value / 35.274, self._min_kg)
503
+ elif self._unit == WeightUnit.G:
504
+ return self._compute(self._value / 1000, self._min_kg)
342
505
 
343
506
  return None
344
507
 
@@ -350,6 +513,10 @@ class Weight:
350
513
  return self._compute(self._value, self._min_lb)
351
514
  elif self._unit == WeightUnit.KG:
352
515
  return self._compute(self._value * 2.205, self._min_lb)
516
+ elif self._unit == WeightUnit.OZ:
517
+ return self._compute(self._value / 16, self._min_lb)
518
+ elif self._unit == WeightUnit.G:
519
+ return self._compute(self._value / 453.6, self._min_lb)
353
520
 
354
521
  return None
355
522
 
@@ -357,10 +524,29 @@ class Weight:
357
524
  def OZ(self) -> typing.Optional[float]:
358
525
  if self._unit is None or self._value is None:
359
526
  return None
527
+ elif self._unit == WeightUnit.OZ:
528
+ return self._compute(self._value, self._min_oz)
360
529
  if self._unit == WeightUnit.LB:
361
530
  return self._compute(self._value * 16, self._min_oz)
362
531
  elif self._unit == WeightUnit.KG:
363
532
  return self._compute(self._value * 35.274, self._min_oz)
533
+ elif self._unit == WeightUnit.G:
534
+ return self._compute(self._value / 28.35, self._min_oz)
535
+
536
+ return None
537
+
538
+ @property
539
+ def G(self) -> typing.Optional[float]:
540
+ if self._unit is None or self._value is None:
541
+ return None
542
+ elif self._unit == WeightUnit.G:
543
+ return self._compute(self._value, self._min_g)
544
+ if self._unit == WeightUnit.LB:
545
+ return self._compute(self._value * 453.6, self._min_g)
546
+ elif self._unit == WeightUnit.KG:
547
+ return self._compute(self._value * 1000, self._min_g)
548
+ elif self._unit == WeightUnit.OZ:
549
+ return self._compute(self._value * 28.35, self._min_g)
364
550
 
365
551
  return None
366
552
 
@@ -419,6 +605,9 @@ class Products(typing.Iterable[Product]):
419
605
  weight_unit: str = None,
420
606
  ):
421
607
  self._items = [Product(item, weight_unit=weight_unit) for item in items]
608
+ self._weight_unit = (
609
+ weight_unit or self._items[0].weight_unit if any(self._items) else None
610
+ )
422
611
 
423
612
  def __len__(self) -> int:
424
613
  return len(self._items)
@@ -435,7 +624,30 @@ class Products(typing.Iterable[Product]):
435
624
 
436
625
  @property
437
626
  def value_amount(self):
438
- return sum((item.value_amount or 0.0 for item in self._items), 0.0)
627
+ return sum(
628
+ (
629
+ item.value_amount * item.quantity
630
+ for item in self._items
631
+ if utils.NF.decimal(item.value_amount) is not None
632
+ ),
633
+ 0.0,
634
+ )
635
+
636
+ @property
637
+ def weight(self) -> Weight:
638
+ return Weight(
639
+ sum([item.weight * item.quantity for item in self._items], 0.0),
640
+ self._weight_unit,
641
+ )
642
+
643
+ @property
644
+ def description(self) -> typing.Optional[str]:
645
+ descriptions = set([item.description for item in self._items])
646
+ description: typing.Optional[str] = utils.SF.concat_str(
647
+ *list(descriptions), join=True
648
+ ) # type:ignore
649
+
650
+ return description
439
651
 
440
652
 
441
653
  class Package:
@@ -449,13 +661,16 @@ class Package:
449
661
  package_option_type: typing.Type[utils.Enum] = utils.Enum,
450
662
  weight_unit: str = None,
451
663
  dimension_unit: str = None,
664
+ shipping_options_initializer: typing.Callable = None,
452
665
  ):
453
666
  self.parcel: models.Parcel = parcel
454
667
  self.preset: PackagePreset = template or PackagePreset()
455
668
 
456
- self._options: "ShippingOptions" = ShippingOptions(
457
- {**parcel.options, **getattr(options, "content", {})},
458
- package_option_type,
669
+ _options = {**parcel.options, **getattr(options, "content", {})}
670
+ self._options: "ShippingOptions" = (
671
+ shipping_options_initializer(_options)
672
+ if shipping_options_initializer is not None
673
+ else ShippingOptions(_options, package_option_type)
459
674
  )
460
675
  self._dimension_unit = (
461
676
  dimension_unit or self.parcel.dimension_unit or self.preset.dimension_unit
@@ -523,7 +738,9 @@ class Package:
523
738
 
524
739
  @property
525
740
  def volume(self) -> Volume:
526
- return Volume(self.width, self.length, self.height)
741
+ return Volume(
742
+ self.width, self.length, self.height, unit=self.dimension_unit.value
743
+ )
527
744
 
528
745
  @property
529
746
  def thickness(self) -> Dimension:
@@ -561,6 +778,17 @@ class Package:
561
778
 
562
779
  return Products(_items, self.weight_unit.value)
563
780
 
781
+ @property
782
+ def total_value(self) -> typing.Optional[float]:
783
+ if not any(self.parcel.items or []):
784
+ return None
785
+
786
+ return self.items.value_amount
787
+
788
+ @property
789
+ def reference_number(self) -> typing.Optional[str]:
790
+ return self.parcel.reference_number
791
+
564
792
 
565
793
  class Packages(typing.Iterable[Package]):
566
794
  """The parcel collection common processing helper"""
@@ -573,6 +801,7 @@ class Packages(typing.Iterable[Package]):
573
801
  max_weight: Weight = None,
574
802
  options: "ShippingOptions" = None,
575
803
  package_option_type: typing.Type[utils.Enum] = utils.Enum,
804
+ shipping_options_initializer: typing.Callable = None,
576
805
  ):
577
806
  self._compatible_units = self._compute_compatible_units(parcels, presets)
578
807
  self._options = options or ShippingOptions({}, package_option_type)
@@ -584,12 +813,14 @@ class Packages(typing.Iterable[Package]):
584
813
  package_option_type=package_option_type,
585
814
  weight_unit=self._compatible_units[0].value,
586
815
  dimension_unit=self._compatible_units[1].value,
816
+ shipping_options_initializer=shipping_options_initializer,
587
817
  )
588
818
  for parcel in parcels
589
819
  ]
590
820
  self._required = required
591
821
  self._max_weight = max_weight
592
822
  self._package_option_type = package_option_type
823
+ self._shipping_options_initializer = shipping_options_initializer
593
824
 
594
825
  self.validate()
595
826
 
@@ -649,6 +880,24 @@ class Packages(typing.Iterable[Package]):
649
880
 
650
881
  return Weight(unit=unit, value=value)
651
882
 
883
+ @property
884
+ def volume(self) -> Volume:
885
+ if not any([pkg.volume.value for pkg in self._items]):
886
+ return Volume(value=None)
887
+
888
+ _, _dimension_unit = self._compatible_units
889
+ _volume_unit = VolumeUnit[_dimension_unit.name]
890
+ _total_volume = sum(
891
+ [
892
+ pkg.volume[_volume_unit.name]
893
+ for pkg in self._items
894
+ if pkg.volume is not None
895
+ ],
896
+ 0.0,
897
+ )
898
+
899
+ return Volume(value=_total_volume, unit=_volume_unit)
900
+
652
901
  @property
653
902
  def package_type(self) -> str:
654
903
  return (
@@ -670,6 +919,15 @@ class Packages(typing.Iterable[Package]):
670
919
 
671
920
  return description
672
921
 
922
+ @property
923
+ def content(self) -> typing.Optional[str]:
924
+ contents = set([item.parcel.content for item in self._items])
925
+ content: typing.Optional[str] = utils.SF.concat_str(
926
+ *list(contents), join=True
927
+ ) # type:ignore
928
+
929
+ return content
930
+
673
931
  @property
674
932
  def options(self) -> "ShippingOptions":
675
933
  def merge_options(acc, pkg) -> dict:
@@ -699,6 +957,9 @@ class Packages(typing.Iterable[Package]):
699
957
  self._options.content,
700
958
  )
701
959
 
960
+ if self._shipping_options_initializer is not None:
961
+ return self._shipping_options_initializer(options)
962
+
702
963
  return ShippingOptions(options, self._package_option_type)
703
964
 
704
965
  @property
@@ -721,6 +982,15 @@ class Packages(typing.Iterable[Package]):
721
982
 
722
983
  return Products(_items, _weight_unit.value)
723
984
 
985
+ @property
986
+ def total_value(self) -> typing.Optional[float]:
987
+ if not any([_.total_value for _ in self._items]):
988
+ return None
989
+
990
+ return sum(
991
+ [pkg.total_value for pkg in self._items if pkg.total_value is not None], 0.0
992
+ )
993
+
724
994
  def validate(self, required: typing.List[str] = None, max_weight: Weight = None):
725
995
  required = required or self._required
726
996
  max_weight = max_weight or self._max_weight
@@ -760,6 +1030,7 @@ class Packages(typing.Iterable[Package]):
760
1030
  max_weight: Weight = None,
761
1031
  options: "ShippingOptions" = None,
762
1032
  package_option_type: typing.Type[utils.Enum] = utils.Enum,
1033
+ shipping_options_initializer: typing.Callable = None,
763
1034
  ) -> typing.Union[typing.List[Package], "Packages"]:
764
1035
  return typing.cast(
765
1036
  typing.Union[typing.List[Package], Packages],
@@ -770,6 +1041,7 @@ class Packages(typing.Iterable[Package]):
770
1041
  max_weight,
771
1042
  options,
772
1043
  package_option_type,
1044
+ shipping_options_initializer,
773
1045
  ),
774
1046
  )
775
1047
 
@@ -796,6 +1068,7 @@ class Options:
796
1068
  _key = key
797
1069
  option_values[key] = _val
798
1070
 
1071
+ self._raw_options = options
799
1072
  self._options = option_values
800
1073
  self._option_list = self._filter(
801
1074
  option_values, (items_filter or utils.identity)
@@ -837,20 +1110,34 @@ class ShippingOption(utils.Enum):
837
1110
  """universal shipment options (special services)"""
838
1111
 
839
1112
  currency = utils.OptionEnum("currency")
1113
+ is_return = utils.OptionEnum("is_return", bool)
840
1114
  insurance = utils.OptionEnum("insurance", float)
841
1115
  cash_on_delivery = utils.OptionEnum("COD", float)
842
1116
  shipment_note = utils.OptionEnum("shipment_note")
843
- shipment_date = utils.OptionEnum("shipment_date")
844
1117
  dangerous_good = utils.OptionEnum("dangerous_good", bool)
845
1118
  declared_value = utils.OptionEnum("declared_value", float)
846
1119
  paperless_trade = utils.OptionEnum("paperless_trade", bool)
847
- hold_at_location = utils.OptionEnum("hold_at_location", bool)
848
- sms_notification = utils.OptionEnum("email_notification", bool)
1120
+ sms_notification = utils.OptionEnum("sms_notification", bool)
849
1121
  email_notification = utils.OptionEnum("email_notification", bool)
850
1122
  email_notification_to = utils.OptionEnum("email_notification_to")
851
1123
  signature_confirmation = utils.OptionEnum("signature_confirmation", bool)
1124
+ saturday_delivery = utils.OptionEnum("saturday_delivery", bool)
1125
+ sunday_delivery = utils.OptionEnum("sunday_delivery", bool)
852
1126
  doc_files = utils.OptionEnum("doc_files", utils.DP.to_dict)
853
1127
  doc_references = utils.OptionEnum("doc_references", utils.DP.to_dict)
1128
+ hold_at_location = utils.OptionEnum("hold_at_location", bool)
1129
+ hold_at_location_address = utils.OptionEnum(
1130
+ "hold_at_location_address",
1131
+ functools.partial(utils.DP.to_object, models.Address),
1132
+ )
1133
+ shipper_instructions = utils.OptionEnum("shipper_instructions")
1134
+ recipient_instructions = utils.OptionEnum("recipient_instructions")
1135
+
1136
+ """TODO: dreprecate these"""
1137
+ shipment_date = utils.OptionEnum("shipment_date")
1138
+
1139
+ """TODO: standardize to these"""
1140
+ shipping_date = utils.OptionEnum("shipping_date") # format: %Y-%m-%dT%H:%M
854
1141
 
855
1142
 
856
1143
  class ShippingOptions(Options):
@@ -896,8 +1183,22 @@ class ShippingOptions(Options):
896
1183
 
897
1184
  @property
898
1185
  def shipment_date(self) -> utils.OptionEnum:
1186
+ # Check if shipment_date is not defined and fallback to shipping_date
1187
+ if not self[ShippingOption.shipment_date.name].state:
1188
+ return utils.OptionEnum(
1189
+ "shipment_date",
1190
+ str,
1191
+ utils.DF.fdate(
1192
+ self._raw_options.get("shipping_date"), "%Y-%m-%dT%H:%M"
1193
+ ),
1194
+ )
1195
+
899
1196
  return self[ShippingOption.shipment_date.name]
900
1197
 
1198
+ @property
1199
+ def shipping_date(self) -> utils.OptionEnum:
1200
+ return self[ShippingOption.shipping_date.name]
1201
+
901
1202
  @property
902
1203
  def signature_confirmation(self) -> utils.OptionEnum:
903
1204
  return self[ShippingOption.signature_confirmation.name]
@@ -922,19 +1223,63 @@ class ShippingOptions(Options):
922
1223
  def shipment_note(self) -> utils.OptionEnum:
923
1224
  return self[ShippingOption.shipment_note.name]
924
1225
 
1226
+ @property
1227
+ def shipper_instructions(self) -> utils.OptionEnum:
1228
+ return self[ShippingOption.shipper_instructions.name]
1229
+
1230
+ @property
1231
+ def recipient_instructions(self) -> utils.OptionEnum:
1232
+ return self[ShippingOption.recipient_instructions.name]
1233
+
925
1234
 
926
1235
  class CustomsOption(utils.Enum):
927
1236
  """common shipment customs identifiers"""
928
1237
 
929
1238
  aes = utils.OptionEnum("aes")
1239
+ ioss = utils.OptionEnum("ioss")
930
1240
  eel_pfc = utils.OptionEnum("eel_pfc")
931
- nip_number = utils.OptionEnum("eori_number")
1241
+ nip_number = utils.OptionEnum("nip_number")
932
1242
  eori_number = utils.OptionEnum("eori_number")
933
1243
  license_number = utils.OptionEnum("license_number")
934
1244
  certificate_number = utils.OptionEnum("certificate_number")
935
1245
  vat_registration_number = utils.OptionEnum("vat_registration_number")
936
1246
 
937
1247
 
1248
+ class CustomsOptions(Options):
1249
+ """The options common processing helper"""
1250
+
1251
+ def __init__(self, *args, **kwargs):
1252
+ super().__init__(*args, **kwargs, base_option_type=CustomsOption)
1253
+
1254
+ @property
1255
+ def aes(self) -> utils.OptionEnum:
1256
+ return self[CustomsOption.aes.name]
1257
+
1258
+ @property
1259
+ def eel_pfc(self) -> utils.OptionEnum:
1260
+ return self[CustomsOption.eel_pfc.name]
1261
+
1262
+ @property
1263
+ def nip_number(self) -> utils.OptionEnum:
1264
+ return self[CustomsOption.nip_number.name]
1265
+
1266
+ @property
1267
+ def eori_number(self) -> utils.OptionEnum:
1268
+ return self[CustomsOption.eori_number.name]
1269
+
1270
+ @property
1271
+ def license_number(self) -> utils.OptionEnum:
1272
+ return self[CustomsOption.license_number.name]
1273
+
1274
+ @property
1275
+ def certificate_number(self) -> utils.OptionEnum:
1276
+ return self[CustomsOption.certificate_number.name]
1277
+
1278
+ @property
1279
+ def vat_registration_number(self) -> utils.OptionEnum:
1280
+ return self[CustomsOption.vat_registration_number.name]
1281
+
1282
+
938
1283
  class CustomsInfo(models.Customs):
939
1284
  """The customs info processing helper"""
940
1285
 
@@ -948,10 +1293,9 @@ class CustomsInfo(models.Customs):
948
1293
  recipient: typing.Optional[models.Address] = None,
949
1294
  ):
950
1295
  _customs = customs or default_to
951
- options = Options(
1296
+ options = CustomsOptions(
952
1297
  getattr(_customs, "options", None) or {},
953
1298
  option_type=option_type,
954
- base_option_type=CustomsOption,
955
1299
  )
956
1300
 
957
1301
  self._customs = _customs
@@ -974,7 +1318,7 @@ class CustomsInfo(models.Customs):
974
1318
  return self._customs is not None
975
1319
 
976
1320
  @property
977
- def duty(self) -> typing.Optional[models.Duty]: # type:ignore
1321
+ def duty(self) -> models.Duty: # type:ignore
978
1322
  return getattr(self._customs, "duty", None) or models.Duty()
979
1323
 
980
1324
  @property
@@ -1116,7 +1460,7 @@ class ComputedAddress(models.Address):
1116
1460
 
1117
1461
  @property
1118
1462
  def country_name(self):
1119
- return Country[self.address.country_code].value
1463
+ return Country.map(self.address.country_code).value
1120
1464
 
1121
1465
  @property
1122
1466
  def address_line(self) -> str:
@@ -1151,7 +1495,6 @@ class ComputedAddress(models.Address):
1151
1495
  join=True,
1152
1496
  ),
1153
1497
  )
1154
- return self.address.address_line1.replace(self.street_number, "").strip()
1155
1498
 
1156
1499
  @property
1157
1500
  def tax_id(self) -> typing.Optional[str]:
@@ -1184,6 +1527,20 @@ class ComputedAddress(models.Address):
1184
1527
  self.address, "company_name", None
1185
1528
  )
1186
1529
 
1530
+ @property
1531
+ def first_name(self) -> typing.Optional[str]:
1532
+ if self.address.person_name is None:
1533
+ return None
1534
+
1535
+ return self.address.person_name.split(" ")[0]
1536
+
1537
+ @property
1538
+ def last_name(self) -> typing.Optional[str]:
1539
+ if self.address.person_name is None:
1540
+ return None
1541
+
1542
+ return self.address.person_name.split(" ")[-1]
1543
+
1187
1544
  def _compute_address_line(self, join: bool = True) -> typing.Optional[str]:
1188
1545
  if any(
1189
1546
  [
@@ -1202,7 +1559,7 @@ class ComputedAddress(models.Address):
1202
1559
  def _compute_street_number(self):
1203
1560
  _value = getattr(self.address, "street_number", None)
1204
1561
 
1205
- if _value is None:
1562
+ if _value is None and self.address:
1206
1563
  words = self.address.address_line1.split(" ")
1207
1564
 
1208
1565
  if any(_.isdigit() for _ in words[0]):
@@ -1231,16 +1588,20 @@ class ComputedDocumentFile(models.DocumentFile):
1231
1588
 
1232
1589
 
1233
1590
  class TrackingStatus(utils.Enum):
1591
+ pending = ["pending"]
1234
1592
  on_hold = ["on_hold"]
1593
+ cancelled = ["cancelled"]
1235
1594
  delivered = ["delivered"]
1236
1595
  in_transit = ["in_transit"]
1237
1596
  delivery_failed = ["delivery_failed"]
1238
1597
  delivery_delayed = ["delivery_delayed"]
1239
1598
  out_for_delivery = ["out_for_delivery"]
1240
1599
  ready_for_pickup = ["ready_for_pickup"]
1600
+ return_to_sender = ["return_to_sender"]
1601
+ unknown = ["unknown"]
1241
1602
 
1242
1603
 
1243
- def create_enum(name, values):
1604
+ def create_enum(name, values) -> utils.Enum:
1244
1605
  return utils.Enum(name, values) # type: ignore
1245
1606
 
1246
1607
 
@@ -1393,6 +1754,7 @@ class Currency(utils.Enum):
1393
1754
 
1394
1755
 
1395
1756
  class Country(utils.Enum):
1757
+ AC = "Ascension Island"
1396
1758
  AD = "Andorra"
1397
1759
  AE = "United Arab Emirates"
1398
1760
  AF = "Afghanistan"
@@ -1627,9 +1989,17 @@ class Country(utils.Enum):
1627
1989
  ZA = "South Africa"
1628
1990
  ZM = "Zambia"
1629
1991
  ZW = "Zimbabwe"
1992
+ # Adding missing country codes
1993
+ EH = "Western Sahara"
1994
+ IM = "Isle of Man"
1995
+ BL = "Saint Barthelemy"
1996
+ MF = "Saint Martin"
1997
+ SX = "Sint Maarten"
1998
+ XK = "Kosovo"
1630
1999
 
1631
2000
 
1632
2001
  class CountryCurrency(utils.Enum):
2002
+ AC = "USD"
1633
2003
  AD = "EUR"
1634
2004
  AE = "AED"
1635
2005
  AF = "USD"
@@ -2357,3 +2727,240 @@ class EUCountry(utils.Enum):
2357
2727
  SK = "Slovakia"
2358
2728
  ES = "Spain"
2359
2729
  SE = "Sweden"
2730
+
2731
+
2732
+ class CountryCode(utils.Enum):
2733
+ AD = "AND" # Andorra
2734
+ AE = "ARE" # United Arab Emirates
2735
+ AF = "AFG" # Afghanistan
2736
+ AG = "ATG" # Antigua
2737
+ AI = "AIA" # Anguilla
2738
+ AL = "ALB" # Albania
2739
+ AM = "ARM" # Armenia
2740
+ AN = "ANT" # Netherlands Antilles
2741
+ AO = "AGO" # Angola
2742
+ AR = "ARG" # Argentina
2743
+ AS = "ASM" # American Samoa
2744
+ AT = "AUT" # Austria
2745
+ AU = "AUS" # Australia
2746
+ AW = "ABW" # Aruba
2747
+ AZ = "AZE" # Azerbaijan
2748
+ BA = "BIH" # Bosnia And Herzegovina
2749
+ BB = "BRB" # Barbados
2750
+ BD = "BGD" # Bangladesh
2751
+ BE = "BEL" # Belgium
2752
+ BF = "BFA" # Burkina Faso
2753
+ BG = "BGR" # Bulgaria
2754
+ BH = "BHR" # Bahrain
2755
+ BI = "BDI" # Burundi
2756
+ BJ = "BEN" # Benin
2757
+ BM = "BMU" # Bermuda
2758
+ BN = "BRN" # Brunei
2759
+ BO = "BOL" # Bolivia
2760
+ BR = "BRA" # Brazil
2761
+ BS = "BHS" # Bahamas
2762
+ BT = "BTN" # Bhutan
2763
+ BW = "BWA" # Botswana
2764
+ BY = "BLR" # Belarus
2765
+ BZ = "BLZ" # Belize
2766
+ CA = "CAN" # Canada
2767
+ CD = "COD" # Congo, Democratic Republic of the
2768
+ CF = "CAF" # Central African Republic
2769
+ CG = "COG" # Congo
2770
+ CH = "CHE" # Switzerland
2771
+ CI = "CIV" # Cote D Ivoire
2772
+ CK = "COK" # Cook Islands
2773
+ CL = "CHL" # Chile
2774
+ CM = "CMR" # Cameroon
2775
+ CN = "CHN" # China
2776
+ CO = "COL" # Colombia
2777
+ CR = "CRI" # Costa Rica
2778
+ CU = "CUB" # Cuba
2779
+ CV = "CPV" # Cape Verde
2780
+ CY = "CYP" # Cyprus
2781
+ CZ = "CZE" # Czech Republic
2782
+ DE = "DEU" # Germany
2783
+ DJ = "DJI" # Djibouti
2784
+ DK = "DNK" # Denmark
2785
+ DM = "DMA" # Dominica
2786
+ DO = "DOM" # Dominican Republic
2787
+ DZ = "DZA" # Algeria
2788
+ EC = "ECU" # Ecuador
2789
+ EE = "EST" # Estonia
2790
+ EG = "EGY" # Egypt
2791
+ ER = "ERI" # Eritrea
2792
+ ES = "ESP" # Spain
2793
+ ET = "ETH" # Ethiopia
2794
+ FI = "FIN" # Finland
2795
+ FJ = "FJI" # Fiji
2796
+ FK = "FLK" # Falkland Islands
2797
+ FM = "FSM" # Micronesia, Federated States Of
2798
+ FO = "FRO" # Faroe Islands
2799
+ FR = "FRA" # France
2800
+ GA = "GAB" # Gabon
2801
+ GB = "GBR" # United Kingdom
2802
+ GD = "GRD" # Grenada
2803
+ GE = "GEO" # Georgia
2804
+ GF = "GUF" # French Guyana
2805
+ GG = "GGY" # Guernsey
2806
+ GH = "GHA" # Ghana
2807
+ GI = "GIB" # Gibraltar
2808
+ GL = "GRL" # Greenland
2809
+ GM = "GMB" # Gambia
2810
+ GN = "GIN" # Guinea Republic
2811
+ GP = "GLP" # Guadeloupe
2812
+ GQ = "GNQ" # Guinea-equatorial
2813
+ GR = "GRC" # Greece
2814
+ GT = "GTM" # Guatemala
2815
+ GU = "GUM" # Guam
2816
+ GW = "GNB" # Guinea-bissau
2817
+ GY = "GUY" # Guyana (british)
2818
+ HK = "HKG" # Hong Kong
2819
+ HN = "HND" # Honduras
2820
+ HR = "HRV" # Croatia
2821
+ HT = "HTI" # Haiti
2822
+ HU = "HUN" # Hungary
2823
+ IC = "ICA" # Canary Islands
2824
+ ID = "IDN" # Indonesia
2825
+ IE = "IRL" # Ireland
2826
+ IL = "ISR" # Israel
2827
+ IN = "IND" # India
2828
+ IQ = "IRQ" # Iraq
2829
+ IR = "IRN" # Iran
2830
+ IS = "ISL" # Iceland
2831
+ IT = "ITA" # Italy
2832
+ JE = "JEY" # Jersey
2833
+ JM = "JAM" # Jamaica
2834
+ JO = "JOR" # Jordan
2835
+ JP = "JPN" # Japan
2836
+ KE = "KEN" # Kenya
2837
+ KG = "KGZ" # Kyrgyzstan
2838
+ KH = "KHM" # Cambodia
2839
+ KI = "KIR" # Kiribati
2840
+ KM = "COM" # Comoros
2841
+ KN = "KNA" # St. Kitts
2842
+ KP = "PRK" # Korea, The D.p.r Of (north K.)
2843
+ KR = "KOR" # Korea, Republic of (South Korea)
2844
+ KV = "XKX" # Kosovo
2845
+ KW = "KWT" # Kuwait
2846
+ KY = "CYM" # Cayman Islands
2847
+ KZ = "KAZ" # Kazakhstan
2848
+ LA = "LAO" # Lao Peoples Democratic Republic
2849
+ LB = "LBN" # Lebanon
2850
+ LC = "LCA" # St. Lucia
2851
+ LI = "LIE" # Liechtenstein
2852
+ LK = "LKA" # Sri Lanka
2853
+ LR = "LBR" # Liberia
2854
+ LS = "LSO" # Lesotho
2855
+ LT = "LTU" # Lithuania
2856
+ LU = "LUX" # Luxembourg
2857
+ LV = "LVA" # Latvia
2858
+ LY = "LBY" # Libya
2859
+ MA = "MAR" # Morocco
2860
+ MC = "MCO" # Monaco
2861
+ MD = "MDA" # Moldova
2862
+ ME = "MNE" # Montenegro
2863
+ MG = "MDG" # Madagascar
2864
+ MH = "MHL" # Marshall Islands
2865
+ MK = "MKD" # Macedonia
2866
+ ML = "MLI" # Mali
2867
+ MM = "MMR" # Myanmar
2868
+ MN = "MNG" # Mongolia
2869
+ MO = "MAC" # Macau
2870
+ MP = "MNP" # Nothern Mariana Islands, Commonwealth of
2871
+ MQ = "MTQ" # Martinique
2872
+ MR = "MRT" # Mauritania
2873
+ MS = "MSR" # Montserrat
2874
+ MT = "MLT" # Malta
2875
+ MU = "MUS" # Mauritius
2876
+ MV = "MDV" # Maldives
2877
+ MW = "MWI" # Malawi
2878
+ MX = "MEX" # Mexico
2879
+ MY = "MYS" # Malaysia
2880
+ MZ = "MOZ" # Mozambique
2881
+ NA = "NAM" # Namibia
2882
+ NC = "NCL" # New Caledonia
2883
+ NE = "NER" # Niger
2884
+ NG = "NGA" # Nigeria
2885
+ NI = "NIC" # Nicaragua
2886
+ NL = "NLD" # Netherlands
2887
+ NO = "NOR" # Norway
2888
+ NP = "NPL" # Nepal
2889
+ NR = "NRU" # Nauru
2890
+ NU = "NIU" # Niue
2891
+ NZ = "NZL" # New Zealand
2892
+ OM = "OMN" # Oman
2893
+ PA = "PAN" # Panama
2894
+ PE = "PER" # Peru
2895
+ PF = "PYF" # Tahiti
2896
+ PG = "PNG" # Papua New Guinea
2897
+ PH = "PHL" # Philippines
2898
+ PK = "PAK" # Pakistan
2899
+ PL = "POL" # Poland
2900
+ PR = "PRI" # Puerto Rico
2901
+ PT = "PRT" # Portugal
2902
+ PW = "PLW" # Palau
2903
+ PY = "PRY" # Paraguay
2904
+ QA = "QAT" # Qatar
2905
+ RE = "REU" # Reunion
2906
+ RO = "ROU" # Romania
2907
+ RS = "SRB" # Serbia
2908
+ RU = "RUS" # Russia
2909
+ RW = "RWA" # Rwanda
2910
+ SA = "SAU" # Saudi Arabia
2911
+ SB = "SLB" # Solomon Islands
2912
+ SC = "SYC" # Seychelles
2913
+ SD = "SDN" # Sudan
2914
+ SE = "SWE" # Sweden
2915
+ SG = "SGP" # Singapore
2916
+ SH = "SHN" # Saint Helena
2917
+ SI = "SVN" # Slovenia
2918
+ SK = "SVK" # Slovakia
2919
+ SL = "SLE" # Sierra Leone
2920
+ SM = "SMR" # San Marino
2921
+ SN = "SEN" # Senegal
2922
+ SO = "SOM" # Somalia
2923
+ SR = "SUR" # Suriname
2924
+ SS = "SSD" # South Sudan
2925
+ ST = "STP" # Sao Tome And Principe
2926
+ SV = "SLV" # El Salvador
2927
+ SY = "SYR" # Syria
2928
+ SZ = "SWZ" # Swaziland
2929
+ TC = "TCA" # Turks And Caicos Islands
2930
+ TD = "TCD" # Chad
2931
+ TG = "TGO" # Togo
2932
+ TH = "THA" # Thailand
2933
+ TJ = "TJK" # Tajikistan
2934
+ TL = "TLS" # Timor Leste
2935
+ TN = "TUN" # Tunisia
2936
+ TO = "TON" # Tonga
2937
+ TR = "TUR" # Turkey
2938
+ TT = "TTO" # Trinidad And Tobago
2939
+ TV = "TUV" # Tuvalu
2940
+ TW = "TWN" # Taiwan
2941
+ TZ = "TZA" # Tanzania
2942
+ UA = "UKR" # Ukraine
2943
+ UG = "UGA" # Uganda
2944
+ US = "USA" # United States
2945
+ UY = "URY" # Uruguay
2946
+ UZ = "UZB" # Uzbekistan
2947
+ VA = "VAT" # Vatican City
2948
+ VC = "VCT" # St. Vincent
2949
+ VE = "VEN" # Venezuela
2950
+ VG = "VGB" # British Virgin Islands
2951
+ VI = "VIR" # U.S. Virgin Islands
2952
+ VN = "VNM" # Vietnam
2953
+ VU = "VUT" # Vanuatu
2954
+ WS = "WSM" # Samoa
2955
+ XB = "BES" # Bonaire
2956
+ XC = "CUW" # Curacao
2957
+ XE = "EUX" # St. Eustatius
2958
+ XM = "SXM" # St. Maarten
2959
+ XN = "KNA" # Nevis
2960
+ XS = "SOM" # Somaliland, Rep Of (north Somalia)
2961
+ XY = "BLM" # St. Barthelemy
2962
+ YE = "YEM" # Yemen
2963
+ YT = "MYT" # Mayotte
2964
+ ZA = "ZAF" # South Africa
2965
+ ZM = "ZMB" # Zambia
2966
+ ZW = "ZWE" # Zimbabwe