karrio-tnt 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/tnt/__init__.py +3 -0
- karrio/mappers/tnt/mapper.py +40 -0
- karrio/mappers/tnt/proxy.py +97 -0
- karrio/mappers/tnt/settings.py +22 -0
- karrio/plugins/tnt/__init__.py +24 -0
- karrio/providers/tnt/__init__.py +10 -0
- karrio/providers/tnt/error.py +129 -0
- karrio/providers/tnt/rate.py +184 -0
- karrio/providers/tnt/shipment/__init__.py +4 -0
- karrio/providers/tnt/shipment/create.py +226 -0
- karrio/providers/tnt/tracking.py +71 -0
- karrio/providers/tnt/units.py +166 -0
- karrio/providers/tnt/utils.py +162 -0
- karrio/schemas/tnt/__init__.py +0 -0
- karrio/schemas/tnt/label_common_definitions.py +3499 -0
- karrio/schemas/tnt/label_request.py +4409 -0
- karrio/schemas/tnt/label_response.py +5744 -0
- karrio/schemas/tnt/rating_common_definitions.py +1610 -0
- karrio/schemas/tnt/rating_request.py +2886 -0
- karrio/schemas/tnt/rating_response.py +3077 -0
- karrio/schemas/tnt/shipping_common_definitions.py +1216 -0
- karrio/schemas/tnt/shipping_request.py +6114 -0
- karrio/schemas/tnt/shipping_response.py +3404 -0
- karrio/schemas/tnt/tracking_request.py +2742 -0
- karrio/schemas/tnt/tracking_response.py +3729 -0
- karrio_tnt-2025.5rc1.dist-info/METADATA +44 -0
- karrio_tnt-2025.5rc1.dist-info/RECORD +30 -0
- karrio_tnt-2025.5rc1.dist-info/WHEEL +5 -0
- karrio_tnt-2025.5rc1.dist-info/entry_points.txt +2 -0
- karrio_tnt-2025.5rc1.dist-info/top_level.txt +3 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
"""Karrio TNT 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.tnt as provider
|
8
|
+
import karrio.mappers.tnt.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 parse_rate_response(
|
28
|
+
self, response: lib.Deserializable
|
29
|
+
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
|
30
|
+
return provider.parse_rate_response(response, self.settings)
|
31
|
+
|
32
|
+
def parse_shipment_response(
|
33
|
+
self, response: lib.Deserializable
|
34
|
+
) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
|
35
|
+
return provider.parse_shipment_response(response, self.settings)
|
36
|
+
|
37
|
+
def parse_tracking_response(
|
38
|
+
self, response: lib.Deserializable
|
39
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
40
|
+
return provider.parse_tracking_response(response, self.settings)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
import urllib.parse
|
2
|
+
import karrio.lib as lib
|
3
|
+
import karrio.api.proxy as proxy
|
4
|
+
import karrio.providers.tnt.error as provider_error
|
5
|
+
import karrio.providers.tnt.utils as provider_utils
|
6
|
+
import karrio.mappers.tnt.settings as provider_settings
|
7
|
+
|
8
|
+
|
9
|
+
class Proxy(proxy.Proxy):
|
10
|
+
settings: provider_settings.Settings
|
11
|
+
|
12
|
+
""" Proxy Methods """
|
13
|
+
|
14
|
+
def get_rates(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
15
|
+
response = lib.request(
|
16
|
+
url=f"{self.settings.server_url}/expressconnect/pricing/getprice",
|
17
|
+
data=urllib.parse.urlencode(dict(xml_in=request.serialize())),
|
18
|
+
trace=self.trace_as("xml"),
|
19
|
+
method="POST",
|
20
|
+
headers={
|
21
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
22
|
+
"Authorization": f"Basic {self.settings.authorization}",
|
23
|
+
},
|
24
|
+
)
|
25
|
+
|
26
|
+
return lib.Deserializable(response, lib.to_element)
|
27
|
+
|
28
|
+
def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
29
|
+
ctx: dict = {}
|
30
|
+
shipment_response = lib.request(
|
31
|
+
url=f"{self.settings.server_url}/expressconnect/shipping/ship",
|
32
|
+
data=urllib.parse.urlencode(dict(xml_in=request.serialize())),
|
33
|
+
trace=self.trace_as("xml"),
|
34
|
+
method="POST",
|
35
|
+
headers={
|
36
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
37
|
+
"Authorization": f"Basic {self.settings.authorization}",
|
38
|
+
},
|
39
|
+
)
|
40
|
+
|
41
|
+
label_request = provider_utils.create_label_request(
|
42
|
+
shipment_response, self.settings, request.ctx
|
43
|
+
)
|
44
|
+
|
45
|
+
if label_request is not None:
|
46
|
+
label_response = lib.request(
|
47
|
+
url=f"{self.settings.server_url}/expresslabel/documentation/getlabel",
|
48
|
+
trace=self.trace_as("xml"),
|
49
|
+
method="POST",
|
50
|
+
headers={
|
51
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
52
|
+
"Authorization": f"Basic {self.settings.authorization}",
|
53
|
+
},
|
54
|
+
data=urllib.parse.urlencode(dict(xml_in=label_request.serialize())),
|
55
|
+
)
|
56
|
+
|
57
|
+
label_errors = provider_error.parse_error_response(
|
58
|
+
lib.to_element(label_response), self.settings
|
59
|
+
)
|
60
|
+
|
61
|
+
if not any(label_errors):
|
62
|
+
label = lib.request(
|
63
|
+
url=f"{self.settings.server_url}/expresswebservices-website/app/render.html",
|
64
|
+
trace=self.trace_as("xml"),
|
65
|
+
method="POST",
|
66
|
+
headers={
|
67
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
68
|
+
"Authorization": f"Basic {self.settings.authorization}",
|
69
|
+
},
|
70
|
+
data=urllib.parse.urlencode(
|
71
|
+
dict(
|
72
|
+
responseXml=label_response,
|
73
|
+
documentType="routingLabel",
|
74
|
+
contentType="pdf",
|
75
|
+
),
|
76
|
+
),
|
77
|
+
decoder=lib.encode_base64,
|
78
|
+
)
|
79
|
+
|
80
|
+
ctx.update(label=label)
|
81
|
+
|
82
|
+
return lib.Deserializable(shipment_response, lib.to_element, ctx=ctx)
|
83
|
+
|
84
|
+
def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[str]:
|
85
|
+
response = lib.request(
|
86
|
+
url="https://necta.az.fxei.fedex.com/ectrack/track",
|
87
|
+
data=urllib.parse.urlencode(dict(xml_in=request.serialize())),
|
88
|
+
trace=self.trace_as("xml"),
|
89
|
+
method="POST",
|
90
|
+
headers={
|
91
|
+
"accept": "text/xml; charset=UTF-8",
|
92
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
93
|
+
"Authorization": f"Basic {self.settings.authorization}",
|
94
|
+
},
|
95
|
+
)
|
96
|
+
|
97
|
+
return lib.Deserializable(response, lib.to_element)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
"""Karrio TNT settings."""
|
2
|
+
|
3
|
+
import attr
|
4
|
+
import karrio.providers.tnt.utils as provider_utils
|
5
|
+
|
6
|
+
|
7
|
+
@attr.s(auto_attribs=True)
|
8
|
+
class Settings(provider_utils.Settings):
|
9
|
+
"""TNT connection settings."""
|
10
|
+
|
11
|
+
# Carrier specific properties
|
12
|
+
username: str
|
13
|
+
password: str
|
14
|
+
account_number: str = None
|
15
|
+
account_country_code: str = None
|
16
|
+
metadata: dict = {}
|
17
|
+
config: dict = {}
|
18
|
+
|
19
|
+
# Base properties
|
20
|
+
id: str = None
|
21
|
+
test_mode: bool = False
|
22
|
+
carrier_id: str = "tnt"
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import karrio.core.metadata as metadata
|
2
|
+
import karrio.mappers.tnt as mappers
|
3
|
+
import karrio.providers.tnt.units as units
|
4
|
+
|
5
|
+
|
6
|
+
METADATA = metadata.PluginMetadata(
|
7
|
+
status="beta",
|
8
|
+
id="tnt",
|
9
|
+
label="TNT",
|
10
|
+
# Integrations
|
11
|
+
Mapper=mappers.Mapper,
|
12
|
+
Proxy=mappers.Proxy,
|
13
|
+
Settings=mappers.Settings,
|
14
|
+
# Data Units
|
15
|
+
options=units.ShippingOption,
|
16
|
+
services=units.ShippingService,
|
17
|
+
packaging_types=units.PackageType,
|
18
|
+
package_presets=units.PackagePresets,
|
19
|
+
connection_configs=units.ConnectionConfig,
|
20
|
+
has_intl_accounts=True,
|
21
|
+
# New fields
|
22
|
+
website="https://www.tnt.com",
|
23
|
+
description="TNT is an international courier delivery services company with headquarters in the Netherlands.",
|
24
|
+
)
|
@@ -0,0 +1,10 @@
|
|
1
|
+
from karrio.providers.tnt.utils import Settings
|
2
|
+
from karrio.providers.tnt.rate import parse_rate_response, rate_request
|
3
|
+
from karrio.providers.tnt.shipment import (
|
4
|
+
parse_shipment_response,
|
5
|
+
shipment_request,
|
6
|
+
)
|
7
|
+
from karrio.providers.tnt.tracking import (
|
8
|
+
parse_tracking_response,
|
9
|
+
tracking_request,
|
10
|
+
)
|
@@ -0,0 +1,129 @@
|
|
1
|
+
import karrio.schemas.tnt.rating_response as tnt
|
2
|
+
import typing
|
3
|
+
import karrio.lib as lib
|
4
|
+
import karrio.core.models as models
|
5
|
+
import karrio.providers.tnt.utils as provider_utils
|
6
|
+
|
7
|
+
|
8
|
+
def parse_error_response(
|
9
|
+
response,
|
10
|
+
settings: provider_utils.Settings,
|
11
|
+
) -> typing.List[models.Message]:
|
12
|
+
structure_errors = lib.find_element("ErrorStructure", response)
|
13
|
+
broken_rules_nodes = lib.find_element("brokenRules", response)
|
14
|
+
broken_rule_nodes = lib.find_element("brokenRule", response)
|
15
|
+
runtime_errors = lib.find_element("runtime_error", response)
|
16
|
+
parse_errors = lib.find_element("parse_error", response)
|
17
|
+
ERRORS = lib.find_element("ERROR", response)
|
18
|
+
errors = lib.find_element("Error", response)
|
19
|
+
faults = lib.find_element("fault", response)
|
20
|
+
|
21
|
+
return [
|
22
|
+
*[_extract_structure_error(node, settings) for node in structure_errors],
|
23
|
+
*[_extract_broken_rules(node, settings) for node in broken_rules_nodes],
|
24
|
+
*[_extract_broken_rule(node, settings) for node in broken_rule_nodes],
|
25
|
+
*[_extract_runtime_error(node, settings) for node in runtime_errors],
|
26
|
+
*[_extract_parse_error(node, settings) for node in parse_errors],
|
27
|
+
*[_extract_structure_error(node, settings) for node in errors],
|
28
|
+
*[_extract_error(node, settings) for node in ERRORS],
|
29
|
+
*[_extract_faut(node, settings) for node in faults],
|
30
|
+
]
|
31
|
+
|
32
|
+
|
33
|
+
def _extract_structure_error(
|
34
|
+
node: lib.Element, settings: provider_utils.Settings
|
35
|
+
) -> models.Message:
|
36
|
+
return models.Message(
|
37
|
+
# context info
|
38
|
+
carrier_name=settings.carrier_name,
|
39
|
+
carrier_id=settings.carrier_id,
|
40
|
+
# carrier error info
|
41
|
+
code=lib.find_element("Code", node, first=True).text,
|
42
|
+
message=lib.find_element("Message", node, first=True).text,
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
def _extract_broken_rules(
|
47
|
+
node: lib.Element, settings: provider_utils.Settings
|
48
|
+
) -> models.Message:
|
49
|
+
return models.Message(
|
50
|
+
# context info
|
51
|
+
carrier_name=settings.carrier_name,
|
52
|
+
carrier_id=settings.carrier_id,
|
53
|
+
# carrier error info
|
54
|
+
code=lib.find_element("errorCode", node, first=True).text,
|
55
|
+
message=lib.find_element("errorMessage", node, first=True).text,
|
56
|
+
)
|
57
|
+
|
58
|
+
|
59
|
+
def _extract_broken_rule(
|
60
|
+
node: lib.Element, settings: provider_utils.Settings
|
61
|
+
) -> models.Message:
|
62
|
+
error = lib.to_object(tnt.brokenRule, node)
|
63
|
+
|
64
|
+
return models.Message(
|
65
|
+
# context info
|
66
|
+
carrier_name=settings.carrier_name,
|
67
|
+
carrier_id=settings.carrier_id,
|
68
|
+
# carrier error info
|
69
|
+
code=getattr(error, "code", None),
|
70
|
+
message=getattr(error, "description", None),
|
71
|
+
details=dict(messageType=getattr(error, "messageType", None)),
|
72
|
+
)
|
73
|
+
|
74
|
+
|
75
|
+
def _extract_runtime_error(
|
76
|
+
node: lib.Element, settings: provider_utils.Settings
|
77
|
+
) -> models.Message:
|
78
|
+
error = lib.to_object(tnt.runtimeError, node)
|
79
|
+
|
80
|
+
return models.Message(
|
81
|
+
# context info
|
82
|
+
carrier_name=settings.carrier_name,
|
83
|
+
carrier_id=settings.carrier_id,
|
84
|
+
# carrier error info
|
85
|
+
code="runtime",
|
86
|
+
message=getattr(error, "errorReason", None),
|
87
|
+
details=dict(srcTxt=getattr(error, "errorSrcText", None)),
|
88
|
+
)
|
89
|
+
|
90
|
+
|
91
|
+
def _extract_parse_error(
|
92
|
+
node: lib.Element, settings: provider_utils.Settings
|
93
|
+
) -> models.Message:
|
94
|
+
error = lib.to_object(tnt.parseError, node)
|
95
|
+
|
96
|
+
return models.Message(
|
97
|
+
# context info
|
98
|
+
carrier_name=settings.carrier_name,
|
99
|
+
carrier_id=settings.carrier_id,
|
100
|
+
# carrier error info
|
101
|
+
code="parsing",
|
102
|
+
message=getattr(error, "errorReason", None),
|
103
|
+
details=dict(srcText=getattr(error, "errorSrcText", None)),
|
104
|
+
)
|
105
|
+
|
106
|
+
|
107
|
+
def _extract_error(
|
108
|
+
node: lib.Element, settings: provider_utils.Settings
|
109
|
+
) -> models.Message:
|
110
|
+
return models.Message(
|
111
|
+
# context info
|
112
|
+
carrier_name=settings.carrier_name,
|
113
|
+
carrier_id=settings.carrier_id,
|
114
|
+
# carrier error info
|
115
|
+
code=lib.find_element("CODE", node, first=True).text,
|
116
|
+
message=lib.find_element("DESCRIPTION", node, first=True).text,
|
117
|
+
)
|
118
|
+
|
119
|
+
|
120
|
+
def _extract_faut(
|
121
|
+
node: lib.Element, settings: provider_utils.Settings
|
122
|
+
) -> models.Message:
|
123
|
+
return models.Message(
|
124
|
+
# context info
|
125
|
+
carrier_name=settings.carrier_name,
|
126
|
+
carrier_id=settings.carrier_id,
|
127
|
+
# carrier error info
|
128
|
+
code=lib.find_element("key", node, first=True).text,
|
129
|
+
)
|
@@ -0,0 +1,184 @@
|
|
1
|
+
import functools
|
2
|
+
import karrio.schemas.tnt.rating_request as tnt
|
3
|
+
import karrio.schemas.tnt.rating_response as rating
|
4
|
+
import typing
|
5
|
+
import karrio.lib as lib
|
6
|
+
import karrio.core.models as models
|
7
|
+
import karrio.providers.tnt.error as provider_error
|
8
|
+
import karrio.providers.tnt.units as provider_units
|
9
|
+
import karrio.providers.tnt.utils as provider_utils
|
10
|
+
|
11
|
+
|
12
|
+
def parse_rate_response(
|
13
|
+
_response: lib.Deserializable[lib.Element],
|
14
|
+
settings: provider_utils.Settings,
|
15
|
+
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
|
16
|
+
response = _response.deserialize()
|
17
|
+
|
18
|
+
messages = provider_error.parse_error_response(response, settings)
|
19
|
+
services: typing.List[rating.ratedServices] = lib.find_element(
|
20
|
+
"ratedServices", response, rating.ratedServices
|
21
|
+
)
|
22
|
+
rates: typing.List[models.RateDetails] = sum(
|
23
|
+
[
|
24
|
+
[
|
25
|
+
_extract_details((rate, svc.currency), settings)
|
26
|
+
for rate in svc.ratedService
|
27
|
+
]
|
28
|
+
for svc in services
|
29
|
+
],
|
30
|
+
start=[],
|
31
|
+
)
|
32
|
+
|
33
|
+
return rates, messages
|
34
|
+
|
35
|
+
|
36
|
+
def _extract_details(
|
37
|
+
details: typing.Tuple[rating.ratedService, str],
|
38
|
+
settings: provider_utils.Settings,
|
39
|
+
) -> models.RateDetails:
|
40
|
+
rate, currency = details
|
41
|
+
service = provider_units.ShippingService.map(rate.product.id)
|
42
|
+
charges = [
|
43
|
+
("Base charge", rate.totalPriceExclVat),
|
44
|
+
("VAT", rate.vatAmount),
|
45
|
+
*(
|
46
|
+
(charge.description, charge.chargeValue)
|
47
|
+
for charge in rate.chargeElements.chargeElement
|
48
|
+
),
|
49
|
+
]
|
50
|
+
|
51
|
+
return models.RateDetails(
|
52
|
+
carrier_name=settings.carrier_name,
|
53
|
+
carrier_id=settings.carrier_id,
|
54
|
+
currency=currency,
|
55
|
+
service=service.name_or_key,
|
56
|
+
total_charge=lib.to_decimal(rate.totalPrice),
|
57
|
+
extra_charges=[
|
58
|
+
models.ChargeDetails(
|
59
|
+
name=name,
|
60
|
+
amount=lib.to_decimal(amount),
|
61
|
+
currency=currency,
|
62
|
+
)
|
63
|
+
for name, amount in charges
|
64
|
+
if amount is not None
|
65
|
+
],
|
66
|
+
meta=dict(service_name=rate.product.productDesc),
|
67
|
+
)
|
68
|
+
|
69
|
+
|
70
|
+
def rate_request(
|
71
|
+
payload: models.RateRequest,
|
72
|
+
settings: provider_utils.Settings,
|
73
|
+
) -> lib.Serializable:
|
74
|
+
service = lib.to_services(payload.services, provider_units.ShippingService).first
|
75
|
+
packages = lib.to_packages(
|
76
|
+
payload.parcels,
|
77
|
+
presets=provider_units.PackagePresets,
|
78
|
+
package_option_type=provider_units.PackageType,
|
79
|
+
)
|
80
|
+
options = lib.to_shipping_options(
|
81
|
+
payload.options,
|
82
|
+
package_options=packages.options,
|
83
|
+
shipper_country_code=payload.shipper.country_code,
|
84
|
+
recipient_country_code=payload.recipient.country_code,
|
85
|
+
is_international=(
|
86
|
+
payload.shipper.country_code != payload.recipient.country_code
|
87
|
+
),
|
88
|
+
initializer=provider_units.shipping_options_initializer,
|
89
|
+
)
|
90
|
+
is_document = all([parcel.is_document for parcel in payload.parcels])
|
91
|
+
|
92
|
+
request = tnt.priceRequest(
|
93
|
+
appId=settings.connection_config.app_id.state or "PC",
|
94
|
+
appVersion="3.2",
|
95
|
+
priceCheck=[
|
96
|
+
tnt.priceCheck(
|
97
|
+
rateId=None,
|
98
|
+
sender=tnt.address(
|
99
|
+
country=payload.shipper.country_code,
|
100
|
+
town=payload.shipper.city,
|
101
|
+
postcode=payload.shipper.postal_code,
|
102
|
+
),
|
103
|
+
delivery=tnt.address(
|
104
|
+
country=payload.recipient.country_code,
|
105
|
+
town=payload.recipient.city,
|
106
|
+
postcode=payload.recipient.postal_code,
|
107
|
+
),
|
108
|
+
collectionDateTime=lib.fdatetime(
|
109
|
+
options.shipment_date.state,
|
110
|
+
current_format="%Y-%m-%d",
|
111
|
+
output_format="%Y-%m-%dT%H:%M:%S",
|
112
|
+
),
|
113
|
+
product=tnt.product(
|
114
|
+
id=getattr(service, "value", None),
|
115
|
+
division=next(
|
116
|
+
(
|
117
|
+
option.code
|
118
|
+
for key, option in options.items()
|
119
|
+
if "division" in key and option.state is True
|
120
|
+
),
|
121
|
+
None,
|
122
|
+
),
|
123
|
+
productDesc=None,
|
124
|
+
type_=("D" if is_document else "N"),
|
125
|
+
options=(
|
126
|
+
tnt.options(
|
127
|
+
option=[
|
128
|
+
tnt.option(optionCode=option.code)
|
129
|
+
for key, option in options.items()
|
130
|
+
if "division" not in key
|
131
|
+
]
|
132
|
+
)
|
133
|
+
if any(options.items())
|
134
|
+
else None
|
135
|
+
),
|
136
|
+
),
|
137
|
+
account=(
|
138
|
+
tnt.account(
|
139
|
+
accountNumber=settings.account_number,
|
140
|
+
accountCountry=settings.account_country_code,
|
141
|
+
)
|
142
|
+
if any([settings.account_number, settings.account_country_code])
|
143
|
+
else None
|
144
|
+
),
|
145
|
+
insurance=(
|
146
|
+
tnt.insurance(
|
147
|
+
insuranceValue=options.insurance.state,
|
148
|
+
goodsValue=options.declared_value.state,
|
149
|
+
)
|
150
|
+
if options.insurance.state is not None
|
151
|
+
else None
|
152
|
+
),
|
153
|
+
termsOfPayment=provider_units.PaymentType.sender.value,
|
154
|
+
currency=options.currency.state,
|
155
|
+
priceBreakDown=True,
|
156
|
+
consignmentDetails=tnt.consignmentDetails(
|
157
|
+
totalWeight=packages.weight.KG,
|
158
|
+
totalVolume=packages.volume.m3,
|
159
|
+
totalNumberOfPieces=len(packages),
|
160
|
+
),
|
161
|
+
pieceLine=[
|
162
|
+
tnt.pieceLine(
|
163
|
+
numberOfPieces=1,
|
164
|
+
pieceMeasurements=tnt.pieceMeasurements(
|
165
|
+
length=package.length.M,
|
166
|
+
width=package.width.M,
|
167
|
+
height=package.height.M,
|
168
|
+
weight=package.weight.KG,
|
169
|
+
),
|
170
|
+
pallet=(package.packaging_type == "pallet"),
|
171
|
+
)
|
172
|
+
for package in packages
|
173
|
+
],
|
174
|
+
)
|
175
|
+
],
|
176
|
+
)
|
177
|
+
|
178
|
+
return lib.Serializable(
|
179
|
+
request,
|
180
|
+
functools.partial(
|
181
|
+
lib.to_xml,
|
182
|
+
namespacedef_='xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
|
183
|
+
),
|
184
|
+
)
|