karrio-landmark 2025.5__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/landmark/__init__.py +3 -0
- karrio/mappers/landmark/mapper.py +51 -0
- karrio/mappers/landmark/proxy.py +63 -0
- karrio/mappers/landmark/settings.py +37 -0
- karrio/plugins/landmark/__init__.py +30 -0
- karrio/providers/landmark/__init__.py +12 -0
- karrio/providers/landmark/error.py +38 -0
- karrio/providers/landmark/services.csv +301 -0
- karrio/providers/landmark/shipment/__init__.py +9 -0
- karrio/providers/landmark/shipment/cancel.py +69 -0
- karrio/providers/landmark/shipment/create.py +477 -0
- karrio/providers/landmark/tracking.py +139 -0
- karrio/providers/landmark/units.py +254 -0
- karrio/providers/landmark/utils.py +35 -0
- karrio/schemas/landmark/__init__.py +0 -0
- karrio/schemas/landmark/cancel_request.py +1524 -0
- karrio/schemas/landmark/cancel_response.py +1439 -0
- karrio/schemas/landmark/error_response.py +1432 -0
- karrio/schemas/landmark/import_request.py +5733 -0
- karrio/schemas/landmark/import_response.py +2064 -0
- karrio/schemas/landmark/ship_request.py +4838 -0
- karrio/schemas/landmark/ship_response.py +2380 -0
- karrio/schemas/landmark/track_request.py +1547 -0
- karrio/schemas/landmark/track_response.py +2046 -0
- karrio_landmark-2025.5.dist-info/METADATA +44 -0
- karrio_landmark-2025.5.dist-info/RECORD +29 -0
- karrio_landmark-2025.5.dist-info/WHEEL +5 -0
- karrio_landmark-2025.5.dist-info/entry_points.txt +2 -0
- karrio_landmark-2025.5.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""Karrio Landmark Global shipment creation implementation."""
|
|
2
|
+
|
|
3
|
+
import karrio.schemas.landmark.import_request as import_req
|
|
4
|
+
import karrio.schemas.landmark.import_response as import_res
|
|
5
|
+
import karrio.schemas.landmark.ship_request as ship_req
|
|
6
|
+
import karrio.schemas.landmark.ship_response as ship_res
|
|
7
|
+
|
|
8
|
+
import typing
|
|
9
|
+
import karrio.lib as lib
|
|
10
|
+
import karrio.core.units as units
|
|
11
|
+
import karrio.core.models as models
|
|
12
|
+
import karrio.providers.landmark.error as error
|
|
13
|
+
import karrio.providers.landmark.utils as provider_utils
|
|
14
|
+
import karrio.providers.landmark.units as provider_units
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_shipment_response(
|
|
18
|
+
_response: lib.Deserializable[lib.Element],
|
|
19
|
+
settings: provider_utils.Settings,
|
|
20
|
+
) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
|
|
21
|
+
response = _response.deserialize()
|
|
22
|
+
messages = error.parse_error_response(response, settings)
|
|
23
|
+
|
|
24
|
+
shipment = (
|
|
25
|
+
_extract_details(response, settings, ctx=_response.ctx)
|
|
26
|
+
if len(lib.find_element("TrackingNumber", response)) > 0
|
|
27
|
+
else None
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return shipment, messages
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extract_details(
|
|
34
|
+
data: lib.Element,
|
|
35
|
+
settings: provider_utils.Settings,
|
|
36
|
+
ctx: dict = dict(),
|
|
37
|
+
) -> models.ShipmentDetails:
|
|
38
|
+
"""Extract shipment details from ImportResponse or ShipResponse"""
|
|
39
|
+
# fmt: off
|
|
40
|
+
label_format = ctx.get("label_format", "PDF")
|
|
41
|
+
packages = lib.find_element("Package", data, ship_res.PackageType)
|
|
42
|
+
result = lib.find_element("Result", data, ship_res.ResultType, first=True)
|
|
43
|
+
label_images = lib.find_element("LabelImages", data, ship_res.LabelImagesType)
|
|
44
|
+
|
|
45
|
+
last_mile_carrier = getattr(result, "ShippingCarrier", None)
|
|
46
|
+
tracking_numbers = [_.LandmarkTrackingNumber for _ in packages if _.LandmarkTrackingNumber is not None]
|
|
47
|
+
last_mile_tracking_numbers = [_.TrackingNumber for _ in packages if _.TrackingNumber is not None]
|
|
48
|
+
package_references = [_.PackageReference for _ in packages if _.PackageReference is not None]
|
|
49
|
+
barcode_datas = [_.BarcodeData for _ in packages if _.BarcodeData is not None]
|
|
50
|
+
landmark_ids = [_.PackageID for _ in packages if _.PackageID is not None]
|
|
51
|
+
|
|
52
|
+
tracking_number = next(iter(tracking_numbers), None)
|
|
53
|
+
shipment_identifier = next(iter(landmark_ids), tracking_number)
|
|
54
|
+
last_mile_tracking_number = next(iter(last_mile_tracking_numbers), None) if last_mile_carrier else None
|
|
55
|
+
last_mile_tracking_numbers = last_mile_tracking_numbers if last_mile_carrier else []
|
|
56
|
+
shipment_identifiers = landmark_ids if len(landmark_ids) > 0 else tracking_numbers
|
|
57
|
+
label = (
|
|
58
|
+
lib.bundle_base64(
|
|
59
|
+
[_.LabelImage[0] for _ in label_images if len(_.LabelImage) > 0],
|
|
60
|
+
label_format,
|
|
61
|
+
)
|
|
62
|
+
if any(_.LabelImage for _ in label_images)
|
|
63
|
+
else ""
|
|
64
|
+
)
|
|
65
|
+
# fmt: on
|
|
66
|
+
|
|
67
|
+
return models.ShipmentDetails(
|
|
68
|
+
carrier_id=settings.carrier_id,
|
|
69
|
+
carrier_name=settings.carrier_name,
|
|
70
|
+
tracking_number=tracking_number,
|
|
71
|
+
shipment_identifier=shipment_identifier,
|
|
72
|
+
label_type=label_format,
|
|
73
|
+
docs=models.Documents(label=label),
|
|
74
|
+
meta=lib.to_dict(
|
|
75
|
+
dict(
|
|
76
|
+
last_mile_carrier=last_mile_carrier,
|
|
77
|
+
last_mile_tracking_number=last_mile_tracking_number,
|
|
78
|
+
last_mile_tracking_numbers=last_mile_tracking_numbers,
|
|
79
|
+
shipment_identifiers=shipment_identifiers,
|
|
80
|
+
package_references=package_references,
|
|
81
|
+
tracking_numbers=tracking_numbers,
|
|
82
|
+
barcode_datas=barcode_datas,
|
|
83
|
+
)
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def shipment_request(
|
|
89
|
+
payload: models.ShipmentRequest,
|
|
90
|
+
settings: provider_utils.Settings,
|
|
91
|
+
) -> lib.Serializable:
|
|
92
|
+
"""Create a shipment request for the carrier API"""
|
|
93
|
+
# Convert karrio models to carrier-specific format
|
|
94
|
+
shipper = lib.to_address(payload.shipper)
|
|
95
|
+
recipient = lib.to_address(payload.recipient)
|
|
96
|
+
return_address = lib.to_address(payload.return_address)
|
|
97
|
+
packages = lib.to_packages(payload.parcels)
|
|
98
|
+
service = provider_units.ShippingService.map(payload.service).value_or_key
|
|
99
|
+
options = lib.to_shipping_options(
|
|
100
|
+
payload.options,
|
|
101
|
+
package_options=packages.options,
|
|
102
|
+
initializer=provider_units.shipping_options_initializer,
|
|
103
|
+
)
|
|
104
|
+
weight_unit, dim_unit = packages.compatible_units
|
|
105
|
+
|
|
106
|
+
customs = lib.to_customs_info(
|
|
107
|
+
payload.customs,
|
|
108
|
+
shipper=payload.shipper,
|
|
109
|
+
recipient=payload.recipient,
|
|
110
|
+
weight_unit=weight_unit.value,
|
|
111
|
+
)
|
|
112
|
+
vendor = lib.to_address(
|
|
113
|
+
dict(
|
|
114
|
+
sender=payload.shipper,
|
|
115
|
+
recipient=payload.recipient,
|
|
116
|
+
third_party=customs.duty_billing_address,
|
|
117
|
+
)[customs.duty.paid_by]
|
|
118
|
+
)
|
|
119
|
+
label_format = lib.identity(
|
|
120
|
+
payload.label_type or settings.connection_config.label_type.state
|
|
121
|
+
)
|
|
122
|
+
label_encoding = "BASE64"
|
|
123
|
+
is_import_request = lib.identity(
|
|
124
|
+
options.landmark_import_request.state
|
|
125
|
+
if options.landmark_import_request.state is not None
|
|
126
|
+
else settings.connection_config.import_request_by_default.state
|
|
127
|
+
)
|
|
128
|
+
produce_label = lib.identity(
|
|
129
|
+
options.landmark_produce_label.state
|
|
130
|
+
if options.landmark_produce_label.state is not None
|
|
131
|
+
else (
|
|
132
|
+
settings.connection_config.impport_request_produce_label.state
|
|
133
|
+
if settings.connection_config.impport_request_produce_label.state
|
|
134
|
+
is not None
|
|
135
|
+
else False
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
items = lib.identity(customs.commodities if payload.customs else packages.items)
|
|
139
|
+
|
|
140
|
+
request = lib.identity(
|
|
141
|
+
import_req.ImportRequest(
|
|
142
|
+
Login=import_req.LoginType(
|
|
143
|
+
Username=settings.username,
|
|
144
|
+
Password=settings.password,
|
|
145
|
+
),
|
|
146
|
+
Test=settings.test_mode,
|
|
147
|
+
ClientID=settings.client_id,
|
|
148
|
+
AccountNumber=settings.account_number,
|
|
149
|
+
Reference=payload.reference,
|
|
150
|
+
ShipTo=import_req.ShipToType(
|
|
151
|
+
Name=recipient.company_name or recipient.person_name or "",
|
|
152
|
+
Attention=recipient.person_name,
|
|
153
|
+
Address1=recipient.address_line1 or "",
|
|
154
|
+
Address2=recipient.address_line2,
|
|
155
|
+
Address3=None,
|
|
156
|
+
City=recipient.city or "",
|
|
157
|
+
State=lib.text(recipient.state_code, max=25) or "",
|
|
158
|
+
PostalCode=recipient.postal_code or "",
|
|
159
|
+
Country=recipient.country_code or "",
|
|
160
|
+
Phone=recipient.phone_number,
|
|
161
|
+
Email=recipient.email,
|
|
162
|
+
ConsigneeTaxID=recipient.tax_id,
|
|
163
|
+
),
|
|
164
|
+
ShippingLane=import_req.ShippingLaneType(
|
|
165
|
+
Region=settings.region,
|
|
166
|
+
),
|
|
167
|
+
ShipMethod=service,
|
|
168
|
+
OrderTotal=lib.identity(
|
|
169
|
+
customs.duty.declared_value or options.declared_value.state
|
|
170
|
+
),
|
|
171
|
+
OrderInsuranceFreightTotal=options.landmark_order_insurance_freight_total.state,
|
|
172
|
+
ShipmentInsuranceFreight=options.landmark_shipment_insurance_freight.state,
|
|
173
|
+
ItemsCurrency=options.currency.state,
|
|
174
|
+
IsCommercialShipment=("1" if customs.commercial_invoice else "0"),
|
|
175
|
+
ProduceLabel=produce_label,
|
|
176
|
+
LabelFormat=label_format,
|
|
177
|
+
LabelEncoding=label_encoding,
|
|
178
|
+
ShipOptions=None,
|
|
179
|
+
VendorInformation=lib.identity(
|
|
180
|
+
import_req.VendorInformationType(
|
|
181
|
+
VendorName=vendor.company_name or vendor.person_name or "",
|
|
182
|
+
VendorPhone=vendor.phone_number or "",
|
|
183
|
+
VendorEmail=vendor.email or "",
|
|
184
|
+
VendorAddress1=vendor.address_line1 or "",
|
|
185
|
+
VendorAddress2=vendor.address_line2,
|
|
186
|
+
VendorCity=vendor.city or "",
|
|
187
|
+
VendorState=lib.text(vendor.state_code, max=25) or "",
|
|
188
|
+
VendorPostalCode=vendor.postal_code or "",
|
|
189
|
+
VendorCountry=vendor.country_code or "",
|
|
190
|
+
VendorLowValueTaxID=customs.options.low_value_tax_id.state,
|
|
191
|
+
VendorCCN=customs.options.ccn.state,
|
|
192
|
+
VendorBusinessNumber=customs.options.business_number.state,
|
|
193
|
+
VendorRGRNumber=customs.options.rgr_number.state,
|
|
194
|
+
VendorIOSSNumber=customs.options.ioss.state,
|
|
195
|
+
VendorEORINumber=customs.options.eori_number.state,
|
|
196
|
+
)
|
|
197
|
+
if payload.customs
|
|
198
|
+
else None
|
|
199
|
+
),
|
|
200
|
+
FulfillmentAddress=import_req.FulfillmentAddressType(
|
|
201
|
+
Name=shipper.company_name or shipper.person_name or "",
|
|
202
|
+
Attention=shipper.person_name,
|
|
203
|
+
Address1=shipper.address_line1 or "",
|
|
204
|
+
Address2=shipper.address_line2,
|
|
205
|
+
Address3=None,
|
|
206
|
+
City=shipper.city or "",
|
|
207
|
+
State=lib.text(shipper.state_code, max=25) or "",
|
|
208
|
+
PostalCode=shipper.postal_code or "",
|
|
209
|
+
Country=shipper.country_code or "",
|
|
210
|
+
),
|
|
211
|
+
ReturnAddress=lib.identity(
|
|
212
|
+
import_req.SendReturnToAddressType(
|
|
213
|
+
Code=options.landmark_return_address_code.state,
|
|
214
|
+
Name=return_address.company_name
|
|
215
|
+
or return_address.person_name
|
|
216
|
+
or "",
|
|
217
|
+
Attention=return_address.person_name,
|
|
218
|
+
Address1=return_address.address_line1 or "",
|
|
219
|
+
Address2=return_address.address_line2,
|
|
220
|
+
Address3=None,
|
|
221
|
+
City=return_address.city or "",
|
|
222
|
+
State=lib.text(return_address.state_code, max=25) or "",
|
|
223
|
+
PostalCode=return_address.postal_code or "",
|
|
224
|
+
Country=return_address.country_code or "",
|
|
225
|
+
)
|
|
226
|
+
if payload.return_address
|
|
227
|
+
else None
|
|
228
|
+
),
|
|
229
|
+
AdditionalFields=None,
|
|
230
|
+
PickSlipAdditions=None,
|
|
231
|
+
ValueAddedServices=None,
|
|
232
|
+
Packages=lib.identity(
|
|
233
|
+
import_req.PackagesType(
|
|
234
|
+
Package=[
|
|
235
|
+
import_req.PackageType(
|
|
236
|
+
WeightUnit=weight_unit.value,
|
|
237
|
+
Weight=pkg.weight[weight_unit.value],
|
|
238
|
+
DimensionsUnit=dim_unit.value,
|
|
239
|
+
Length=pkg.length[dim_unit.value],
|
|
240
|
+
Width=pkg.width[dim_unit.value],
|
|
241
|
+
Height=pkg.height[dim_unit.value],
|
|
242
|
+
PackageReference=pkg.reference_number,
|
|
243
|
+
)
|
|
244
|
+
for pkg in packages
|
|
245
|
+
]
|
|
246
|
+
)
|
|
247
|
+
if options.fulfilled_by_landmark.state is not True
|
|
248
|
+
else None
|
|
249
|
+
),
|
|
250
|
+
Items=lib.identity(
|
|
251
|
+
import_req.ItemsType(
|
|
252
|
+
Item=[
|
|
253
|
+
import_req.ItemType(
|
|
254
|
+
Sku=item.sku or "",
|
|
255
|
+
LineNumber=item.metadata.get("LineNumber"),
|
|
256
|
+
Quantity=lib.to_int(item.quantity),
|
|
257
|
+
UnitPrice=lib.to_money(item.value_amount),
|
|
258
|
+
Description=item.description or item.title or "",
|
|
259
|
+
HSCode=item.hs_code,
|
|
260
|
+
CountryOfOrigin=item.origin_country,
|
|
261
|
+
URL=item.product_url,
|
|
262
|
+
USMID=item.metadata.get("USMID"),
|
|
263
|
+
ContentCategory=import_req.ContentCategoryType(
|
|
264
|
+
ReturnCustomsInfo=lib.identity(
|
|
265
|
+
import_req.ReturnCustomsInfoType(
|
|
266
|
+
HSCode=item.hs_code,
|
|
267
|
+
HSRegionCode=item.origin_country,
|
|
268
|
+
)
|
|
269
|
+
if item.hs_code or item.origin_country
|
|
270
|
+
else None
|
|
271
|
+
),
|
|
272
|
+
DangerousGoodsInformation=lib.identity(
|
|
273
|
+
import_req.DangerousGoodsInformationType(
|
|
274
|
+
UNCode=item.metadata.get("UNCode"),
|
|
275
|
+
PackingGroup=item.metadata.get("PackingGroup"),
|
|
276
|
+
PackingInstructions=item.metadata.get(
|
|
277
|
+
"PackingInstructions"
|
|
278
|
+
),
|
|
279
|
+
ItemWeight=item.weight,
|
|
280
|
+
ItemWeightUnit=(
|
|
281
|
+
item.weight_unit.lower()
|
|
282
|
+
if item.weight_unit
|
|
283
|
+
else None
|
|
284
|
+
),
|
|
285
|
+
ItemVolume=item.metadata.get("ItemVolume"),
|
|
286
|
+
ItemVolumeUnit=item.metadata.get(
|
|
287
|
+
"ItemVolumeUnit"
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
if options.dangerous_goods.state
|
|
291
|
+
else None
|
|
292
|
+
),
|
|
293
|
+
ValueAddedServices=None,
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
for item in items
|
|
297
|
+
]
|
|
298
|
+
)
|
|
299
|
+
if len(items) > 0
|
|
300
|
+
else None
|
|
301
|
+
),
|
|
302
|
+
FreightDetails=lib.identity(
|
|
303
|
+
import_req.FreightDetailsType(
|
|
304
|
+
ProNumber=options.landmark_freight_pro_number.state or "",
|
|
305
|
+
PieceUnit=options.landmark_freight_piece_unit.state or "",
|
|
306
|
+
)
|
|
307
|
+
if options.landmark_freight_pro_number.state
|
|
308
|
+
and options.landmark_freight_piece_unit.state
|
|
309
|
+
else None
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
if is_import_request
|
|
313
|
+
else ship_req.ShipRequest(
|
|
314
|
+
Login=ship_req.LoginType(
|
|
315
|
+
Username=settings.username,
|
|
316
|
+
Password=settings.password,
|
|
317
|
+
),
|
|
318
|
+
Test=settings.test_mode,
|
|
319
|
+
ClientID=settings.client_id,
|
|
320
|
+
AccountNumber=settings.account_number,
|
|
321
|
+
Reference=payload.reference,
|
|
322
|
+
ShipTo=ship_req.ShipToType(
|
|
323
|
+
Name=recipient.company_name or recipient.person_name or "",
|
|
324
|
+
Attention=recipient.person_name,
|
|
325
|
+
Address1=recipient.address_line1 or "",
|
|
326
|
+
Address2=recipient.address_line2,
|
|
327
|
+
Address3=None,
|
|
328
|
+
City=recipient.city or "",
|
|
329
|
+
State=lib.text(recipient.state_code, max=25) or "",
|
|
330
|
+
PostalCode=recipient.postal_code or "",
|
|
331
|
+
Country=recipient.country_code or "",
|
|
332
|
+
Phone=recipient.phone_number,
|
|
333
|
+
Email=recipient.email,
|
|
334
|
+
ConsigneeTaxID=recipient.tax_id,
|
|
335
|
+
),
|
|
336
|
+
ShippingLane=lib.identity(
|
|
337
|
+
ship_req.ShippingLaneType(Region=settings.region)
|
|
338
|
+
if settings.region
|
|
339
|
+
else None
|
|
340
|
+
),
|
|
341
|
+
ShipMethod=service,
|
|
342
|
+
OrderTotal=lib.identity(
|
|
343
|
+
customs.duty.declared_value
|
|
344
|
+
if customs.duty and customs.duty.declared_value
|
|
345
|
+
else options.declared_value.state
|
|
346
|
+
),
|
|
347
|
+
OrderInsuranceFreightTotal=options.landmark_order_insurance_freight_total.state,
|
|
348
|
+
ShipmentInsuranceFreight=options.landmark_shipment_insurance_freight.state,
|
|
349
|
+
ItemsCurrency=options.currency.state,
|
|
350
|
+
IsCommercialShipment="1" if customs.commercial_invoice else "0",
|
|
351
|
+
LabelFormat=label_format,
|
|
352
|
+
LabelDPI=("300" if label_format == "ZPL" else None),
|
|
353
|
+
LabelEncoding=label_encoding,
|
|
354
|
+
ShipOptions=None,
|
|
355
|
+
VendorInformation=lib.identity(
|
|
356
|
+
ship_req.VendorInformationType(
|
|
357
|
+
VendorName=vendor.company_name or vendor.person_name or "",
|
|
358
|
+
VendorPhone=vendor.phone_number or "",
|
|
359
|
+
VendorEmail=vendor.email or "",
|
|
360
|
+
VendorAddress1=vendor.address_line1 or "",
|
|
361
|
+
VendorAddress2=vendor.address_line2,
|
|
362
|
+
VendorCity=vendor.city or "",
|
|
363
|
+
VendorState=lib.text(vendor.state_code, max=25) or "",
|
|
364
|
+
VendorPostalCode=vendor.postal_code or "",
|
|
365
|
+
VendorCountry=vendor.country_code or "",
|
|
366
|
+
VendorLowValueTaxID=customs.options.low_value_tax_id.state,
|
|
367
|
+
VendorCCN=customs.options.ccn.state,
|
|
368
|
+
VendorBusinessNumber=customs.options.business_number.state,
|
|
369
|
+
VendorRGRNumber=customs.options.rgr_number.state,
|
|
370
|
+
VendorIOSSNumber=customs.options.ioss.state,
|
|
371
|
+
VendorEORINumber=customs.options.eori_number.state,
|
|
372
|
+
)
|
|
373
|
+
if payload.customs
|
|
374
|
+
else None
|
|
375
|
+
),
|
|
376
|
+
ReturnInformation=None,
|
|
377
|
+
FulfillmentAddress=ship_req.FulfillmentAddressType(
|
|
378
|
+
Name=shipper.company_name or shipper.person_name or "",
|
|
379
|
+
Attention=shipper.person_name,
|
|
380
|
+
Address1=shipper.address_line1 or "",
|
|
381
|
+
Address2=shipper.address_line2,
|
|
382
|
+
Address3=None,
|
|
383
|
+
City=shipper.city or "",
|
|
384
|
+
State=lib.text(shipper.state_code, max=25) or "",
|
|
385
|
+
PostalCode=shipper.postal_code or "",
|
|
386
|
+
Country=shipper.country_code or "",
|
|
387
|
+
),
|
|
388
|
+
SendReturnToAddress=lib.identity(
|
|
389
|
+
ship_req.SendReturnToAddressType(
|
|
390
|
+
Code=options.landmark_return_address_code.state,
|
|
391
|
+
Name=return_address.company_name
|
|
392
|
+
or return_address.person_name
|
|
393
|
+
or "",
|
|
394
|
+
Attention=return_address.person_name,
|
|
395
|
+
Address1=return_address.address_line1 or "",
|
|
396
|
+
Address2=return_address.address_line2,
|
|
397
|
+
Address3=None,
|
|
398
|
+
City=return_address.city or "",
|
|
399
|
+
State=lib.text(return_address.state_code, max=25) or "",
|
|
400
|
+
PostalCode=return_address.postal_code or "",
|
|
401
|
+
Country=return_address.country_code or "",
|
|
402
|
+
)
|
|
403
|
+
if payload.return_address
|
|
404
|
+
else None
|
|
405
|
+
),
|
|
406
|
+
AdditionalFields=None,
|
|
407
|
+
Packages=lib.identity(
|
|
408
|
+
ship_req.PackagesType(
|
|
409
|
+
Package=[
|
|
410
|
+
ship_req.PackageType(
|
|
411
|
+
WeightUnit=pkg.weight_unit.value,
|
|
412
|
+
Weight=pkg.weight[weight_unit.value],
|
|
413
|
+
DimensionsUnit=dim_unit.value,
|
|
414
|
+
Length=pkg.length[dim_unit.value],
|
|
415
|
+
Width=pkg.width[dim_unit.value],
|
|
416
|
+
Height=pkg.height[dim_unit.value],
|
|
417
|
+
PackageReference=pkg.parcel.reference_number,
|
|
418
|
+
)
|
|
419
|
+
for pkg in packages
|
|
420
|
+
]
|
|
421
|
+
)
|
|
422
|
+
if options.fulfilled_by_landmark.state is not True
|
|
423
|
+
else None
|
|
424
|
+
),
|
|
425
|
+
Items=lib.identity(
|
|
426
|
+
ship_req.ItemsType(
|
|
427
|
+
Item=[
|
|
428
|
+
ship_req.ItemType(
|
|
429
|
+
Sku=item.sku or "",
|
|
430
|
+
LineNumber=item.metadata.get("LineNumber"),
|
|
431
|
+
Quantity=item.quantity,
|
|
432
|
+
UnitPrice=item.value_amount,
|
|
433
|
+
Description=item.description or item.title or "",
|
|
434
|
+
HSCode=item.hs_code,
|
|
435
|
+
CountryOfOrigin=item.origin_country,
|
|
436
|
+
ContentCategory=item.category,
|
|
437
|
+
URL=item.product_url,
|
|
438
|
+
USMID=item.metadata.get("USMID"),
|
|
439
|
+
ReturnCustomsInfo=lib.identity(
|
|
440
|
+
ship_req.ReturnCustomsInfoType(
|
|
441
|
+
HSCode=item.hs_code,
|
|
442
|
+
HSRegionCode=item.origin_country,
|
|
443
|
+
)
|
|
444
|
+
if item.hs_code or item.origin_country
|
|
445
|
+
else None
|
|
446
|
+
),
|
|
447
|
+
DangerousGoodsInformation=None,
|
|
448
|
+
)
|
|
449
|
+
for item in items
|
|
450
|
+
]
|
|
451
|
+
)
|
|
452
|
+
if len(items) > 0
|
|
453
|
+
else None
|
|
454
|
+
),
|
|
455
|
+
FreightDetails=lib.identity(
|
|
456
|
+
ship_req.FreightDetailsType(
|
|
457
|
+
ProNumber=options.landmark_freight_pro_number.state or "",
|
|
458
|
+
PieceUnit=options.landmark_freight_piece_unit.state or "",
|
|
459
|
+
)
|
|
460
|
+
if options.landmark_freight_pro_number.state
|
|
461
|
+
and options.landmark_freight_piece_unit.state
|
|
462
|
+
else None
|
|
463
|
+
),
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
return lib.Serializable(
|
|
468
|
+
request,
|
|
469
|
+
lib.to_xml,
|
|
470
|
+
ctx=dict(
|
|
471
|
+
API=("Import" if is_import_request else "Ship"),
|
|
472
|
+
label_format=label_format,
|
|
473
|
+
label_encoding=label_encoding,
|
|
474
|
+
is_import_request=is_import_request,
|
|
475
|
+
produce_label=(produce_label if is_import_request else None),
|
|
476
|
+
),
|
|
477
|
+
)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Karrio Landmark Global tracking API implementation."""
|
|
2
|
+
|
|
3
|
+
import karrio.schemas.landmark.track_request as landmark_req
|
|
4
|
+
import karrio.schemas.landmark.track_response as landmark_res
|
|
5
|
+
|
|
6
|
+
import typing
|
|
7
|
+
import karrio.lib as lib
|
|
8
|
+
import karrio.core.models as models
|
|
9
|
+
import karrio.providers.landmark.error as error
|
|
10
|
+
import karrio.providers.landmark.utils as provider_utils
|
|
11
|
+
import karrio.providers.landmark.units as provider_units
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Supported datetime formats for Landmark Global
|
|
15
|
+
DATETIME_FORMATS = [
|
|
16
|
+
"%m/%d/%Y %I:%M %p", # 10/01/2025 03:02 PM
|
|
17
|
+
"%-m/%d/%Y %I:%M %p", # 0/01/2025 03:03 PM (single digit month)
|
|
18
|
+
"%Y-%m-%d %H:%M:%S", # 2019-01-01 13:21:45
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_tracking_response(
|
|
23
|
+
_response: lib.Deserializable[typing.List[typing.Tuple[str, lib.Element]]],
|
|
24
|
+
settings: provider_utils.Settings,
|
|
25
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
|
26
|
+
responses = _response.deserialize()
|
|
27
|
+
|
|
28
|
+
messages: typing.List[models.Message] = sum(
|
|
29
|
+
[
|
|
30
|
+
error.parse_error_response(
|
|
31
|
+
response, settings, tracking_number=tracking_number
|
|
32
|
+
)
|
|
33
|
+
for tracking_number, response in responses
|
|
34
|
+
],
|
|
35
|
+
start=[],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
tracking_details = [
|
|
39
|
+
_extract_details(details, settings)
|
|
40
|
+
for _, details in responses
|
|
41
|
+
if len(lib.find_element("TrackingNumber", details)) > 0
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
return tracking_details, messages
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _extract_details(
|
|
48
|
+
data: lib.Element,
|
|
49
|
+
settings: provider_utils.Settings,
|
|
50
|
+
) -> models.TrackingDetails:
|
|
51
|
+
"""Extract tracking details from carrier response data"""
|
|
52
|
+
|
|
53
|
+
details = lib.find_element(
|
|
54
|
+
"ShipmentDetails", data, landmark_res.ShipmentDetailsType, first=True
|
|
55
|
+
)
|
|
56
|
+
package = lib.find_element("Package", data, landmark_res.PackageType, first=True)
|
|
57
|
+
|
|
58
|
+
# Transform events to TrackingEvent models
|
|
59
|
+
tracking_events = lib.sort_events(
|
|
60
|
+
[
|
|
61
|
+
models.TrackingEvent(
|
|
62
|
+
date=lib.fdate(event.DateTime, try_formats=DATETIME_FORMATS),
|
|
63
|
+
time=lib.flocaltime(
|
|
64
|
+
event.DateTime,
|
|
65
|
+
output_format="%I:%M %p",
|
|
66
|
+
try_formats=DATETIME_FORMATS,
|
|
67
|
+
),
|
|
68
|
+
description=event.Status,
|
|
69
|
+
code=event.EventCode,
|
|
70
|
+
location=event.Location,
|
|
71
|
+
)
|
|
72
|
+
for event in package.Events.Event
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Determine status based on the most recent event
|
|
77
|
+
latest_event = tracking_events[0] if tracking_events else None
|
|
78
|
+
status = next(
|
|
79
|
+
(
|
|
80
|
+
status.name
|
|
81
|
+
for status in list(provider_units.TrackingStatus)
|
|
82
|
+
if latest_event and latest_event.code in status.value
|
|
83
|
+
),
|
|
84
|
+
provider_units.TrackingStatus.in_transit.name,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return models.TrackingDetails(
|
|
88
|
+
carrier_id=settings.carrier_id,
|
|
89
|
+
carrier_name=settings.carrier_name,
|
|
90
|
+
tracking_number=package.LandmarkTrackingNumber,
|
|
91
|
+
events=tracking_events,
|
|
92
|
+
delivered=status == "delivered",
|
|
93
|
+
status=status,
|
|
94
|
+
info=models.TrackingInfo(
|
|
95
|
+
carrier_tracking_link=settings.tracking_url.format(
|
|
96
|
+
package.LandmarkTrackingNumber
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
meta=lib.to_dict(
|
|
100
|
+
dict(
|
|
101
|
+
last_mile_tracking_number=package.TrackingNumber,
|
|
102
|
+
last_mile_carrier=lib.identity(
|
|
103
|
+
"landmark"
|
|
104
|
+
if "routed" in details.EndDeliveryCarrier.lower()
|
|
105
|
+
else details.EndDeliveryCarrier
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def tracking_request(
|
|
113
|
+
payload: models.TrackingRequest,
|
|
114
|
+
settings: provider_utils.Settings,
|
|
115
|
+
) -> lib.Serializable:
|
|
116
|
+
"""Create tracking requests for the carrier API"""
|
|
117
|
+
# Create a request for each tracking number
|
|
118
|
+
requests = [
|
|
119
|
+
landmark_req.TrackRequest(
|
|
120
|
+
Login=landmark_req.LoginType(
|
|
121
|
+
Username=settings.username,
|
|
122
|
+
Password=settings.password,
|
|
123
|
+
),
|
|
124
|
+
Test=settings.test_mode,
|
|
125
|
+
ClientID=settings.client_id,
|
|
126
|
+
Reference=payload.reference,
|
|
127
|
+
TrackingNumber=tracking_number,
|
|
128
|
+
PackageReference=None,
|
|
129
|
+
RetrievalType="Historical",
|
|
130
|
+
)
|
|
131
|
+
for tracking_number in payload.tracking_numbers
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
return lib.Serializable(
|
|
135
|
+
requests,
|
|
136
|
+
lambda __: [
|
|
137
|
+
lib.typed(number=_.TrackingNumber, request=lib.to_xml(_)) for _ in __
|
|
138
|
+
],
|
|
139
|
+
)
|