karrio-seko 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/mappers/seko/__init__.py +3 -0
- karrio/mappers/seko/mapper.py +64 -0
- karrio/mappers/seko/proxy.py +88 -0
- karrio/mappers/seko/settings.py +20 -0
- karrio/plugins/seko/__init__.py +20 -0
- karrio/providers/seko/__init__.py +17 -0
- karrio/providers/seko/error.py +73 -0
- karrio/providers/seko/manifest.py +62 -0
- karrio/providers/seko/rate.py +107 -0
- karrio/providers/seko/shipment/__init__.py +9 -0
- karrio/providers/seko/shipment/cancel.py +61 -0
- karrio/providers/seko/shipment/create.py +224 -0
- karrio/providers/seko/tracking.py +95 -0
- karrio/providers/seko/units.py +239 -0
- karrio/providers/seko/utils.py +114 -0
- karrio/schemas/seko/__init__.py +0 -0
- karrio/schemas/seko/error_response.py +11 -0
- karrio/schemas/seko/manifest_response.py +19 -0
- karrio/schemas/seko/rating_request.py +46 -0
- karrio/schemas/seko/rating_response.py +41 -0
- karrio/schemas/seko/shipping_request.py +131 -0
- karrio/schemas/seko/shipping_response.py +79 -0
- karrio/schemas/seko/tracking_response.py +24 -0
- karrio_seko-2025.5rc1.dist-info/METADATA +45 -0
- karrio_seko-2025.5rc1.dist-info/RECORD +28 -0
- karrio_seko-2025.5rc1.dist-info/WHEEL +5 -0
- karrio_seko-2025.5rc1.dist-info/entry_points.txt +2 -0
- karrio_seko-2025.5rc1.dist-info/top_level.txt +3 -0
@@ -0,0 +1,224 @@
|
|
1
|
+
"""Karrio SEKO Logistics shipment API implementation."""
|
2
|
+
|
3
|
+
import karrio.schemas.seko.shipping_request as seko
|
4
|
+
import karrio.schemas.seko.shipping_response as shipping
|
5
|
+
|
6
|
+
import typing
|
7
|
+
import karrio.lib as lib
|
8
|
+
import karrio.core.units as units
|
9
|
+
import karrio.core.models as models
|
10
|
+
import karrio.providers.seko.error as error
|
11
|
+
import karrio.providers.seko.utils as provider_utils
|
12
|
+
import karrio.providers.seko.units as provider_units
|
13
|
+
|
14
|
+
|
15
|
+
def parse_shipment_response(
|
16
|
+
_response: lib.Deserializable[dict],
|
17
|
+
settings: provider_utils.Settings,
|
18
|
+
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
|
19
|
+
response = _response.deserialize()
|
20
|
+
print(_response.ctx)
|
21
|
+
messages = error.parse_error_response(response, settings)
|
22
|
+
shipment = lib.identity(
|
23
|
+
_extract_details(response, settings, ctx=_response.ctx)
|
24
|
+
if any(response.get("Consignments", []))
|
25
|
+
else None
|
26
|
+
)
|
27
|
+
|
28
|
+
return shipment, messages
|
29
|
+
|
30
|
+
|
31
|
+
def _extract_details(
|
32
|
+
data: dict,
|
33
|
+
settings: provider_utils.Settings,
|
34
|
+
ctx: dict = None,
|
35
|
+
) -> models.ShipmentDetails:
|
36
|
+
details = lib.to_object(shipping.ShippingResponseType, data)
|
37
|
+
Connotes = [_.Connote for _ in details.Consignments]
|
38
|
+
TrackingUrls = [_.TrackingUrl for _ in details.Consignments]
|
39
|
+
ConsignmentIds = [_.ConsignmentId for _ in details.Consignments]
|
40
|
+
label_type = ctx.get("label_type")
|
41
|
+
label_format = ctx.get("label_format")
|
42
|
+
|
43
|
+
label = lib.bundle_base64(
|
44
|
+
sum([_["OutputFiles"][label_type] for _ in data["Consignments"]], []),
|
45
|
+
label_format,
|
46
|
+
)
|
47
|
+
|
48
|
+
return models.ShipmentDetails(
|
49
|
+
carrier_id=settings.carrier_id,
|
50
|
+
carrier_name=settings.carrier_name,
|
51
|
+
tracking_number=Connotes[0],
|
52
|
+
shipment_identifier=ConsignmentIds[0],
|
53
|
+
label_type=label_format,
|
54
|
+
docs=models.Documents(label=label),
|
55
|
+
meta=dict(
|
56
|
+
carrier_tracking_link=TrackingUrls[0],
|
57
|
+
seko_site_id=details.SiteId,
|
58
|
+
tracking_urls=TrackingUrls,
|
59
|
+
consignment_id=ConsignmentIds[0],
|
60
|
+
consignment_ids=ConsignmentIds,
|
61
|
+
seko_carrier_id=details.CarrierId,
|
62
|
+
seko_carrier_name=details.CarrierName,
|
63
|
+
seko_carrier_type=details.CarrierType,
|
64
|
+
rate_provider=details.CarrierName,
|
65
|
+
seko_invoice_response=details.InvoiceResponse,
|
66
|
+
),
|
67
|
+
)
|
68
|
+
|
69
|
+
|
70
|
+
def shipment_request(
|
71
|
+
payload: models.ShipmentRequest,
|
72
|
+
settings: provider_utils.Settings,
|
73
|
+
) -> lib.Serializable:
|
74
|
+
shipper = lib.to_address(payload.shipper)
|
75
|
+
recipient = lib.to_address(payload.recipient)
|
76
|
+
packages = lib.to_packages(payload.parcels)
|
77
|
+
service = provider_units.ShippingService.map(payload.service).value_or_key
|
78
|
+
options = lib.to_shipping_options(
|
79
|
+
payload.options,
|
80
|
+
package_options=packages.options,
|
81
|
+
initializer=provider_units.shipping_options_initializer,
|
82
|
+
)
|
83
|
+
customs = lib.to_customs_info(
|
84
|
+
payload.customs,
|
85
|
+
shipper=payload.shipper,
|
86
|
+
recipient=payload.recipient,
|
87
|
+
weight_unit=units.WeightUnit.KG.name,
|
88
|
+
option_type=provider_units.CustomsOption,
|
89
|
+
)
|
90
|
+
commodities: units.Products = lib.identity(
|
91
|
+
customs.commodities if payload.customs else packages.items
|
92
|
+
)
|
93
|
+
[label_format, label_type] = lib.identity(
|
94
|
+
provider_units.LabelType.map(payload.label_type).value
|
95
|
+
or provider_units.LabelType.PDF.value
|
96
|
+
)
|
97
|
+
commercial_invoice = lib.identity(
|
98
|
+
options.seko_invoice_data.state
|
99
|
+
or next((
|
100
|
+
_["doc_file"] for _ in options.doc_files.state or []
|
101
|
+
if _["doc_type"] == "commercial_invoice"
|
102
|
+
and _.get("doc_format", "PDF").lower() == "pdf"
|
103
|
+
), None)
|
104
|
+
)
|
105
|
+
|
106
|
+
# map data to convert karrio model to seko specific type
|
107
|
+
request = seko.ShippingRequestType(
|
108
|
+
DeliveryReference=payload.reference,
|
109
|
+
Reference2=options.seko_reference_2.state,
|
110
|
+
Reference3=options.seko_reference_3.state,
|
111
|
+
Origin=seko.DestinationType(
|
112
|
+
Id=options.seko_origin_id.state,
|
113
|
+
Name=lib.identity(shipper.company_name or shipper.contact or "Shipper"),
|
114
|
+
Address=seko.AddressType(
|
115
|
+
BuildingName=None,
|
116
|
+
StreetAddress=shipper.street,
|
117
|
+
Suburb=shipper.city,
|
118
|
+
City=shipper.state_code or shipper.city,
|
119
|
+
PostCode=shipper.postal_code,
|
120
|
+
CountryCode=shipper.country_code,
|
121
|
+
),
|
122
|
+
ContactPerson=shipper.contact,
|
123
|
+
PhoneNumber=shipper.phone_number or "000 000 0000",
|
124
|
+
Email=shipper.email or " ",
|
125
|
+
DeliveryInstructions=options.origin_instructions.state,
|
126
|
+
RecipientTaxId=shipper.tax_id,
|
127
|
+
SendTrackingEmail=None,
|
128
|
+
),
|
129
|
+
Destination=seko.DestinationType(
|
130
|
+
Id=options.seko_destination_id.state,
|
131
|
+
Name=lib.identity(
|
132
|
+
recipient.company_name or recipient.contact or "Recipient"
|
133
|
+
),
|
134
|
+
Address=seko.AddressType(
|
135
|
+
BuildingName=None,
|
136
|
+
StreetAddress=recipient.street,
|
137
|
+
Suburb=recipient.city,
|
138
|
+
City=recipient.state_code or recipient.city,
|
139
|
+
PostCode=recipient.postal_code,
|
140
|
+
CountryCode=recipient.country_code,
|
141
|
+
),
|
142
|
+
ContactPerson=recipient.contact,
|
143
|
+
PhoneNumber=recipient.phone_number or "000 000 0000",
|
144
|
+
Email=recipient.email or " ",
|
145
|
+
DeliveryInstructions=options.destination_instructions.state,
|
146
|
+
RecipientTaxId=recipient.tax_id,
|
147
|
+
SendTrackingEmail=options.seko_send_tracking_email.state,
|
148
|
+
),
|
149
|
+
DangerousGoods=None,
|
150
|
+
Commodities=[
|
151
|
+
seko.CommodityType(
|
152
|
+
Description=lib.identity(
|
153
|
+
lib.text(commodity.description or commodity.title, max=35) or "item"
|
154
|
+
),
|
155
|
+
HarmonizedCode=commodity.hs_code or "0000.00.00",
|
156
|
+
Units=commodity.quantity,
|
157
|
+
UnitValue=commodity.value_amount,
|
158
|
+
UnitKg=commodity.weight,
|
159
|
+
Currency=commodity.value_currency,
|
160
|
+
Country=commodity.origin_country,
|
161
|
+
IsDG=None,
|
162
|
+
itemSKU=commodity.sku,
|
163
|
+
DangerousGoodsItem=None,
|
164
|
+
)
|
165
|
+
for commodity in commodities
|
166
|
+
],
|
167
|
+
Packages=[
|
168
|
+
seko.PackageType(
|
169
|
+
Height=package.height.CM,
|
170
|
+
Length=package.length.CM,
|
171
|
+
Width=package.width.CM,
|
172
|
+
Kg=package.weight.KG,
|
173
|
+
Name=lib.text(package.description, max=50),
|
174
|
+
Type=lib.identity(
|
175
|
+
provider_units.PackagingType.map(package.packaging_type).value
|
176
|
+
),
|
177
|
+
OverLabelBarcode=package.reference_number,
|
178
|
+
)
|
179
|
+
for package in packages
|
180
|
+
],
|
181
|
+
issignaturerequired=options.seko_is_signature_required.state,
|
182
|
+
DutiesAndTaxesByReceiver=lib.identity(
|
183
|
+
customs.duty.paid_by == "recipient" if payload.customs else None
|
184
|
+
),
|
185
|
+
ProductCategory=options.seko_product_category.state,
|
186
|
+
ShipType=options.seko_ship_type.state,
|
187
|
+
PrintToPrinter=lib.identity(
|
188
|
+
options.seko_print_to_printer.state
|
189
|
+
if options.seko_print_to_printer.state is not None
|
190
|
+
else True
|
191
|
+
),
|
192
|
+
IncludeLineDetails=True,
|
193
|
+
Carrier=options.seko_carrier.state,
|
194
|
+
Service=service,
|
195
|
+
CostCentreName=settings.connection_config.cost_center.state,
|
196
|
+
CostCentreId=settings.connection_config.cost_center_id.state,
|
197
|
+
CodValue=options.cash_on_delivery.state,
|
198
|
+
TaxCollected=lib.identity(
|
199
|
+
options.seko_tax_collected.state
|
200
|
+
if options.seko_tax_collected.state is not None
|
201
|
+
else True
|
202
|
+
),
|
203
|
+
AmountCollected=lib.to_money(options.seko_amount_collected.state),
|
204
|
+
CIFValue=options.seko_cif_value.state,
|
205
|
+
FreightValue=options.seko_freight_value.state,
|
206
|
+
SendLabel="Y" if options.seko_send_label.state else None,
|
207
|
+
LabelBranding=settings.connection_config.label_branding.state,
|
208
|
+
InvoiceData=commercial_invoice,
|
209
|
+
TaxIds=[
|
210
|
+
seko.TaxIDType(
|
211
|
+
IdType=option.code,
|
212
|
+
IdNumber=option.state,
|
213
|
+
)
|
214
|
+
for key, option in customs.options.items()
|
215
|
+
if key in provider_units.CustomsOption and option.state is not None
|
216
|
+
],
|
217
|
+
Outputs=[label_type],
|
218
|
+
)
|
219
|
+
|
220
|
+
return lib.Serializable(
|
221
|
+
request,
|
222
|
+
lib.to_dict,
|
223
|
+
dict(label_type=label_type, label_format=label_format),
|
224
|
+
)
|
@@ -0,0 +1,95 @@
|
|
1
|
+
"""Karrio SEKO Logistics tracking API implementation."""
|
2
|
+
|
3
|
+
import karrio.schemas.seko.tracking_response as tracking
|
4
|
+
|
5
|
+
import typing
|
6
|
+
import karrio.lib as lib
|
7
|
+
import karrio.core.units as units
|
8
|
+
import karrio.core.models as models
|
9
|
+
import karrio.providers.seko.error as error
|
10
|
+
import karrio.providers.seko.utils as provider_utils
|
11
|
+
import karrio.providers.seko.units as provider_units
|
12
|
+
|
13
|
+
|
14
|
+
def parse_tracking_response(
|
15
|
+
_response: lib.Deserializable[typing.List[dict]],
|
16
|
+
settings: provider_utils.Settings,
|
17
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
18
|
+
responses = _response.deserialize()
|
19
|
+
|
20
|
+
messages: typing.List[models.Message] = error.parse_error_response(
|
21
|
+
responses, settings
|
22
|
+
)
|
23
|
+
tracking_details = [
|
24
|
+
_extract_details(_, settings)
|
25
|
+
for _ in (responses if isinstance(responses, list) else [responses])
|
26
|
+
if any(_.get("Events", []))
|
27
|
+
]
|
28
|
+
|
29
|
+
return tracking_details, messages
|
30
|
+
|
31
|
+
|
32
|
+
def _extract_details(
|
33
|
+
data: dict,
|
34
|
+
settings: provider_utils.Settings,
|
35
|
+
) -> models.TrackingDetails:
|
36
|
+
details = lib.to_object(tracking.TrackingResponseElementType, data)
|
37
|
+
events = list(reversed(details.Events))
|
38
|
+
latest_status = lib.identity(
|
39
|
+
events[0].OmniCode if any(events) else getattr(details, "Status", None)
|
40
|
+
)
|
41
|
+
status = next(
|
42
|
+
(
|
43
|
+
status.name
|
44
|
+
for status in list(provider_units.TrackingStatus)
|
45
|
+
if latest_status in status.value
|
46
|
+
),
|
47
|
+
provider_units.TrackingStatus.in_transit.name,
|
48
|
+
)
|
49
|
+
|
50
|
+
return models.TrackingDetails(
|
51
|
+
carrier_id=settings.carrier_id,
|
52
|
+
carrier_name=settings.carrier_name,
|
53
|
+
tracking_number=details.ConsignmentNo,
|
54
|
+
events=[
|
55
|
+
models.TrackingEvent(
|
56
|
+
date=lib.fdate(
|
57
|
+
event.EventDT,
|
58
|
+
try_formats=["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"],
|
59
|
+
),
|
60
|
+
description=event.Description,
|
61
|
+
code=event.OmniCode or event.Code,
|
62
|
+
time=lib.flocaltime(
|
63
|
+
event.EventDT,
|
64
|
+
try_formats=["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"],
|
65
|
+
),
|
66
|
+
location=event.Location,
|
67
|
+
)
|
68
|
+
for event in events
|
69
|
+
],
|
70
|
+
delivered=status == "delivered",
|
71
|
+
status=status,
|
72
|
+
info=models.TrackingInfo(
|
73
|
+
carrier_tracking_link=details.Tracking,
|
74
|
+
expected_delivery=lib.fdate(
|
75
|
+
details.Delivered,
|
76
|
+
try_formats=["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"],
|
77
|
+
),
|
78
|
+
shipping_date=lib.fdate(
|
79
|
+
details.Picked,
|
80
|
+
try_formats=["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"],
|
81
|
+
),
|
82
|
+
),
|
83
|
+
meta=dict(reference=details.Reference1),
|
84
|
+
)
|
85
|
+
|
86
|
+
|
87
|
+
def tracking_request(
|
88
|
+
payload: models.TrackingRequest,
|
89
|
+
settings: provider_utils.Settings,
|
90
|
+
) -> lib.Serializable:
|
91
|
+
|
92
|
+
# map data to convert karrio model to seko specific type
|
93
|
+
request = payload.tracking_numbers
|
94
|
+
|
95
|
+
return lib.Serializable(request, lib.to_dict)
|
@@ -0,0 +1,239 @@
|
|
1
|
+
import karrio.lib as lib
|
2
|
+
import karrio.core.units as units
|
3
|
+
|
4
|
+
|
5
|
+
class LabelType(lib.Enum):
|
6
|
+
LABEL_PDF = ("PDF", "LABEL_PDF")
|
7
|
+
LABEL_PNG_100X150 = ("PNG", "LABEL_PNG_100X150")
|
8
|
+
LABEL_PNG_100X175 = ("PNG", "LABEL_PNG_100X175")
|
9
|
+
LABEL_PDF_100X175 = ("PDF", "LABEL_PDF_100X175")
|
10
|
+
LABEL_PDF_100X150 = ("PDF", "LABEL_PDF_100X150")
|
11
|
+
LABEL_ZPL_100X175 = ("ZPL", "LABEL_ZPL_100X175")
|
12
|
+
LABEL_ZPL_100X150 = ("ZPL", "LABEL_ZPL_100X150")
|
13
|
+
|
14
|
+
""" Unified Label type mapping """
|
15
|
+
PDF = LABEL_PDF_100X150
|
16
|
+
ZPL = LABEL_ZPL_100X150
|
17
|
+
PNG = LABEL_PNG_100X150
|
18
|
+
|
19
|
+
|
20
|
+
class PackagingType(lib.StrEnum):
|
21
|
+
"""Carrier specific packaging type"""
|
22
|
+
|
23
|
+
Bag = "Bag"
|
24
|
+
Box = "Box"
|
25
|
+
Carton = "Carton"
|
26
|
+
Container = "Container"
|
27
|
+
Crate = "Crate"
|
28
|
+
Envelope = "Envelope"
|
29
|
+
Pail = "Pail"
|
30
|
+
Pallet = "Pallet"
|
31
|
+
Satchel = "Satchel"
|
32
|
+
Tube = "Tube"
|
33
|
+
Custom = "Custom"
|
34
|
+
|
35
|
+
""" Unified Packaging type mapping """
|
36
|
+
envelope = Envelope
|
37
|
+
pak = Satchel
|
38
|
+
tube = Tube
|
39
|
+
pallet = Pallet
|
40
|
+
small_box = Box
|
41
|
+
medium_box = Carton
|
42
|
+
your_packaging = Custom
|
43
|
+
|
44
|
+
|
45
|
+
class ShippingService(lib.StrEnum):
|
46
|
+
"""Carrier specific services"""
|
47
|
+
|
48
|
+
seko_ecommerce_standard_tracked = "eCommerce Standard Tracked"
|
49
|
+
seko_ecommerce_express_tracked = "eCommerce Express Tracked"
|
50
|
+
seko_domestic_express = "Domestic Express"
|
51
|
+
seko_domestic_standard = "Domestic Standard"
|
52
|
+
seko_domestic_large_parcel = "Domestic Large Parcel"
|
53
|
+
|
54
|
+
|
55
|
+
class ShippingOption(lib.Enum):
|
56
|
+
"""Carrier specific options"""
|
57
|
+
|
58
|
+
seko_carrier = lib.OptionEnum("Carrier")
|
59
|
+
seko_ship_type = lib.OptionEnum("ShipType")
|
60
|
+
seko_package_id = lib.OptionEnum("PackageId")
|
61
|
+
seko_destination_id = lib.OptionEnum("DestinationId")
|
62
|
+
seko_product_category = lib.OptionEnum("ProductCategory")
|
63
|
+
origin_instructions = lib.OptionEnum("OriginInstructions")
|
64
|
+
destination_instructions = lib.OptionEnum("DestinationInstructions")
|
65
|
+
seko_is_saturday_delivery = lib.OptionEnum("IsSaturdayDelivery", bool)
|
66
|
+
seko_is_signature_required = lib.OptionEnum("IsSignatureRequired", bool)
|
67
|
+
seko_send_tracking_email = lib.OptionEnum("SendTrackingEmail", bool)
|
68
|
+
seko_amount_collected = lib.OptionEnum("AmountCollected", float)
|
69
|
+
seko_tax_collected = lib.OptionEnum("TaxCollected", bool)
|
70
|
+
seko_cod_amount = lib.OptionEnum("CODAmount", float)
|
71
|
+
seko_reference_2 = lib.OptionEnum("Reference2")
|
72
|
+
seko_reference_3 = lib.OptionEnum("Reference3")
|
73
|
+
seko_invoice_data = lib.OptionEnum("InvoiceData")
|
74
|
+
seko_origin_id = lib.OptionEnum("OriginId", int)
|
75
|
+
seko_print_to_printer = lib.OptionEnum("PrintToPrinter", bool)
|
76
|
+
seko_cif_value = lib.OptionEnum("CIFValue", float)
|
77
|
+
seko_freight_value = lib.OptionEnum("FreightValue", float)
|
78
|
+
seko_send_label = lib.OptionEnum("SendLabel", bool)
|
79
|
+
|
80
|
+
""" Unified Option type mapping """
|
81
|
+
saturday_delivery = seko_is_saturday_delivery
|
82
|
+
signature_required = seko_is_signature_required
|
83
|
+
email_notification = seko_send_tracking_email
|
84
|
+
doc_files = lib.OptionEnum("doc_files", lib.to_dict)
|
85
|
+
doc_references = lib.OptionEnum("doc_references", lib.to_dict)
|
86
|
+
|
87
|
+
|
88
|
+
def shipping_options_initializer(
|
89
|
+
options: dict,
|
90
|
+
package_options: units.ShippingOptions = None,
|
91
|
+
) -> units.ShippingOptions:
|
92
|
+
"""
|
93
|
+
Apply default values to the given options.
|
94
|
+
"""
|
95
|
+
|
96
|
+
if package_options is not None:
|
97
|
+
options.update(package_options.content)
|
98
|
+
|
99
|
+
def items_filter(key: str) -> bool:
|
100
|
+
return key in ShippingOption # type: ignore
|
101
|
+
|
102
|
+
return units.ShippingOptions(options, ShippingOption, items_filter=items_filter)
|
103
|
+
|
104
|
+
|
105
|
+
class CustomsOption(lib.Enum):
|
106
|
+
XIEORINumber = lib.OptionEnum("XIEORINumber")
|
107
|
+
IOSSNUMBER = lib.OptionEnum("IOSSNUMBER")
|
108
|
+
GBEORINUMBER = lib.OptionEnum("GBEORINUMBER")
|
109
|
+
VOECNUMBER = lib.OptionEnum("VOECNUMBER")
|
110
|
+
VATNUMBER = lib.OptionEnum("VATNUMBER")
|
111
|
+
VENDORID = lib.OptionEnum("VENDORID")
|
112
|
+
NZIRDNUMBER = lib.OptionEnum("NZIRDNUMBER")
|
113
|
+
SWISS_VAT = lib.OptionEnum("SWISS VAT")
|
114
|
+
OVRNUMBER = lib.OptionEnum("OVRNUMBER")
|
115
|
+
EUEORINumber = lib.OptionEnum("EUEORINumber")
|
116
|
+
EUVATNumber = lib.OptionEnum("EUVATNumber")
|
117
|
+
LVGRegistrationNumber = lib.OptionEnum("LVGRegistrationNumber")
|
118
|
+
|
119
|
+
""" Unified Customs Identifier type mapping """
|
120
|
+
|
121
|
+
ioss = IOSSNUMBER
|
122
|
+
nip_number = VATNUMBER
|
123
|
+
eori_number = EUEORINumber
|
124
|
+
|
125
|
+
|
126
|
+
class TrackingStatus(lib.Enum):
|
127
|
+
pending = [
|
128
|
+
"OP-1", # Pending
|
129
|
+
"OP-8", # Manifest Received by Carrier
|
130
|
+
"OP-9", # Not yet received by carrier
|
131
|
+
"OP-11", # Received by carrier – no manifest sent
|
132
|
+
"OP-12", # Collection request received by carrier
|
133
|
+
]
|
134
|
+
on_hold = [
|
135
|
+
"OP-26", # Held by carrier
|
136
|
+
"OP-2", # Held at Export Hub
|
137
|
+
"OP-6", # Customs held for inspection and clearance
|
138
|
+
"OP-49", # Held by Delivery Courier
|
139
|
+
"OP-70", # Parcel Blocked
|
140
|
+
"OP-87", # Aged Parcel - High Value Unpaid
|
141
|
+
"OP-88", # Held at Export Hub - Additional Payment Required
|
142
|
+
"OP-41", # Incorrect details declared by sender
|
143
|
+
"OP-36", # Delivery arranged with receiver
|
144
|
+
"OP-39", # Package repacked
|
145
|
+
"OP-44", # Selected for redelivery
|
146
|
+
"OP-46", # Customer Enquiry lodged
|
147
|
+
"OP-52", # Parcel Redirection Requested
|
148
|
+
"OP-53", # Parcel Redirected
|
149
|
+
"OP-91", # Parcel Blocked - Declared LIT
|
150
|
+
]
|
151
|
+
delivered = [
|
152
|
+
"OP-71", # Delivered in part
|
153
|
+
"OP-72", # Delivered
|
154
|
+
"OP-73", # Delivered to neighbour
|
155
|
+
"OP-74", # Delivered - Authority to Leave / Safe Drop
|
156
|
+
"OP-75", # Delivered - Parcel Collected
|
157
|
+
"OP-76", # Delivered to locker/collection point
|
158
|
+
"OP-77", # Delivered to alternate delivery point
|
159
|
+
]
|
160
|
+
in_transit = [
|
161
|
+
"OP-18", # In transit
|
162
|
+
"OP-20", # Sub-contractor update
|
163
|
+
"OP-22", # Received by Sub-contractor
|
164
|
+
"OP-3", # Processed through Export Hub
|
165
|
+
"OP-4", # International transit to destination country
|
166
|
+
"OP-5", # Customs cleared
|
167
|
+
"OP-47", # Processed through Sorting Facility
|
168
|
+
"OP-50", # Parcel arrived to courier processing facility
|
169
|
+
"OP-51", # Parcel departed courier processing facility
|
170
|
+
"OP-78", # Flight Arrived
|
171
|
+
"OP-79", # InTransit
|
172
|
+
"OP-80", # Reshipped
|
173
|
+
"OP-81", # Flight Departed
|
174
|
+
"OP-7", # Picked up by Delivery Courier
|
175
|
+
"OP-10", # Received by carrier
|
176
|
+
"OP-14", # Parcel received and accepted
|
177
|
+
"OP-31", # Information
|
178
|
+
"OP-32", # Information
|
179
|
+
"OP-33", # Collected from sender
|
180
|
+
"OP-48", # With Delivery Courier
|
181
|
+
"OP-82", # Inbound freight received
|
182
|
+
"OP-83", # Delivery exception
|
183
|
+
"OP-84", # Recipient not available
|
184
|
+
"OP-89", # Collected from sender
|
185
|
+
"OP-54", # Transferred to Collection Point
|
186
|
+
"OP-56", # Transferred to delivery provider
|
187
|
+
]
|
188
|
+
delivery_failed = [
|
189
|
+
"OP-24", # Attempted Delivery - Receiver carded
|
190
|
+
"OP-27", # Attempted Delivery - Customer not known at address
|
191
|
+
"OP-28", # Attempted Delivery - Refused by customer
|
192
|
+
"OP-29", # Return to sender
|
193
|
+
"OP-30", # Non delivery
|
194
|
+
"OP-37", # Attempted Delivery - No access to receivers address
|
195
|
+
"OP-38", # Attempted Delivery - Customer Identification failed
|
196
|
+
"OP-45", # Attempted delivery
|
197
|
+
"OP-55", # Attempted Delivery - Returned to Sender
|
198
|
+
"OP-15", # Parcel lost
|
199
|
+
"OP-17", # Parcel Damaged
|
200
|
+
"OP-23", # Invalid / Insufficient Address
|
201
|
+
"OP-86", # Attempted delivery
|
202
|
+
"OP-40", # Package disposed
|
203
|
+
"OP-92", # Amazon RTS - DESTROY
|
204
|
+
"OP-43", # Not collected from store
|
205
|
+
]
|
206
|
+
delivery_delayed = [
|
207
|
+
"OP-16", # Parcel Delayed
|
208
|
+
"OP-13", # Misdirected
|
209
|
+
"OP-35", # Mis sorted by carrier
|
210
|
+
]
|
211
|
+
out_for_delivery = [
|
212
|
+
"OP-21", # Out for delivery
|
213
|
+
]
|
214
|
+
ready_for_pickup = [
|
215
|
+
"OP-19", # Awaiting Collection
|
216
|
+
"OP-25", # Customer to collect from carrier
|
217
|
+
"OP-42", # Awaiting collection
|
218
|
+
]
|
219
|
+
cancelled = [
|
220
|
+
"OP-34", # Cancelled
|
221
|
+
"OP-67", # RTS - Cancelled Order
|
222
|
+
]
|
223
|
+
return_to_sender = [
|
224
|
+
"OP-57", # RTS Received - Authorised Return
|
225
|
+
"OP-58", # RTS Received - Cancelled Order
|
226
|
+
"OP-59", # RTS Received - Card Left, Never Collected
|
227
|
+
"OP-60", # RTS - Fraudulant
|
228
|
+
"OP-61", # RTS Received - Invalid or Insufficient Address
|
229
|
+
"OP-62", # RTS Received- No Reason Given
|
230
|
+
"OP-63", # RTS - High Value Rejected
|
231
|
+
"OP-64", # RTS Received - Refused
|
232
|
+
"OP-65", # RTS Received - Unclaimed
|
233
|
+
"OP-66", # Return to Sender
|
234
|
+
"OP-68", # RTS Received
|
235
|
+
"OP-69", # RTS Received - Damaged Parcel
|
236
|
+
"OP-85", # RTS - In transit
|
237
|
+
"OP-90", # Return processed
|
238
|
+
"OP-94", # RTS consolidated
|
239
|
+
]
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import base64
|
2
|
+
import datetime
|
3
|
+
import karrio.lib as lib
|
4
|
+
import karrio.core as core
|
5
|
+
import karrio.core.errors as errors
|
6
|
+
|
7
|
+
|
8
|
+
class Settings(core.Settings):
|
9
|
+
"""SEKO Logistics connection settings."""
|
10
|
+
|
11
|
+
# Add carrier specific api connection properties here
|
12
|
+
access_key: str
|
13
|
+
|
14
|
+
@property
|
15
|
+
def carrier_name(self):
|
16
|
+
return "seko"
|
17
|
+
|
18
|
+
@property
|
19
|
+
def server_url(self):
|
20
|
+
return (
|
21
|
+
"https://staging.omniparcel.com"
|
22
|
+
if self.test_mode
|
23
|
+
else "https://api.omniparcel.com"
|
24
|
+
)
|
25
|
+
|
26
|
+
# """uncomment the following code block to expose a carrier tracking url."""
|
27
|
+
# @property
|
28
|
+
# def tracking_url(self):
|
29
|
+
# return "https://www.carrier.com/tracking?tracking-id={}"
|
30
|
+
|
31
|
+
# """uncomment the following code block to implement the Basic auth."""
|
32
|
+
# @property
|
33
|
+
# def authorization(self):
|
34
|
+
# pair = "%s:%s" % (self.username, self.password)
|
35
|
+
# return base64.b64encode(pair.encode("utf-8")).decode("ascii")
|
36
|
+
|
37
|
+
@property
|
38
|
+
def connection_config(self) -> lib.units.Options:
|
39
|
+
return lib.to_connection_config(
|
40
|
+
self.config or {},
|
41
|
+
option_type=ConnectionConfig,
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
# """uncomment the following code block to implement the oauth login."""
|
46
|
+
# @property
|
47
|
+
# def access_token(self):
|
48
|
+
# """Retrieve the access_token using the client_id|client_secret pair
|
49
|
+
# or collect it from the cache if an unexpired access_token exist.
|
50
|
+
# """
|
51
|
+
# cache_key = f"{self.carrier_name}|{self.client_id}|{self.client_secret}"
|
52
|
+
# now = datetime.datetime.now() + datetime.timedelta(minutes=30)
|
53
|
+
|
54
|
+
# auth = self.connection_cache.get(cache_key) or {}
|
55
|
+
# token = auth.get("access_token")
|
56
|
+
# expiry = lib.to_date(auth.get("expiry"), current_format="%Y-%m-%d %H:%M:%S")
|
57
|
+
|
58
|
+
# if token is not None and expiry is not None and expiry > now:
|
59
|
+
# return token
|
60
|
+
|
61
|
+
# self.connection_cache.set(cache_key, lambda: login(self))
|
62
|
+
# new_auth = self.connection_cache.get(cache_key)
|
63
|
+
|
64
|
+
# return new_auth["access_token"]
|
65
|
+
|
66
|
+
# """uncomment the following code block to implement the oauth login."""
|
67
|
+
# def login(settings: Settings):
|
68
|
+
# import karrio.providers.seko.error as error
|
69
|
+
|
70
|
+
# result = lib.request(
|
71
|
+
# url=f"{settings.server_url}/oauth/token",
|
72
|
+
# method="POST",
|
73
|
+
# headers={"content-Type": "application/x-www-form-urlencoded"},
|
74
|
+
# data=lib.to_query_string(
|
75
|
+
# dict(
|
76
|
+
# grant_type="client_credentials",
|
77
|
+
# client_id=settings.client_id,
|
78
|
+
# client_secret=settings.client_secret,
|
79
|
+
# )
|
80
|
+
# ),
|
81
|
+
# )
|
82
|
+
|
83
|
+
# response = lib.to_dict(result)
|
84
|
+
# messages = error.parse_error_response(response, settings)
|
85
|
+
|
86
|
+
# if any(messages):
|
87
|
+
# raise errors.ParsedMessagesError(messages)
|
88
|
+
|
89
|
+
# expiry = datetime.datetime.now() + datetime.timedelta(
|
90
|
+
# seconds=float(response.get("expires_in", 0))
|
91
|
+
# )
|
92
|
+
# return {**response, "expiry": lib.fdatetime(expiry)}
|
93
|
+
|
94
|
+
|
95
|
+
class ConnectionConfig(lib.Enum):
|
96
|
+
"""SEKO Logistics connection configuration."""
|
97
|
+
|
98
|
+
currency = lib.OptionEnum("currency", str)
|
99
|
+
cost_center = lib.OptionEnum("cost_center", str)
|
100
|
+
cost_center_id = lib.OptionEnum("cost_center_id", str)
|
101
|
+
shipping_options = lib.OptionEnum("shipping_options", list)
|
102
|
+
shipping_services = lib.OptionEnum("shipping_services", list)
|
103
|
+
|
104
|
+
|
105
|
+
def parse_error_response(response):
|
106
|
+
"""Parse the error response from the SAPIENT API."""
|
107
|
+
content = lib.failsafe(lambda: lib.decode(response.read()))
|
108
|
+
|
109
|
+
if any(content or ""):
|
110
|
+
return content
|
111
|
+
|
112
|
+
return lib.to_json(
|
113
|
+
dict(Errors=[dict(code=str(response.code), Message=response.reason)])
|
114
|
+
)
|
File without changes
|