karrio-parcelone 2026.1__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/parcelone/__init__.py +3 -0
- karrio/mappers/parcelone/mapper.py +50 -0
- karrio/mappers/parcelone/proxy.py +84 -0
- karrio/mappers/parcelone/settings.py +36 -0
- karrio/plugins/parcelone/__init__.py +28 -0
- karrio/providers/parcelone/__init__.py +17 -0
- karrio/providers/parcelone/error.py +76 -0
- karrio/providers/parcelone/rate.py +162 -0
- karrio/providers/parcelone/shipment/__init__.py +17 -0
- karrio/providers/parcelone/shipment/cancel.py +60 -0
- karrio/providers/parcelone/shipment/create.py +250 -0
- karrio/providers/parcelone/tracking.py +141 -0
- karrio/providers/parcelone/units.py +326 -0
- karrio/providers/parcelone/utils.py +63 -0
- karrio/schemas/parcelone/__init__.py +6 -0
- karrio/schemas/parcelone/error.py +27 -0
- karrio/schemas/parcelone/shipping_request.py +202 -0
- karrio/schemas/parcelone/shipping_response.py +171 -0
- karrio/schemas/parcelone/tracking_response.py +45 -0
- karrio_parcelone-2026.1.dist-info/METADATA +47 -0
- karrio_parcelone-2026.1.dist-info/RECORD +26 -0
- karrio_parcelone-2026.1.dist-info/WHEEL +5 -0
- karrio_parcelone-2026.1.dist-info/entry_points.txt +2 -0
- karrio_parcelone-2026.1.dist-info/top_level.txt +5 -0
- modules/connectors/parcelone/karrio/schemas/__init__.py +0 -0
- modules/connectors/parcelone/karrio/schemas/parcelone/__init__.py +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Karrio ParcelOne client mapper."""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
import karrio.lib as lib
|
|
5
|
+
import karrio.api.mapper as mapper
|
|
6
|
+
import karrio.core.models as models
|
|
7
|
+
import karrio.providers.parcelone as provider
|
|
8
|
+
import karrio.mappers.parcelone.settings as provider_settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Mapper(mapper.Mapper):
|
|
12
|
+
settings: provider_settings.Settings
|
|
13
|
+
|
|
14
|
+
def create_rate_request(self, payload: models.RateRequest) -> lib.Serializable:
|
|
15
|
+
return provider.rate_request(payload, self.settings)
|
|
16
|
+
|
|
17
|
+
def create_tracking_request(
|
|
18
|
+
self, payload: models.TrackingRequest
|
|
19
|
+
) -> lib.Serializable:
|
|
20
|
+
return provider.tracking_request(payload, self.settings)
|
|
21
|
+
|
|
22
|
+
def create_shipment_request(
|
|
23
|
+
self, payload: models.ShipmentRequest
|
|
24
|
+
) -> lib.Serializable:
|
|
25
|
+
return provider.shipment_request(payload, self.settings)
|
|
26
|
+
|
|
27
|
+
def create_cancel_shipment_request(
|
|
28
|
+
self, payload: models.ShipmentCancelRequest
|
|
29
|
+
) -> lib.Serializable:
|
|
30
|
+
return provider.shipment_cancel_request(payload, self.settings)
|
|
31
|
+
|
|
32
|
+
def parse_rate_response(
|
|
33
|
+
self, response: lib.Deserializable[str]
|
|
34
|
+
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
|
|
35
|
+
return provider.parse_rate_response(response, self.settings)
|
|
36
|
+
|
|
37
|
+
def parse_cancel_shipment_response(
|
|
38
|
+
self, response: lib.Deserializable[str]
|
|
39
|
+
) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
|
|
40
|
+
return provider.parse_shipment_cancel_response(response, self.settings)
|
|
41
|
+
|
|
42
|
+
def parse_shipment_response(
|
|
43
|
+
self, response: lib.Deserializable[str]
|
|
44
|
+
) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
|
|
45
|
+
return provider.parse_shipment_response(response, self.settings)
|
|
46
|
+
|
|
47
|
+
def parse_tracking_response(
|
|
48
|
+
self, response: lib.Deserializable[str]
|
|
49
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
|
50
|
+
return provider.parse_tracking_response(response, self.settings)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Karrio ParcelOne REST API client proxy."""
|
|
2
|
+
|
|
3
|
+
import karrio.lib as lib
|
|
4
|
+
import karrio.api.proxy as proxy
|
|
5
|
+
import karrio.mappers.parcelone.settings as provider_settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Proxy(proxy.Proxy):
|
|
9
|
+
settings: provider_settings.Settings
|
|
10
|
+
|
|
11
|
+
def get_rates(self, request: lib.Serializable) -> lib.Deserializable[dict]:
|
|
12
|
+
"""Get shipping rates - not directly supported by ParcelOne API.
|
|
13
|
+
|
|
14
|
+
ParcelOne returns charges after shipment creation.
|
|
15
|
+
This method creates a shipment with ReturnCharges=1 to get rates.
|
|
16
|
+
"""
|
|
17
|
+
response = lib.request(
|
|
18
|
+
url=f"{self.settings.server_url}/shipment",
|
|
19
|
+
data=lib.to_json(request.serialize()),
|
|
20
|
+
trace=self.trace_as("json"),
|
|
21
|
+
method="POST",
|
|
22
|
+
headers={
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
"Authorization": self.settings.authorization,
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return lib.Deserializable(response, lib.to_dict, request.ctx)
|
|
29
|
+
|
|
30
|
+
def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[dict]:
|
|
31
|
+
"""Create a shipment and get label."""
|
|
32
|
+
response = lib.request(
|
|
33
|
+
url=f"{self.settings.server_url}/shipment",
|
|
34
|
+
data=lib.to_json(request.serialize()),
|
|
35
|
+
trace=self.trace_as("json"),
|
|
36
|
+
method="POST",
|
|
37
|
+
headers={
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
"Authorization": self.settings.authorization,
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return lib.Deserializable(response, lib.to_dict, request.ctx)
|
|
44
|
+
|
|
45
|
+
def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable[dict]:
|
|
46
|
+
"""Cancel a shipment."""
|
|
47
|
+
data = request.serialize()
|
|
48
|
+
ref_field = data.get("ref_field", "ShipmentID")
|
|
49
|
+
ref_value = data.get("ref_value")
|
|
50
|
+
|
|
51
|
+
response = lib.request(
|
|
52
|
+
url=f"{self.settings.server_url}/shipment/{ref_field}/{ref_value}",
|
|
53
|
+
trace=self.trace_as("json"),
|
|
54
|
+
method="DELETE",
|
|
55
|
+
headers={
|
|
56
|
+
"Authorization": self.settings.authorization,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return lib.Deserializable(response, lib.to_dict)
|
|
61
|
+
|
|
62
|
+
def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[list]:
|
|
63
|
+
"""Get tracking information for multiple tracking numbers."""
|
|
64
|
+
tracking_requests = request.serialize()
|
|
65
|
+
|
|
66
|
+
# Make individual requests for each tracking number
|
|
67
|
+
responses = [
|
|
68
|
+
(
|
|
69
|
+
req["tracking_id"],
|
|
70
|
+
lib.to_dict(
|
|
71
|
+
lib.request(
|
|
72
|
+
url=f"{self.settings.tracking_url}/tracking/{req['carrier_id']}/{req['tracking_id']}",
|
|
73
|
+
trace=self.trace_as("json"),
|
|
74
|
+
method="GET",
|
|
75
|
+
headers={
|
|
76
|
+
"Authorization": self.settings.authorization,
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
for req in tracking_requests
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
return lib.Deserializable(responses, lambda x: x)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Karrio ParcelOne client settings."""
|
|
2
|
+
|
|
3
|
+
import attr
|
|
4
|
+
import typing
|
|
5
|
+
import jstruct
|
|
6
|
+
import karrio.core.models as models
|
|
7
|
+
import karrio.providers.parcelone.units as provider_units
|
|
8
|
+
import karrio.providers.parcelone.utils as provider_utils
|
|
9
|
+
import karrio.universal.mappers.rating_proxy as rating_proxy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@attr.s(auto_attribs=True)
|
|
13
|
+
class Settings(provider_utils.Settings, rating_proxy.RatingMixinSettings):
|
|
14
|
+
"""ParcelOne connection settings."""
|
|
15
|
+
|
|
16
|
+
# Required credentials
|
|
17
|
+
username: str
|
|
18
|
+
password: str
|
|
19
|
+
mandator_id: str
|
|
20
|
+
consigner_id: str
|
|
21
|
+
|
|
22
|
+
# Generic properties
|
|
23
|
+
id: str = None
|
|
24
|
+
test_mode: bool = False
|
|
25
|
+
carrier_id: str = "parcelone"
|
|
26
|
+
services: typing.List[models.ServiceLevel] = jstruct.JList[models.ServiceLevel, False, dict(default=provider_units.DEFAULT_SERVICES)] # type: ignore
|
|
27
|
+
account_country_code: str = "DE"
|
|
28
|
+
metadata: dict = {}
|
|
29
|
+
config: dict = {}
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def shipping_services(self) -> typing.List[models.ServiceLevel]:
|
|
33
|
+
if any(self.services or []):
|
|
34
|
+
return self.services
|
|
35
|
+
|
|
36
|
+
return provider_units.DEFAULT_SERVICES
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from karrio.core.metadata import PluginMetadata
|
|
2
|
+
|
|
3
|
+
from karrio.mappers.parcelone.mapper import Mapper
|
|
4
|
+
from karrio.mappers.parcelone.proxy import Proxy
|
|
5
|
+
from karrio.mappers.parcelone.settings import Settings
|
|
6
|
+
import karrio.providers.parcelone.units as units
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# This METADATA object is used by Karrio to discover and register this plugin
|
|
10
|
+
# when loaded through Python entrypoints or local plugin directories.
|
|
11
|
+
# The entrypoint is defined in pyproject.toml under [project.entry-points."karrio.plugins"]
|
|
12
|
+
METADATA = PluginMetadata(
|
|
13
|
+
id="parcelone",
|
|
14
|
+
label="ParcelOne",
|
|
15
|
+
description="ParcelOne multi-carrier shipping integration for Karrio",
|
|
16
|
+
# Integrations
|
|
17
|
+
Mapper=Mapper,
|
|
18
|
+
Proxy=Proxy,
|
|
19
|
+
Settings=Settings,
|
|
20
|
+
# Data Units
|
|
21
|
+
is_hub=True, # ParcelOne is a multi-carrier hub
|
|
22
|
+
options=units.ShippingOption,
|
|
23
|
+
services=units.ShippingService,
|
|
24
|
+
connection_configs=units.ConnectionConfig,
|
|
25
|
+
# Extra info
|
|
26
|
+
website="https://parcel.one",
|
|
27
|
+
documentation="https://parcel.one/api-documentation",
|
|
28
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Karrio ParcelOne provider imports."""
|
|
2
|
+
|
|
3
|
+
from karrio.providers.parcelone.utils import Settings
|
|
4
|
+
from karrio.providers.parcelone.rate import (
|
|
5
|
+
parse_rate_response,
|
|
6
|
+
rate_request,
|
|
7
|
+
)
|
|
8
|
+
from karrio.providers.parcelone.tracking import (
|
|
9
|
+
parse_tracking_response,
|
|
10
|
+
tracking_request,
|
|
11
|
+
)
|
|
12
|
+
from karrio.providers.parcelone.shipment import (
|
|
13
|
+
parse_shipment_cancel_response,
|
|
14
|
+
parse_shipment_response,
|
|
15
|
+
shipment_cancel_request,
|
|
16
|
+
shipment_request,
|
|
17
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Karrio ParcelOne error parser."""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
import karrio.lib as lib
|
|
5
|
+
import karrio.core.models as models
|
|
6
|
+
import karrio.providers.parcelone.utils as provider_utils
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_error_response(
|
|
10
|
+
response: dict,
|
|
11
|
+
settings: provider_utils.Settings,
|
|
12
|
+
**details,
|
|
13
|
+
) -> typing.List[models.Message]:
|
|
14
|
+
"""Parse error response from ParcelOne REST API."""
|
|
15
|
+
results = response.get("results") or {}
|
|
16
|
+
action_result = results.get("ActionResult") or results
|
|
17
|
+
|
|
18
|
+
# Collect all errors using list comprehensions
|
|
19
|
+
errors: typing.List[dict] = sum(
|
|
20
|
+
[
|
|
21
|
+
# Top-level API errors
|
|
22
|
+
[
|
|
23
|
+
*[
|
|
24
|
+
{
|
|
25
|
+
"code": e.get("ErrorNo") or e.get("StatusCode"),
|
|
26
|
+
"message": e.get("Message"),
|
|
27
|
+
"instance": response.get("instance"),
|
|
28
|
+
"uniq_id": response.get("UniqId"),
|
|
29
|
+
}
|
|
30
|
+
for e in (response.get("errors") or [])
|
|
31
|
+
],
|
|
32
|
+
# Top-level error message when no specific errors
|
|
33
|
+
*(
|
|
34
|
+
[
|
|
35
|
+
{
|
|
36
|
+
"code": str(response.get("status", "")),
|
|
37
|
+
"message": response.get("message"),
|
|
38
|
+
"instance": response.get("instance"),
|
|
39
|
+
"uniq_id": response.get("UniqId"),
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
if (
|
|
43
|
+
response.get("success") == -1
|
|
44
|
+
or response.get("type") == "error"
|
|
45
|
+
)
|
|
46
|
+
and not response.get("errors")
|
|
47
|
+
and response.get("message")
|
|
48
|
+
else []
|
|
49
|
+
),
|
|
50
|
+
],
|
|
51
|
+
# ActionResult errors
|
|
52
|
+
[
|
|
53
|
+
{
|
|
54
|
+
"code": e.get("ErrorNo") or e.get("StatusCode"),
|
|
55
|
+
"message": e.get("Message"),
|
|
56
|
+
"shipment_id": action_result.get("ShipmentID"),
|
|
57
|
+
"tracking_id": action_result.get("TrackingID"),
|
|
58
|
+
}
|
|
59
|
+
for e in (action_result.get("Errors") or [])
|
|
60
|
+
if isinstance(action_result, dict)
|
|
61
|
+
],
|
|
62
|
+
],
|
|
63
|
+
[],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
models.Message(
|
|
68
|
+
carrier_id=settings.carrier_id,
|
|
69
|
+
carrier_name=settings.carrier_name,
|
|
70
|
+
code=error.get("code"),
|
|
71
|
+
message=error.get("message"),
|
|
72
|
+
details=lib.to_dict({**details, **{k: v for k, v in error.items() if k not in ("code", "message")}}),
|
|
73
|
+
)
|
|
74
|
+
for error in errors
|
|
75
|
+
if error.get("code") or error.get("message")
|
|
76
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Karrio ParcelOne rating implementation.
|
|
2
|
+
|
|
3
|
+
Note: ParcelOne REST API returns charges as part of the shipment creation response.
|
|
4
|
+
This implementation creates a rate request that can be used to get charges
|
|
5
|
+
by setting ReturnCharges=1 on the shipment request.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import typing
|
|
9
|
+
import karrio.schemas.parcelone as parcelone
|
|
10
|
+
import karrio.lib as lib
|
|
11
|
+
import karrio.core.models as models
|
|
12
|
+
import karrio.providers.parcelone.error as error
|
|
13
|
+
import karrio.providers.parcelone.utils as provider_utils
|
|
14
|
+
import karrio.providers.parcelone.units as provider_units
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_rate_response(
|
|
18
|
+
_response: lib.Deserializable[dict],
|
|
19
|
+
settings: provider_utils.Settings,
|
|
20
|
+
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
|
|
21
|
+
"""Parse rate response from ParcelOne REST API."""
|
|
22
|
+
response = _response.deserialize()
|
|
23
|
+
messages = error.parse_error_response(response, settings)
|
|
24
|
+
|
|
25
|
+
rates = lib.identity(
|
|
26
|
+
[_extract_rate(response, settings, _response.ctx)]
|
|
27
|
+
if response.get("success") == 1 and response.get("results")
|
|
28
|
+
else []
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return [rate for rate in rates if rate is not None], messages
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_rate(
|
|
35
|
+
data: dict,
|
|
36
|
+
settings: provider_utils.Settings,
|
|
37
|
+
ctx: typing.Dict[str, typing.Any] = None,
|
|
38
|
+
) -> typing.Optional[models.RateDetails]:
|
|
39
|
+
"""Extract rate details from API response."""
|
|
40
|
+
ctx = ctx or {}
|
|
41
|
+
result = lib.to_object(parcelone.ShipmentResultType, data.get("results") or {})
|
|
42
|
+
|
|
43
|
+
# Check if we have charges in the response
|
|
44
|
+
if result.TotalCharges is None and not result.Charges:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
service_code = ctx.get("service_code", "parcelone_pa1_eco")
|
|
48
|
+
service = provider_units.ShippingService.map(service_code)
|
|
49
|
+
|
|
50
|
+
total_charge = lib.failsafe(lambda: float(result.TotalCharges.Value)) if result.TotalCharges else 0.0
|
|
51
|
+
currency = lib.failsafe(lambda: result.TotalCharges.Currency) or "EUR"
|
|
52
|
+
|
|
53
|
+
extra_charges = [
|
|
54
|
+
models.ChargeDetails(
|
|
55
|
+
name=charge.Description or "Shipping Charge",
|
|
56
|
+
amount=lib.to_money(charge.Value),
|
|
57
|
+
currency=charge.Currency or currency,
|
|
58
|
+
)
|
|
59
|
+
for charge in (result.Charges or [])
|
|
60
|
+
if charge.Value
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
return models.RateDetails(
|
|
64
|
+
carrier_name=settings.carrier_name,
|
|
65
|
+
carrier_id=settings.carrier_id,
|
|
66
|
+
service=service.name_or_key,
|
|
67
|
+
total_charge=lib.to_money(total_charge),
|
|
68
|
+
currency=currency,
|
|
69
|
+
extra_charges=extra_charges,
|
|
70
|
+
meta=dict(
|
|
71
|
+
service_name=service.name_or_key,
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def rate_request(
|
|
77
|
+
payload: models.RateRequest,
|
|
78
|
+
settings: provider_utils.Settings,
|
|
79
|
+
) -> lib.Serializable:
|
|
80
|
+
"""Create ParcelOne rate request.
|
|
81
|
+
|
|
82
|
+
Uses the shipment endpoint with ReturnCharges=1 to get rates.
|
|
83
|
+
"""
|
|
84
|
+
shipper = lib.to_address(payload.shipper)
|
|
85
|
+
recipient = lib.to_address(payload.recipient)
|
|
86
|
+
packages = lib.to_packages(payload.parcels, required=["weight"])
|
|
87
|
+
services = lib.to_services(payload.services, service_type=provider_units.ShippingService)
|
|
88
|
+
options = lib.to_shipping_options(
|
|
89
|
+
payload.options,
|
|
90
|
+
package_options=packages.options,
|
|
91
|
+
initializer=provider_units.shipping_options_initializer,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Get first service or use default
|
|
95
|
+
service = lib.identity(
|
|
96
|
+
next(iter(services), None)
|
|
97
|
+
or provider_units.ShippingService.parcelone_pa1_eco
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Parse service for CEP and product IDs
|
|
101
|
+
service_code = service.value if hasattr(service, 'value') else str(service)
|
|
102
|
+
cep_id, product_id = provider_units.parse_service_code(service_code)
|
|
103
|
+
cep_id = cep_id or settings.connection_config.cep_id.state
|
|
104
|
+
product_id = product_id or settings.connection_config.product_id.state
|
|
105
|
+
|
|
106
|
+
request = parcelone.ShippingDataRequestType(
|
|
107
|
+
ShippingData=parcelone.ShipmentType(
|
|
108
|
+
CEPID=cep_id,
|
|
109
|
+
ProductID=product_id,
|
|
110
|
+
MandatorID=settings.mandator_id,
|
|
111
|
+
ConsignerID=settings.consigner_id,
|
|
112
|
+
ShipToData=parcelone.ShipToType(
|
|
113
|
+
Name1=recipient.company_name or recipient.person_name,
|
|
114
|
+
ShipmentAddress=parcelone.AddressType(
|
|
115
|
+
Street=recipient.street,
|
|
116
|
+
PostalCode=recipient.postal_code,
|
|
117
|
+
City=recipient.city,
|
|
118
|
+
Country=recipient.country_code,
|
|
119
|
+
State=recipient.state_code,
|
|
120
|
+
),
|
|
121
|
+
PrivateAddressIndicator=1 if recipient.residential else 0,
|
|
122
|
+
),
|
|
123
|
+
ShipFromData=parcelone.ShipFromType(
|
|
124
|
+
Name1=shipper.company_name or shipper.person_name,
|
|
125
|
+
ShipmentAddress=parcelone.AddressType(
|
|
126
|
+
Street=shipper.street,
|
|
127
|
+
PostalCode=shipper.postal_code,
|
|
128
|
+
City=shipper.city,
|
|
129
|
+
Country=shipper.country_code,
|
|
130
|
+
State=shipper.state_code,
|
|
131
|
+
),
|
|
132
|
+
) if shipper else None,
|
|
133
|
+
ReturnCharges=1, # Request charges only
|
|
134
|
+
PrintLabel=0, # Don't generate label for rate request
|
|
135
|
+
Software="Karrio",
|
|
136
|
+
Packages=[
|
|
137
|
+
parcelone.ShipmentPackageType(
|
|
138
|
+
PackageRef=str(index),
|
|
139
|
+
PackageWeight=parcelone.MeasurementType(
|
|
140
|
+
Value=str(pkg.weight.KG),
|
|
141
|
+
Unit="kg",
|
|
142
|
+
),
|
|
143
|
+
PackageDimensions=(
|
|
144
|
+
parcelone.DimensionsType(
|
|
145
|
+
Length=str(pkg.length.CM),
|
|
146
|
+
Width=str(pkg.width.CM),
|
|
147
|
+
Height=str(pkg.height.CM),
|
|
148
|
+
)
|
|
149
|
+
if pkg.length.CM and pkg.width.CM and pkg.height.CM
|
|
150
|
+
else None
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
for index, pkg in enumerate(packages, 1)
|
|
154
|
+
],
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return lib.Serializable(
|
|
159
|
+
request,
|
|
160
|
+
lib.to_dict,
|
|
161
|
+
dict(service_code=service.name if hasattr(service, 'name') else str(service)),
|
|
162
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""ParcelOne shipment operations."""
|
|
2
|
+
|
|
3
|
+
from karrio.providers.parcelone.shipment.create import (
|
|
4
|
+
parse_shipment_response,
|
|
5
|
+
shipment_request,
|
|
6
|
+
)
|
|
7
|
+
from karrio.providers.parcelone.shipment.cancel import (
|
|
8
|
+
parse_shipment_cancel_response,
|
|
9
|
+
shipment_cancel_request,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"parse_shipment_response",
|
|
14
|
+
"shipment_request",
|
|
15
|
+
"parse_shipment_cancel_response",
|
|
16
|
+
"shipment_cancel_request",
|
|
17
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Karrio ParcelOne shipment cancellation implementation."""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
import karrio.lib as lib
|
|
5
|
+
import karrio.core.models as models
|
|
6
|
+
import karrio.providers.parcelone.error as error
|
|
7
|
+
import karrio.providers.parcelone.utils as provider_utils
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_shipment_cancel_response(
|
|
11
|
+
_response: lib.Deserializable[dict],
|
|
12
|
+
settings: provider_utils.Settings,
|
|
13
|
+
) -> typing.Tuple[typing.Optional[models.ConfirmationDetails], typing.List[models.Message]]:
|
|
14
|
+
"""Parse shipment cancellation response from ParcelOne REST API."""
|
|
15
|
+
response = _response.deserialize()
|
|
16
|
+
messages = error.parse_error_response(response, settings)
|
|
17
|
+
success = response.get("success") == 1
|
|
18
|
+
|
|
19
|
+
confirmation = lib.identity(
|
|
20
|
+
models.ConfirmationDetails(
|
|
21
|
+
carrier_id=settings.carrier_id,
|
|
22
|
+
carrier_name=settings.carrier_name,
|
|
23
|
+
operation="Cancel Shipment",
|
|
24
|
+
success=success,
|
|
25
|
+
)
|
|
26
|
+
if success
|
|
27
|
+
else None
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return confirmation, messages
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def shipment_cancel_request(
|
|
34
|
+
payload: models.ShipmentCancelRequest,
|
|
35
|
+
settings: provider_utils.Settings,
|
|
36
|
+
) -> lib.Serializable:
|
|
37
|
+
"""Create ParcelOne shipment cancel request.
|
|
38
|
+
|
|
39
|
+
The API uses DELETE /shipment/{ShipmentRefField}/{ShipmentRefValue}
|
|
40
|
+
where ShipmentRefField can be: ShipmentID, ShipmentRef, or TrackingID
|
|
41
|
+
"""
|
|
42
|
+
# Determine which reference field to use based on identifier format
|
|
43
|
+
identifier = payload.shipment_identifier
|
|
44
|
+
|
|
45
|
+
# If it's a numeric string, it's likely a ShipmentID
|
|
46
|
+
# If it starts with digits and has a specific length (13), it's likely a TrackingID
|
|
47
|
+
# Otherwise, treat it as a ShipmentRef
|
|
48
|
+
if identifier.isdigit() and len(identifier) < 10:
|
|
49
|
+
ref_field = "ShipmentID"
|
|
50
|
+
elif identifier.isdigit() and len(identifier) >= 10:
|
|
51
|
+
ref_field = "TrackingID"
|
|
52
|
+
else:
|
|
53
|
+
ref_field = "ShipmentRef"
|
|
54
|
+
|
|
55
|
+
request = dict(
|
|
56
|
+
ref_field=ref_field,
|
|
57
|
+
ref_value=identifier,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return lib.Serializable(request, lib.to_dict)
|