Skip to content

Commit a536e2b

Browse files
committed
Add dpd_fr transporter with api v2
1 parent 46b0897 commit a536e2b

File tree

4 files changed

+327
-0
lines changed

4 files changed

+327
-0
lines changed

roulier/carriersv2/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
from .dpd_fr import transporter
12
from .mondialrelay import transporter

roulier/carriersv2/dpd_fr/__init__.py

Whitespace-only changes.

roulier/carriersv2/dpd_fr/schema.py

+294
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
from base64 import b64encode
5+
from datetime import date
6+
from enum import Enum
7+
from pydantic.functional_validators import AfterValidator
8+
from typing_extensions import Annotated
9+
from zeep import xsd
10+
11+
from ..helpers import prefix, clean_empty, REMOVED
12+
from ..schema import (
13+
LabelInput,
14+
Address,
15+
LabelOutput,
16+
Auth,
17+
Service,
18+
Parcel,
19+
ParcelLabel,
20+
Label,
21+
Tracking,
22+
)
23+
24+
25+
class Format(str, Enum):
26+
PNG = "PNG"
27+
PDF = "PDF"
28+
PDF_A6 = "PDF_A6"
29+
ZPL = "ZPL"
30+
ZPL300 = "ZPL300"
31+
ZPL_A6 = "ZPL_A6"
32+
ZPL300_A6 = "ZPL300_A6"
33+
EPL = "EPL"
34+
35+
36+
class Notifications(str, Enum):
37+
No = "No"
38+
Predict = "Predict"
39+
AutomacticSMS = "AutomacticSMS"
40+
AutomacticMail = "AutomacticMail"
41+
42+
43+
class Product(str, Enum):
44+
DPD_Classic = "DPD_Classic"
45+
DPD_Predict = "DPD_Predict"
46+
DPD_Relais = "DPD_Relais"
47+
48+
49+
class DpdFrAuth(Auth):
50+
login: str
51+
52+
def soap(self):
53+
return xsd.Element(
54+
"UserCredentials",
55+
xsd.ComplexType(
56+
[
57+
xsd.Element("userid", xsd.String()),
58+
xsd.Element("password", xsd.String()),
59+
]
60+
),
61+
)(
62+
userid=self.login,
63+
password=self.password,
64+
)
65+
66+
67+
def dpd_service_validator(service):
68+
if (
69+
service.product in (Product.DPD_Predict, Product.DPD_Classic)
70+
and service.pickupLocationId
71+
):
72+
raise ValueError(f"pickupLocationId can't be used with {service.product}")
73+
74+
if service.product == Product.DPD_Predict:
75+
if service.notifications != Notifications.Predict:
76+
raise ValueError("Predict notifications must be set to Predict")
77+
else:
78+
if service.notifications == Notifications.Predict:
79+
raise ValueError(
80+
f"Predict notifications can't be used with {service.product}"
81+
)
82+
if service.product == Product.DPD_Relais and not service.pickupLocationId:
83+
raise ValueError("pickupLocationId is mandatory for Relais")
84+
85+
return service
86+
87+
88+
class DpdFrService(Service):
89+
labelFormat: Format = Format.PDF
90+
agencyId: str
91+
customerCountry: str
92+
customerId: str
93+
shippingDate: date | None = None
94+
notifications: Notifications = Notifications.No
95+
product: Product = Product.DPD_Classic
96+
pickupLocationId: str | None = None
97+
98+
def soap(self, client, phone, email, ref):
99+
service = client.get_type("ns0:StdServices")
100+
contact = client.get_type("ns0:Contact")
101+
label_type = client.get_type("ns0:LabelType")
102+
ref_in_barcode = client.get_type("ns0:ReferenceInBarcode")
103+
104+
service_kwargs = {
105+
"contact": contact(sms=phone, email=email, type=self.notifications.value),
106+
}
107+
108+
if self.product == Product.DPD_Relais:
109+
parcel_shop = client.get_type("ns0:ParcelShop")
110+
shop_address = client.get_type("ns0:ShopAddress")
111+
service_kwargs.update(
112+
{
113+
"parcelShop": parcel_shop(
114+
Shopaddress=shop_address(
115+
shopid=self.pickupLocationId,
116+
)
117+
)
118+
}
119+
)
120+
121+
return {
122+
"customer_countrycode": self.customerCountry,
123+
"customer_centernumber": self.agencyId,
124+
"customer_number": self.customerId,
125+
"referencenumber": self.reference1,
126+
"reference2": self.reference2 or ref,
127+
"reference3": self.reference3,
128+
"refnrasbarcode": str(bool(self.reference2)).lower(),
129+
"referenceInBarcode": ref_in_barcode(type="Reference2"),
130+
"shippingdate": self.shippingDate.strftime("%d/%m/%Y"),
131+
"labelType": label_type(
132+
type=(
133+
self.labelFormat.value
134+
if self.labelFormat != Format.PNG
135+
else "Default"
136+
)
137+
),
138+
"services": service(
139+
**service_kwargs,
140+
),
141+
}
142+
143+
144+
class DpdFrParcel(Parcel):
145+
def soap(self):
146+
return {
147+
"weight": self.weight,
148+
}
149+
150+
151+
class DpdFrAddress(Address):
152+
country: str
153+
zip: str
154+
city: str
155+
street1: str
156+
name2: str | None = None
157+
name3: str | None = None
158+
name4: str | None = None
159+
door1: str | None = None
160+
door2: str | None = None
161+
intercom: str | None = None
162+
163+
def soap(self, client):
164+
address = client.get_type("ns0:Address")
165+
address_info = client.get_type("ns0:AddressInfo")
166+
return {
167+
"address": address(
168+
name=", ".join(
169+
[part for part in (self.name, self.company) if part],
170+
)[0:35],
171+
countryPrefix=self.country,
172+
zipCode=self.zip,
173+
city=self.city,
174+
street=", ".join(
175+
[part for part in (self.street1, self.street2) if part]
176+
)[0:70],
177+
phoneNumber=self.phone,
178+
),
179+
"info": address_info(
180+
contact=self.company,
181+
name2=self.name2,
182+
name3=self.name3,
183+
name4=self.name4,
184+
digicode1=self.door1,
185+
digicode2=self.door2,
186+
intercomid=self.intercom,
187+
vinfo1=(
188+
self.delivery_instructions[0:35]
189+
if getattr(self, "delivery_instructions", None)
190+
else None
191+
),
192+
vinfo2=(
193+
self.delivery_instructions[35:70]
194+
if getattr(self, "delivery_instructions", None)
195+
and len(self.delivery_instructions) > 35
196+
else None
197+
),
198+
),
199+
}
200+
201+
202+
class DpdFrFromAddress(DpdFrAddress):
203+
phone: str
204+
205+
def soap(self, client):
206+
rv = super().soap(client)
207+
return {
208+
"shipperaddress": rv["address"],
209+
"shipperinfo": rv["info"],
210+
}
211+
212+
213+
class DpdFrToAddress(DpdFrAddress):
214+
def soap(self, client):
215+
rv = super().soap(client)
216+
return {
217+
"receiveraddress": rv["address"],
218+
"receiverinfo": rv["info"],
219+
}
220+
221+
222+
class DpdFrLabelInput(LabelInput):
223+
auth: DpdFrAuth
224+
service: Annotated[DpdFrService, AfterValidator(dpd_service_validator)]
225+
parcels: list[DpdFrParcel]
226+
from_address: DpdFrFromAddress
227+
to_address: DpdFrToAddress
228+
229+
def soap(self, client):
230+
request = client.get_type("ns0:StdShipmentLabelRequest")
231+
request_kwargs = {
232+
**self.service.soap(
233+
client,
234+
self.to_address.phone,
235+
self.to_address.email,
236+
self.parcels[0].reference,
237+
),
238+
**self.parcels[0].soap(),
239+
**self.from_address.soap(client),
240+
**self.to_address.soap(client),
241+
}
242+
243+
return {
244+
"_soapheaders": [self.auth.soap()],
245+
"request": request(**request_kwargs),
246+
}
247+
248+
249+
class DpdFrLabel(Label):
250+
@classmethod
251+
def from_soap(cls, result, format):
252+
return cls.model_construct(
253+
data=b64encode(result["label"]),
254+
name=f"{format.value} Label",
255+
type=format.value,
256+
)
257+
258+
259+
class DpdFrTracking(Tracking):
260+
@classmethod
261+
def from_soap(cls, result):
262+
return cls.model_construct(
263+
number=result["BarcodeId"],
264+
)
265+
266+
267+
class DpdFrParcelLabel(ParcelLabel):
268+
label: DpdFrLabel | None = None
269+
270+
@classmethod
271+
def from_soap(cls, id, shipment, label, format):
272+
return cls.model_construct(
273+
id=id,
274+
label=DpdFrLabel.from_soap(label, format),
275+
reference=shipment["Shipment"]["BarCode"],
276+
tracking=DpdFrTracking.from_soap(shipment["Shipment"]),
277+
)
278+
279+
280+
class DpdFrLabelOutput(LabelOutput):
281+
parcels: list[DpdFrParcelLabel]
282+
283+
@classmethod
284+
def from_soap(cls, result, format):
285+
shipments = result["shipments"]["ShipmentBc"]
286+
labels = result["labels"]["Label"]
287+
assert len(shipments) == len(labels), "Mismatched shipments and labels"
288+
parcels = zip(shipments, labels)
289+
return cls.model_construct(
290+
parcels=[
291+
DpdFrParcelLabel.from_soap(i, shipment, label, format)
292+
for i, (shipment, label) in enumerate(parcels)
293+
]
294+
)
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
import zeep
5+
6+
from ..api import Transporter, action
7+
from ...exception import CarrierError
8+
from .schema import DpdFrLabelInput, DpdFrLabelOutput
9+
10+
11+
class DpdFr(Transporter):
12+
__key__ = "dpd_fr"
13+
__url__ = (
14+
"https://e-station.cargonet.software/dpd-eprintwebservice/eprintwebservice.asmx"
15+
)
16+
__url_test__ = "https://e-station-testenv.cargonet.software/eprintwebservice/eprintwebservice.asmx"
17+
__ns_prefix__ = "http://www.cargonet.software"
18+
19+
def _get_client(self, is_test):
20+
url = self.__url_test__ if is_test else self.__url__
21+
client = zeep.Client(wsdl=f"{url}?WSDL")
22+
client.set_ns_prefix(None, self.__ns_prefix__)
23+
return client
24+
25+
@action
26+
def get_label(self, input: DpdFrLabelInput) -> DpdFrLabelOutput:
27+
client = self._get_client(input.auth.isTest)
28+
try:
29+
result = client.service.CreateShipmentWithLabelsBc(**input.soap(client))
30+
except zeep.exceptions.Fault as e:
31+
raise CarrierError(None, msg=str(e)) from e
32+
return DpdFrLabelOutput.from_soap(result, input.service.labelFormat)

0 commit comments

Comments
 (0)