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,250 @@
|
|
|
1
|
+
"""Karrio ParcelOne shipment creation implementation."""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
import karrio.schemas.parcelone as parcelone
|
|
5
|
+
import karrio.lib as lib
|
|
6
|
+
import karrio.core.models as models
|
|
7
|
+
import karrio.providers.parcelone.error as error
|
|
8
|
+
import karrio.providers.parcelone.utils as provider_utils
|
|
9
|
+
import karrio.providers.parcelone.units as provider_units
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_shipment_response(
|
|
13
|
+
_response: lib.Deserializable[dict],
|
|
14
|
+
settings: provider_utils.Settings,
|
|
15
|
+
) -> typing.Tuple[typing.Optional[models.ShipmentDetails], typing.List[models.Message]]:
|
|
16
|
+
"""Parse shipment creation response from ParcelOne REST API."""
|
|
17
|
+
response = _response.deserialize()
|
|
18
|
+
messages = error.parse_error_response(response, settings)
|
|
19
|
+
shipment = lib.identity(
|
|
20
|
+
_extract_details(response, settings, ctx=_response.ctx)
|
|
21
|
+
if response.get("success") == 1 and response.get("results")
|
|
22
|
+
else None
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return shipment, messages
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _extract_details(
|
|
29
|
+
data: dict,
|
|
30
|
+
settings: provider_utils.Settings,
|
|
31
|
+
ctx: dict = None,
|
|
32
|
+
) -> typing.Optional[models.ShipmentDetails]:
|
|
33
|
+
"""Extract shipment details from API response."""
|
|
34
|
+
result = lib.to_object(parcelone.ShipmentResultType, data.get("results") or {})
|
|
35
|
+
|
|
36
|
+
# Check action result for success
|
|
37
|
+
action = result.ActionResult
|
|
38
|
+
if action is None or action.Success != 1:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
# Get tracking IDs from package results
|
|
42
|
+
packages = result.PackageResults or []
|
|
43
|
+
tracking_ids = [p.TrackingID for p in packages if p.TrackingID]
|
|
44
|
+
tracking_urls = [p.TrackingURL for p in packages if p.TrackingURL]
|
|
45
|
+
|
|
46
|
+
# Get labels from package results - use list comprehension
|
|
47
|
+
labels = [p.Label for p in packages if p.Label]
|
|
48
|
+
|
|
49
|
+
# Bundle labels if multiple, otherwise use single label
|
|
50
|
+
label_format = ctx.get("label_format") if ctx else "PDF"
|
|
51
|
+
label = lib.bundle_base64(labels, label_format) if len(labels) > 1 else lib.failsafe(lambda: labels[0])
|
|
52
|
+
|
|
53
|
+
# Calculate total charges if available
|
|
54
|
+
total_charge = lib.failsafe(lambda: float(result.TotalCharges.Value)) if result.TotalCharges else None
|
|
55
|
+
|
|
56
|
+
return models.ShipmentDetails(
|
|
57
|
+
carrier_name=settings.carrier_name,
|
|
58
|
+
carrier_id=settings.carrier_id,
|
|
59
|
+
tracking_number=action.TrackingID or lib.failsafe(lambda: tracking_ids[0]),
|
|
60
|
+
shipment_identifier=action.ShipmentID or action.TrackingID,
|
|
61
|
+
label_type=label_format,
|
|
62
|
+
docs=models.Documents(label=label),
|
|
63
|
+
meta=dict(
|
|
64
|
+
shipment_id=action.ShipmentID,
|
|
65
|
+
shipment_ref=action.ShipmentRef,
|
|
66
|
+
tracking_numbers=tracking_ids,
|
|
67
|
+
tracking_urls=tracking_urls,
|
|
68
|
+
label_url=result.LabelURL,
|
|
69
|
+
carrier_tracking_link=settings.tracking_link.format(
|
|
70
|
+
action.TrackingID or lib.failsafe(lambda: tracking_ids[0])
|
|
71
|
+
),
|
|
72
|
+
total_charge=total_charge,
|
|
73
|
+
currency=lib.failsafe(lambda: result.TotalCharges.Currency) or "EUR",
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def shipment_request(
|
|
79
|
+
payload: models.ShipmentRequest,
|
|
80
|
+
settings: provider_utils.Settings,
|
|
81
|
+
) -> lib.Serializable:
|
|
82
|
+
"""Create ParcelOne shipment request."""
|
|
83
|
+
shipper = lib.to_address(payload.shipper)
|
|
84
|
+
recipient = lib.to_address(payload.recipient)
|
|
85
|
+
packages = lib.to_packages(payload.parcels, required=["weight"])
|
|
86
|
+
options = lib.to_shipping_options(
|
|
87
|
+
payload.options,
|
|
88
|
+
package_options=packages.options,
|
|
89
|
+
initializer=provider_units.shipping_options_initializer,
|
|
90
|
+
)
|
|
91
|
+
customs = payload.customs
|
|
92
|
+
is_international = shipper.country_code != recipient.country_code
|
|
93
|
+
|
|
94
|
+
# Parse service to get CEP and product
|
|
95
|
+
service = provider_units.ShippingService.map(payload.service)
|
|
96
|
+
service_code = service.value_or_key
|
|
97
|
+
cep_id, product_id = provider_units.parse_service_code(service_code)
|
|
98
|
+
cep_id = cep_id or settings.connection_config.cep_id.state
|
|
99
|
+
product_id = product_id or settings.connection_config.product_id.state
|
|
100
|
+
|
|
101
|
+
# Determine label format
|
|
102
|
+
label_format = provider_units.LabelFormat.map(
|
|
103
|
+
payload.label_type or settings.connection_config.label_format.state
|
|
104
|
+
).value or "PDF"
|
|
105
|
+
label_size = settings.connection_config.label_size.state or "A6"
|
|
106
|
+
|
|
107
|
+
request = parcelone.ShippingDataRequestType(
|
|
108
|
+
ShippingData=parcelone.ShipmentType(
|
|
109
|
+
ShipmentRef=payload.reference,
|
|
110
|
+
CEPID=cep_id,
|
|
111
|
+
ProductID=product_id,
|
|
112
|
+
MandatorID=settings.mandator_id,
|
|
113
|
+
ConsignerID=settings.consigner_id,
|
|
114
|
+
ShipToData=parcelone.ShipToType(
|
|
115
|
+
Name1=recipient.company_name or recipient.person_name,
|
|
116
|
+
Name2=recipient.person_name if recipient.company_name else None,
|
|
117
|
+
ShipmentAddress=parcelone.AddressType(
|
|
118
|
+
Street=recipient.street,
|
|
119
|
+
Streetno=recipient.street_number,
|
|
120
|
+
PostalCode=recipient.postal_code,
|
|
121
|
+
City=recipient.city,
|
|
122
|
+
State=recipient.state_code,
|
|
123
|
+
Country=recipient.country_code,
|
|
124
|
+
),
|
|
125
|
+
ShipmentContact=parcelone.ContactType(
|
|
126
|
+
Email=recipient.email,
|
|
127
|
+
Phone=recipient.phone_number,
|
|
128
|
+
),
|
|
129
|
+
PrivateAddressIndicator=1 if recipient.residential else 0,
|
|
130
|
+
),
|
|
131
|
+
ShipFromData=parcelone.ShipFromType(
|
|
132
|
+
Name1=shipper.company_name or shipper.person_name,
|
|
133
|
+
Name2=shipper.person_name if shipper.company_name else None,
|
|
134
|
+
ShipmentAddress=parcelone.AddressType(
|
|
135
|
+
Street=shipper.street,
|
|
136
|
+
Streetno=shipper.street_number,
|
|
137
|
+
PostalCode=shipper.postal_code,
|
|
138
|
+
City=shipper.city,
|
|
139
|
+
State=shipper.state_code,
|
|
140
|
+
Country=shipper.country_code,
|
|
141
|
+
),
|
|
142
|
+
ShipmentContact=parcelone.ContactType(
|
|
143
|
+
Email=shipper.email,
|
|
144
|
+
Phone=shipper.phone_number,
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
ReturnShipmentIndicator=1 if options.is_return.state else 0,
|
|
148
|
+
PrintLabel=1,
|
|
149
|
+
LabelFormat=parcelone.FormatType(
|
|
150
|
+
Type=label_format,
|
|
151
|
+
Size=label_size,
|
|
152
|
+
),
|
|
153
|
+
PrintDocuments=1 if is_international else 0,
|
|
154
|
+
Software="Karrio",
|
|
155
|
+
Packages=[
|
|
156
|
+
parcelone.ShipmentPackageType(
|
|
157
|
+
PackageRef=pkg.parcel.id or str(index),
|
|
158
|
+
PackageWeight=parcelone.MeasurementType(
|
|
159
|
+
Value=str(pkg.weight.KG),
|
|
160
|
+
Unit="kg",
|
|
161
|
+
),
|
|
162
|
+
PackageDimensions=(
|
|
163
|
+
parcelone.DimensionsType(
|
|
164
|
+
Length=str(pkg.length.CM),
|
|
165
|
+
Width=str(pkg.width.CM),
|
|
166
|
+
Height=str(pkg.height.CM),
|
|
167
|
+
)
|
|
168
|
+
if pkg.length.CM and pkg.width.CM and pkg.height.CM
|
|
169
|
+
else None
|
|
170
|
+
),
|
|
171
|
+
IntDocData=(
|
|
172
|
+
parcelone.IntDocDataType(
|
|
173
|
+
InvoiceNo=customs.invoice,
|
|
174
|
+
ItemCategory=1,
|
|
175
|
+
CustomDetails=[
|
|
176
|
+
parcelone.CustomDetailType(
|
|
177
|
+
Contents=item.description or item.title,
|
|
178
|
+
Quantity=item.quantity,
|
|
179
|
+
ItemValue=item.value_amount,
|
|
180
|
+
NetWeight=item.weight,
|
|
181
|
+
Origin=item.origin_country,
|
|
182
|
+
TariffNumber=item.hs_code,
|
|
183
|
+
)
|
|
184
|
+
for item in (customs.commodities or [])
|
|
185
|
+
] if customs.commodities else None,
|
|
186
|
+
)
|
|
187
|
+
if is_international and customs
|
|
188
|
+
else None
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
for index, pkg in enumerate(packages, 1)
|
|
192
|
+
],
|
|
193
|
+
Services=[
|
|
194
|
+
*(
|
|
195
|
+
[
|
|
196
|
+
parcelone.ShipmentServiceType(
|
|
197
|
+
ServiceID="COD",
|
|
198
|
+
Value=parcelone.AmountType(
|
|
199
|
+
Value=str(options.cash_on_delivery.state),
|
|
200
|
+
Currency=options.parcelone_cod_currency.state or "EUR",
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
]
|
|
204
|
+
if options.cash_on_delivery.state
|
|
205
|
+
else []
|
|
206
|
+
),
|
|
207
|
+
*(
|
|
208
|
+
[
|
|
209
|
+
parcelone.ShipmentServiceType(
|
|
210
|
+
ServiceID="INS",
|
|
211
|
+
Value=parcelone.AmountType(
|
|
212
|
+
Value=str(options.insurance.state),
|
|
213
|
+
Currency=options.parcelone_insurance_currency.state or "EUR",
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
]
|
|
217
|
+
if options.insurance.state
|
|
218
|
+
else []
|
|
219
|
+
),
|
|
220
|
+
*([parcelone.ShipmentServiceType(ServiceID="SIG")] if options.signature_required.state else []),
|
|
221
|
+
*([parcelone.ShipmentServiceType(ServiceID="SDO")] if options.saturday_delivery.state else []),
|
|
222
|
+
*(
|
|
223
|
+
[
|
|
224
|
+
parcelone.ShipmentServiceType(
|
|
225
|
+
ServiceID="MAIL",
|
|
226
|
+
Parameters=options.parcelone_notification_email.state,
|
|
227
|
+
)
|
|
228
|
+
]
|
|
229
|
+
if options.parcelone_notification_email.state
|
|
230
|
+
else []
|
|
231
|
+
),
|
|
232
|
+
*(
|
|
233
|
+
[
|
|
234
|
+
parcelone.ShipmentServiceType(
|
|
235
|
+
ServiceID="SMS",
|
|
236
|
+
Parameters=options.parcelone_notification_sms.state,
|
|
237
|
+
)
|
|
238
|
+
]
|
|
239
|
+
if options.parcelone_notification_sms.state
|
|
240
|
+
else []
|
|
241
|
+
),
|
|
242
|
+
],
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return lib.Serializable(
|
|
247
|
+
request,
|
|
248
|
+
lib.to_dict,
|
|
249
|
+
dict(label_format=label_format),
|
|
250
|
+
)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Karrio ParcelOne tracking implementation."""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
import karrio.schemas.parcelone as parcelone
|
|
5
|
+
import karrio.lib as lib
|
|
6
|
+
import karrio.core.models as models
|
|
7
|
+
import karrio.providers.parcelone.error as error
|
|
8
|
+
import karrio.providers.parcelone.utils as provider_utils
|
|
9
|
+
import karrio.providers.parcelone.units as provider_units
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_tracking_response(
|
|
13
|
+
_response: lib.Deserializable[typing.List[typing.Tuple[str, dict]]],
|
|
14
|
+
settings: provider_utils.Settings,
|
|
15
|
+
) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
|
|
16
|
+
"""Parse tracking response from ParcelOne TrackLMC REST API."""
|
|
17
|
+
responses = _response.deserialize()
|
|
18
|
+
messages: typing.List[models.Message] = []
|
|
19
|
+
tracking_details: typing.List[models.TrackingDetails] = []
|
|
20
|
+
|
|
21
|
+
for tracking_number, response in responses:
|
|
22
|
+
# Parse errors for this response
|
|
23
|
+
response_messages = error.parse_error_response(
|
|
24
|
+
response,
|
|
25
|
+
settings,
|
|
26
|
+
tracking_number=tracking_number,
|
|
27
|
+
)
|
|
28
|
+
messages.extend(response_messages)
|
|
29
|
+
|
|
30
|
+
# Extract tracking details if successful
|
|
31
|
+
if response.get("success") == 1 and response.get("results"):
|
|
32
|
+
details = _extract_tracking_details(
|
|
33
|
+
response.get("results"),
|
|
34
|
+
tracking_number,
|
|
35
|
+
settings,
|
|
36
|
+
)
|
|
37
|
+
if details:
|
|
38
|
+
tracking_details.append(details)
|
|
39
|
+
|
|
40
|
+
return tracking_details, messages
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _extract_tracking_details(
|
|
44
|
+
result: dict,
|
|
45
|
+
tracking_number: str,
|
|
46
|
+
settings: provider_utils.Settings,
|
|
47
|
+
) -> typing.Optional[models.TrackingDetails]:
|
|
48
|
+
"""Extract tracking details from API response."""
|
|
49
|
+
tracking_result = lib.to_object(parcelone.TrackingResultType, result)
|
|
50
|
+
|
|
51
|
+
# Parse events
|
|
52
|
+
events = sorted(
|
|
53
|
+
[
|
|
54
|
+
_parse_tracking_event(event)
|
|
55
|
+
for event in (tracking_result.Events or [])
|
|
56
|
+
],
|
|
57
|
+
key=lambda e: e.timestamp or e.date or "",
|
|
58
|
+
reverse=True,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if not events:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
latest_event = events[0] if events else None
|
|
65
|
+
status = lib.identity(
|
|
66
|
+
provider_units.TrackingStatus.find(
|
|
67
|
+
tracking_result.StatusCode or (latest_event.code if latest_event else None)
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return models.TrackingDetails(
|
|
72
|
+
carrier_name=settings.carrier_name,
|
|
73
|
+
carrier_id=settings.carrier_id,
|
|
74
|
+
tracking_number=tracking_number,
|
|
75
|
+
events=events,
|
|
76
|
+
delivered=(status.name == "delivered") if status else False,
|
|
77
|
+
status=status.name if status else None,
|
|
78
|
+
estimated_delivery=lib.fdate(tracking_result.EstimatedDelivery),
|
|
79
|
+
info=models.TrackingInfo(
|
|
80
|
+
carrier_tracking_link=settings.tracking_link.format(tracking_number),
|
|
81
|
+
signed_by=tracking_result.SignedBy,
|
|
82
|
+
),
|
|
83
|
+
meta=dict(
|
|
84
|
+
carrier_tracking_id=tracking_result.CarrierTrackingID,
|
|
85
|
+
last_mile_carrier=tracking_result.CarrierIDLMC,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parse_tracking_event(event: parcelone.TrackingEventType) -> models.TrackingEvent:
|
|
91
|
+
"""Parse a single tracking event."""
|
|
92
|
+
datetime_str = event.DateTime or ""
|
|
93
|
+
date = lib.fdate(
|
|
94
|
+
datetime_str,
|
|
95
|
+
try_formats=["%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"],
|
|
96
|
+
)
|
|
97
|
+
time = lib.flocaltime(
|
|
98
|
+
datetime_str,
|
|
99
|
+
try_formats=["%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"],
|
|
100
|
+
)
|
|
101
|
+
status = provider_units.TrackingStatus.find(event.StatusCode)
|
|
102
|
+
|
|
103
|
+
return models.TrackingEvent(
|
|
104
|
+
date=date,
|
|
105
|
+
time=time,
|
|
106
|
+
description=event.Description or event.Status,
|
|
107
|
+
code=event.StatusCode,
|
|
108
|
+
location=event.Location,
|
|
109
|
+
timestamp=lib.fiso_timestamp(
|
|
110
|
+
datetime_str,
|
|
111
|
+
current_format="%Y-%m-%dT%H:%M:%S",
|
|
112
|
+
),
|
|
113
|
+
status=status.name if status else None,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def tracking_request(
|
|
118
|
+
payload: models.TrackingRequest,
|
|
119
|
+
settings: provider_utils.Settings,
|
|
120
|
+
) -> lib.Serializable:
|
|
121
|
+
"""Create ParcelOne tracking request.
|
|
122
|
+
|
|
123
|
+
The TrackLMC API uses: GET /tracking/{CarrierIDLMC}/{TrackingID}
|
|
124
|
+
For ParcelOne tracking numbers, CarrierIDLMC is typically not needed
|
|
125
|
+
as the tracking ID encodes the carrier information.
|
|
126
|
+
"""
|
|
127
|
+
# For each tracking number, we'll need to make a separate API call
|
|
128
|
+
# The carrier_id can be passed as an option if known
|
|
129
|
+
requests = [
|
|
130
|
+
dict(
|
|
131
|
+
tracking_id=tracking_number,
|
|
132
|
+
carrier_id=lib.identity(
|
|
133
|
+
payload.options.get("carrier_id")
|
|
134
|
+
or payload.options.get(f"{tracking_number}_carrier_id")
|
|
135
|
+
or "PA1" # Default to ParcelOne
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
for tracking_number in payload.tracking_numbers
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
return lib.Serializable(requests, lib.to_dict)
|