diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bfd7ac5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# Configuration for known file extensions +[*.{css,js,json,less,md,py,rst,sass,scss,xml,yaml,yml}] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{json,yml,yaml,rst,md}] +indent_size = 2 + +# Do not configure editor for libs and autogenerated content +[{*/static/{lib,src/lib}/**,*/static/description/index.html,*/readme/../README.rst}] +charset = unset +end_of_line = unset +indent_size = unset +indent_style = unset +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/roulier/carriers/__init__.py b/roulier/carriers/__init__.py index 139754f..138ce4a 100755 --- a/roulier/carriers/__init__.py +++ b/roulier/carriers/__init__.py @@ -4,6 +4,7 @@ from . import chronopost_fr from . import dpd_fr_soap from . import dpd_fr +from . import dhl_express from . import geodis_fr from . import mondialrelay from . import mondialrelay_fr diff --git a/roulier/carriers/dhl_express/__init__.py b/roulier/carriers/dhl_express/__init__.py new file mode 100644 index 0000000..a0c5503 --- /dev/null +++ b/roulier/carriers/dhl_express/__init__.py @@ -0,0 +1 @@ +from . import carrier diff --git a/roulier/carriers/dhl_express/carrier.py b/roulier/carriers/dhl_express/carrier.py new file mode 100644 index 0000000..d871879 --- /dev/null +++ b/roulier/carriers/dhl_express/carrier.py @@ -0,0 +1,104 @@ +# Copyright 2026 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +import requests + +from ...carrier import Carrier, action +from ...exception import CarrierError +from .schema import DHLExpressLabelInput, DHLExpressLabelOutput + +_logger = logging.getLogger(__name__) + + +def int_maybe(value): + try: + return int(value) + except ValueError: + return value + + +class DHLExpress(Carrier): + __key__ = "dhl_express" + __url__ = "https://express.api.dhl.com/mydhlapi" + __url_test__ = "https://express.api.dhl.com/mydhlapi/test" + + __ref__ = "https://developer.dhl.com/api-reference/mydhl-api-dhl-express" + + def _get_url(self, is_test): + return self.__url_test__ if is_test else self.__url__ + + def _raise_for_status(self, response): + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + try: + json = response.json() + if "message" in json: + id = 0 + detail = json.get("detail", "") + if detail and ":" in detail and detail.split(":", 1)[0].isdigit(): + id, detail = detail.split(":", 1) + id = int_maybe(id.strip()) + detail = detail.strip() + msg = [ + { + "id": id, + "message": f"{json['message']} ({detail})", + } + ] + + if "additionalDetails" in json: + for additional_detail in json["additionalDetails"]: + if ( + "message" in additional_detail + and ":" in additional_detail["message"] + and additional_detail["message"] + .split(":", 1)[0] + .isdigit() + ): + id, message = additional_detail["message"].split(":", 1) + id = int_maybe(id.strip()) + message = message.strip() + else: + id = 0 + message = additional_detail.get("message", "") + msg.append( + { + "id": id, + "message": message, + } + ) + else: + raise + except Exception: + msg = response.text + + raise CarrierError(response, msg) from e + + return response + + def request(self, url, method, json): + json = json.copy() + headers = { + "Accept": "application/json", + "Authorization": json.pop("authorization"), + "Content-Type": "application/json", + "x-version": "3.1.0", + } + response = requests.post(f"{url}/{method}", json=json, headers=headers) + self._raise_for_status(response) + return response + + def _parse_response(self, response): + return response.json() + + @action + def get_label(self, input: DHLExpressLabelInput) -> DHLExpressLabelOutput: + url = self._get_url(input.auth.isTest) + params = input.params() + response = self.request(url, "shipments", params) + result = self._parse_response(response) + return DHLExpressLabelOutput.from_params(result, input) diff --git a/roulier/carriers/dhl_express/schema.py b/roulier/carriers/dhl_express/schema.py new file mode 100644 index 0000000..12a2968 --- /dev/null +++ b/roulier/carriers/dhl_express/schema.py @@ -0,0 +1,547 @@ +# Copyright 2026 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from base64 import b64encode +from datetime import date, datetime, timezone +from enum import Enum + +from pydantic import BaseModel + +from ...helpers import filter_empty, merge, unaccent +from ...schema import ( + Address, + Auth, + Label, + LabelInput, + LabelOutput, + Parcel, + ParcelLabel, + Service, + Tracking, +) + + +class Format(Enum): + PDF = "PDF" + ZPL = "ZPL" + DPL = "LP2" + EPL = "EPL" + + +class DocumentFormat(Enum): + PDF = "PDF" + PNG = "PNG" + GIF = "GIF" + TIFF = "TIFF" + JPEG = "JPEG" + + +class Incoterm(Enum): + EXW = "EXW" + FCA = "FCA" + CPT = "CPT" + CIP = "CIP" + DPU = "DPU" + DAP = "DAP" + DDP = "DDP" + FAS = "FAS" + FOB = "FOB" + CFR = "CFR" + CIF = "CIF" + DAF = "DAF" + DAT = "DAT" + DDU = "DDU" + DEQ = "DEQ" + DES = "DES" + + +class AccountType(Enum): + SHIPPER = "shipper" + PAYER = "payer" + DUTIES_AND_TAXES = "duties-taxes" + + +class ReferenceType(Enum): + AAO = "AAO" + CU = "CU" + FF = "FF" + FN = "FN" + IBC = "IBC" + LLR = "LLR" + OBC = "OBC" + PRN = "PRN" + ACP = "ACP" + ACS = "ACS" + ACR = "ACR" + CDN = "CDN" + STD = "STD" + CO = "CO" + AFM = "AFM" + + +class UoM(Enum): + METRIC = "metric" + IMPERIAL = "imperial" + + +class ProductUoM(Enum): + BOX = "BOX" + _2GM = "2GM" + M3 = "M3" + DPR = "DPR" + DOZ = "DOZ" + PCS = "PCS" + GM = "GM" + GRS = "GRS" + KG = "KG" + M = "M" + _3GM = "3GM" + X = "X" + NO = "NO" + PRS = "PRS" + CM2 = "CM2" + _2M2 = "2M2" + _3M2 = "3M2" + M2 = "M2" + _4M2 = "4M2" + CM = "CM" + CONE = "CONE" + CT = "CT" + EA = "EA" + LBS = "LBS" + RILL = "RILL" + ROLL = "ROLL" + SET = "SET" + TU = "TU" + KM = "KM" + IN = "IN" + FT = "FT" + YD = "YD" + MI = "MI" + LTR = "LTR" + MMQ = "MMQ" + CM3 = "CM3" + DMQ = "DMQ" + MLT = "MLT" + CLT = "CLT" + DLT = "DLT" + INQ = "INQ" + FT3 = "FT3" + YD3 = "YD3" + GLI = "GLI" + GLL = "GLL" + PT = "PT" + PTI = "PTI" + QTI = "QTI" + PTL = "PTL" + QTL = "QTL" + PTD = "PTD" + OZI = "OZI" + J57 = "J57" + NM3 = "NM3" + SM3 = "SM3" + TNE = "TNE" + LB = "LB" + ONZ = "ONZ" + CEL = "CEL" + + +class ExportType(Enum): + PERMANENT = "permanent" + TEMPORARY = "temporary" + RETURN = "return" + USED_EXHIBITION_GOODS_TO_ORIGIN = "used_exhibition_goods_to_origin" + INTERCOMPANY_USE = "intercompany_use" + COMMERCIAL_PURPOSE_OR_SALE = "commercial_purpose_or_sale" + PERSONAL_BELONGINGS_OR_PERSONAL_USE = "personal_belongings_or_personal_use" + SAMPLE = "sample" + GIFT = "gift" + RETURN_TO_ORIGIN = "return_to_origin" + WARRANTY_REPLACEMENT = "warranty_replacement" + DIPLOMATIC_GOODS = "diplomatic_goods" + DEFENCE_MATERIAL = "defence_material" + + +class DHLExpressAuth(Auth): + login: str + password: str + + def params(self): + return { + "authorization": "Basic " + + b64encode(f"{self.login}:{self.password}".encode()).decode() + } + + +class DHLExpressAddress(Address): + country: str + zip: str + city: str + street1: str + street2: str | None = None + street3: str | None = None + phone: str + landlinePhone: str | None = None + stateOrProvinceCode: str | None = None + + def params(self): + return { + "postalAddress": { + "postalCode": self.zip, + "cityName": self.city, + "countryCode": self.country, + "provinceCode": self.stateOrProvinceCode, + "addressLine1": self.street1, + "addressLine2": self.street2, + "addressLine3": self.street3, + }, + "contactInformation": { + "phone": self.landlinePhone or self.phone, + "mobilePhone": self.phone, + "companyName": self.company or self.name, + "fullName": self.name, + "email": self.email, + }, + } + + +class DHLExpressService(Service): + shippingDate: datetime + labelFormat: Format | None = Format.PDF + shipmentDescription: str | None = None + incoterm: Incoterm = Incoterm.DAP + unitOfMeasurement: UoM | None = UoM.METRIC + accountType: AccountType | None = AccountType.SHIPPER + referenceType: ReferenceType | None = ReferenceType.CU + product: str + pickupLocationId: str | None = None + + def params(self): + shipping_date = self.shippingDate + if shipping_date.tzinfo is None: + shipping_date = shipping_date.replace(tzinfo=timezone.utc) + + shipping_date = shipping_date.isoformat(timespec="seconds") + shipping_date = shipping_date.replace("+", " GMT+") + return { + "productCode": self.product, + "plannedShippingDateAndTime": shipping_date, + "pickup": { + "isRequested": self.pickupLocationId is not None, + "location": self.pickupLocationId, + }, + "content": { + "description": self.shipmentDescription or "N/A", + "incoterm": self.incoterm.value, + "unitOfMeasurement": self.unitOfMeasurement.value + if self.unitOfMeasurement + else UoM.METRIC.value, + }, + "outputImageProperties": { + "encodingFormat": ( + self.labelFormat.value if self.labelFormat else Format.PDF.value + ).lower(), + "imageOptions": [ + { + "typeCode": "label", + "templateName": "ECOM26_64_002", + } + ], + "allDocumentsInOneImage": False, + "splitTransportAndWaybillDocLabels": True, + "receiptAndLabelsInOneImage": False, + }, + "accounts": [ + { + "number": self.customerId, + "typeCode": self.accountType.value + if self.accountType + else AccountType.SHIPPER.value, + } + ], + "customerReferences": [ + { + "value": self.reference1, + "typeCode": self.referenceType.value + if self.referenceType + else ReferenceType.CU.value, + } + ], + } + + +class DHLExpressArticle(BaseModel): + number: int | None = None + description: str + value: float + quantity: int + uom: ProductUoM | None = ProductUoM.PCS + weight: float | None = None + weightGross: float | None = None + originCountry: str + exportType: ExportType | None = ExportType.COMMERCIAL_PURPOSE_OR_SALE + + hsCode: str + inboundHsCode: str | None = None + + def params(self, i): + return { + "number": self.number or i + 1, + "description": self.description, + "price": self.value, + "quantity": { + "value": self.quantity, + "unitOfMeasurement": self.uom.value + if self.uom + else ProductUoM.PCS.value, + }, + "commodityCodes": [ + {"typeCode": "outbound", "value": self.hsCode}, + {"typeCode": "inbound", "value": self.inboundHsCode or self.hsCode}, + ], + "exportReasonType": self.exportType.value + if self.exportType + else ExportType.COMMERCIAL_PURPOSE_OR_SALE.value, + "manufacturerCountry": self.originCountry, + "weight": { + "netValue": self.weight if self.weight else None, + "grossValue": self.weightGross if self.weightGross else None, + }, + } + + +class DHLExpressInvoice(BaseModel): + number: str + content: str + date: date + format: DocumentFormat | None = DocumentFormat.PDF + + def params(self): + return { + "content": { + "exportDeclaration": { + "invoice": { + "number": self.number, + "date": self.date.strftime("%Y-%m-%d"), + }, + } + }, + "documentImages": [ + { + "imageFormat": DocumentFormat.PDF.value + if self.format + else DocumentFormat.PDF.value, + "content": self.content, + "typeCode": "INV", + } + ], + } + + +class DHLExpressCustoms(BaseModel): + invoice: DHLExpressInvoice + numberOfCopies: int | None = None + articles: list[DHLExpressArticle] = [] + vat: float | None = None + delivery: float | None = None + insurance: float | None = None + + def params(self): + return merge( + self.invoice.params(), + { + "content": { + "isCustomsDeclarable": True, + "exportDeclaration": { + "lineItems": [ + article.params(i) for i, article in enumerate(self.articles) + ], + "additionalCharges": ( + [ + { + "caption": "Freight", + "value": self.delivery, + "typeCode": "freight", + } + ] + if self.delivery + else [] + ) + + ( + [ + { + "caption": "Insurance", + "value": self.insurance, + "typeCode": "insurance", + } + ] + if self.insurance + else [] + ) + + ( + [ + { + "caption": "VAT", + "value": self.vat, + "typeCode": "vat", + } + ] + if self.vat + else [] + ), + }, + "declaredValueCurrency": "EUR", + "declaredValue": 1155.0, + }, + }, + { + "outputImageProperties": { + "imageOptions": [ + { + "templateName": "COMMERCIAL_INVOICE_P_10", + "invoiceType": "commercial", + "isRequested": False, + "typeCode": "invoice", + } + ] + } + }, + ) + + +class DHLExpressParcel(Parcel): + length: float + width: float + height: float + referenceType: ReferenceType | None = ReferenceType.CU + + def params(self): + return { + "weight": self.weight, + # "typeCode":"2BP", + "dimensions": { + "length": self.length, + "width": self.width, + "height": self.height, + }, + "customerReferences": [ + { + "value": self.reference, + "typeCode": self.referenceType.value + if self.referenceType + else ReferenceType.CU.value, + } + ], + } + + +class DHLExpressLabelInput(LabelInput): + auth: DHLExpressAuth + service: DHLExpressService + parcels: list[DHLExpressParcel] + to_address: DHLExpressAddress + from_address: DHLExpressAddress + customs: DHLExpressCustoms | None = None + + def params(self): + return unaccent( + filter_empty( + merge( + self.auth.params(), + self.service.params(), + { + "content": { + "packages": [parcel.params() for parcel in self.parcels], + } + }, + { + "customerDetails": { + "shipperDetails": self.from_address.params(), + "receiverDetails": self.to_address.params(), + } + }, + self.customs.params() + if self.customs + else { + "content": { + "isCustomsDeclarable": False, + } + }, + ) + ) + ) + + +class DHLExpressTracking(Tracking): + @classmethod + def from_params(cls, result): + return cls.model_construct( + number=result["trackingNumber"], + url=result.get("trackingUrl"), + ) + + +class DHLExpressLabel(Label): + @classmethod + def from_params(cls, result, name, format): + return cls.model_construct( + data=result, + name=name, + type=format, + ) + + +class DHLExpressParcelLabel(ParcelLabel): + label: DHLExpressLabel | None = None + tracking: DHLExpressTracking | None = None + + @classmethod + def from_params(cls, result, document, input, id): + return cls.model_construct( + id=id, + reference=input.parcels[result["referenceNumber"] - 1].reference, + label=( + DHLExpressLabel.from_params( + document["content"], "label", document["imageFormat"] + ) + ), + tracking=DHLExpressTracking.from_params(result), + ) + + +class DHLExpressLabelOutput(LabelOutput): + shipmentTrackingNumber: str + shipmentTrackingUrl: str | None = None + parcels: list[DHLExpressParcelLabel] + annexes: list[DHLExpressLabel] + + @classmethod + def from_params(cls, results, input): + labels_by_referrence = { + document.get("packageReferenceNumber"): document + for document in results.get("documents", []) + if document["typeCode"] == "label" + } + return cls.model_construct( + shipmentTrackingNumber=results["shipmentTrackingNumber"], + shipmentTrackingUrl=results.get("trackingUrl"), + parcels=[ + DHLExpressParcelLabel.from_params( + result, + labels_by_referrence.get( + result["referenceNumber"], labels_by_referrence.get(None) + ), + input, + i + 1, + ) + for i, result in enumerate(results["packages"]) + ], + annexes=[ + DHLExpressLabel.from_params( + document["content"], document["typeCode"], document["imageFormat"] + ) + for document in results.get("documents", []) + if document["typeCode"] != "label" + ], + ) diff --git a/roulier/carriers/dhl_express/tests/__init__.py b/roulier/carriers/dhl_express/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_common_failed_get_label_1.yaml b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_common_failed_get_label_1.yaml new file mode 100644 index 0000000..18a20be --- /dev/null +++ b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_common_failed_get_label_1.yaml @@ -0,0 +1,75 @@ +interactions: +- request: + body: '{"productCode": "N", "plannedShippingDateAndTime": "2026-02-13T00:00:00 + GMT+00:00", "pickup": {"isRequested": false}, "content": {"description": "N/A", + "incoterm": "DAP", "unitOfMeasurement": "metric", "packages": [{"weight": 0.0, + "dimensions": {"length": 10, "width": 10, "height": 10}, "customerReferences": + [{"value": "Parcel 1", "typeCode": "CU"}]}], "isCustomsDeclarable": false}, + "outputImageProperties": {"encodingFormat": "pdf", "imageOptions": [{"typeCode": + "label", "templateName": "ECOM26_64_002"}], "allDocumentsInOneImage": false, + "splitTransportAndWaybillDocLabels": true, "receiptAndLabelsInOneImage": false}, + "accounts": [{"number": "xxxx", "typeCode": "shipper"}], "customerReferences": + [{"value": "Ref1", "typeCode": "CU"}], "customerDetails": {"shipperDetails": + {"postalAddress": {"postalCode": "69100", "cityName": "Villeurbanne", "countryCode": + "FR", "addressLine1": "27 rue Henri Rolland", "addressLine2": "Batiment B"}, + "contactInformation": {"phone": "+33482538457", "mobilePhone": "+33482538457", + "companyName": "Akretion", "fullName": "Akretion"}}, "receiverDetails": {"postalAddress": + {"postalCode": "75004", "cityName": "Paris", "countryCode": "FR", "addressLine1": + "6 Place des Vosges"}, "contactInformation": {"phone": "+33600000000", "mobilePhone": + "+33600000000", "companyName": "Hugo", "fullName": "Hugo", "email": "hugo.victor@example.com"}}}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '1382' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.5 + x-version: + - 3.1.0 + method: POST + uri: https://express.api.dhl.com/mydhlapi/test/shipments + response: + body: + string: '{"instance":"\/expressapi\/shipments","detail":"#\/content\/packages\/0\/weight: + 0.0 is not greater or equal to 0.001","title":"Validation error","message":"Unprocessable + Entity","status":"422"}' + headers: + Connection: + - keep-alive + Content-Language: + - eng + Content-Length: + - '194' + Content-Type: + - application/problem+json + Date: + - Fri, 13 Feb 2026 16:09:37 GMT + Invocation-Id: + - 20260213160937_2f1b_2dd3116e-90e7-4431-bf20-6d0d127167b8 + Max-Forwards: + - '20' + Message-Reference: + - e9fcc342-cdfa-466c-8622-65ecb094fe76 + Set-Cookie: + - BIGipServer~WSB~pl_wsb-express-chd.dhl.com_443=227362981.21308.0000; path=/; + Httponly; Secure + - TS019fcce8=012d4839b34c83204e2c848a58cb566db10a9a179076a0d8b5435f2a5c27232963a53191c63c0113e27db99fb2a1ca58f2c88f2f11c36b6ac22b774acd80e87068d0baebf6; + Path=/; Secure; HttpOnly + Via: + - 1.1 czstlls0994.prg-dc.dhl.com () + X-Content-Type-Options: + - nosniff + X-CorrelationID: + - Id-c14c8f692388310b7a5c886b 0 + X-XSS-Protection: + - 1; mode=block + status: + code: 422 + message: '' +version: 1 diff --git a/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_dhl_express_bad_product.yaml b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_dhl_express_bad_product.yaml new file mode 100644 index 0000000..efdcc7a --- /dev/null +++ b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_dhl_express_bad_product.yaml @@ -0,0 +1,74 @@ +interactions: +- request: + body: '{"productCode": "?", "plannedShippingDateAndTime": "2026-02-13T00:00:00 + GMT+00:00", "pickup": {"isRequested": false}, "content": {"description": "N/A", + "incoterm": "DAP", "unitOfMeasurement": "metric", "packages": [{"weight": 1.2, + "dimensions": {"length": 10, "width": 10, "height": 10}, "customerReferences": + [{"value": "Parcel 1", "typeCode": "CU"}]}], "isCustomsDeclarable": false}, + "outputImageProperties": {"encodingFormat": "pdf", "imageOptions": [{"typeCode": + "label", "templateName": "ECOM26_64_002"}], "allDocumentsInOneImage": false, + "splitTransportAndWaybillDocLabels": true, "receiptAndLabelsInOneImage": false}, + "accounts": [{"number": "xxxx", "typeCode": "shipper"}], "customerReferences": + [{"value": "Ref1", "typeCode": "CU"}], "customerDetails": {"shipperDetails": + {"postalAddress": {"postalCode": "69100", "cityName": "Villeurbanne", "countryCode": + "FR", "addressLine1": "27 rue Henri Rolland", "addressLine2": "Batiment B"}, + "contactInformation": {"phone": "+33482538457", "mobilePhone": "+33482538457", + "companyName": "Akretion", "fullName": "Akretion"}}, "receiverDetails": {"postalAddress": + {"postalCode": "75004", "cityName": "Paris", "countryCode": "FR", "addressLine1": + "6 Place des Vosges"}, "contactInformation": {"phone": "+33600000000", "mobilePhone": + "+33600000000", "companyName": "Hugo", "fullName": "Hugo", "email": "hugo.victor@example.com"}}}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '1382' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.5 + x-version: + - 3.1.0 + method: POST + uri: https://express.api.dhl.com/mydhlapi/test/shipments + response: + body: + string: '{"instance":"\/expressapi\/shipments","detail":"8007: Error getting + Product details from GREF","title":"Bad request","message":"Bad request","status":"400"}' + headers: + Connection: + - keep-alive + Content-Language: + - eng + Content-Length: + - '156' + Content-Type: + - application/problem+json + Date: + - Fri, 13 Feb 2026 16:09:37 GMT + Invocation-Id: + - 20260213160937_2f1b_d47d6a51-a911-4695-9d53-bf96ec0443ca + Max-Forwards: + - '20' + Message-Reference: + - 1f1e00e3-ee44-4c6a-97ac-f3bdff51d69b + Set-Cookie: + - BIGipServer~WSB~pl_wsb-express-chd.dhl.com_443=1181239461.21308.0000; path=/; + Httponly; Secure + - TS019fcce8=012d4839b372c8b4d40c708b982dcc40e2e3feffa1ce9dc7990be3e79f7c7a8aa684e9f08c6cc7308f8c575ddd2e547dfa3e2e1dfeb36f3d54c313aefb0cc9841a1a0823dd; + Path=/; Secure; HttpOnly + Via: + - 1.1 czchols5949.prg-dc.dhl.com () + X-Content-Type-Options: + - nosniff + X-CorrelationID: + - Id-c14c8f69df848dc4f0db37ff 0 + X-XSS-Protection: + - 1; mode=block + status: + code: 400 + message: '' +version: 1 diff --git a/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_dhl_express_label.yaml b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_dhl_express_label.yaml new file mode 100644 index 0000000..589a4d4 --- /dev/null +++ b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_dhl_express_label.yaml @@ -0,0 +1,73 @@ +interactions: +- request: + body: '{"productCode": "N", "plannedShippingDateAndTime": "2026-02-13T00:00:00 + GMT+00:00", "pickup": {"isRequested": false}, "content": {"description": "N/A", + "incoterm": "DAP", "unitOfMeasurement": "metric", "packages": [{"weight": 1.2, + "dimensions": {"length": 10, "width": 10, "height": 10}, "customerReferences": + [{"value": "Parcel 1", "typeCode": "CU"}]}], "isCustomsDeclarable": false}, + "outputImageProperties": {"encodingFormat": "pdf", "imageOptions": [{"typeCode": + "label", "templateName": "ECOM26_64_002"}], "allDocumentsInOneImage": false, + "splitTransportAndWaybillDocLabels": true, "receiptAndLabelsInOneImage": false}, + "accounts": [{"number": "xxxx", "typeCode": "shipper"}], "customerReferences": + [{"value": "Ref1", "typeCode": "CU"}], "customerDetails": {"shipperDetails": + {"postalAddress": {"postalCode": "69100", "cityName": "Villeurbanne", "countryCode": + "FR", "addressLine1": "27 rue Henri Rolland", "addressLine2": "Batiment B"}, + "contactInformation": {"phone": "+33482538457", "mobilePhone": "+33482538457", + "companyName": "Akretion", "fullName": "Akretion"}}, "receiverDetails": {"postalAddress": + {"postalCode": "75004", "cityName": "Paris", "countryCode": "FR", "addressLine1": + "6 Place des Vosges"}, "contactInformation": {"phone": "+33600000000", "mobilePhone": + "+33600000000", "companyName": "Hugo", "fullName": "Hugo", "email": "hugo.victor@example.com"}}}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '1382' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.5 + x-version: + - 3.1.0 + method: POST + uri: https://express.api.dhl.com/mydhlapi/test/shipments + response: + body: + string: '{"shipmentTrackingNumber":"3663796592","trackingUrl":"https://express.api.dhl.com/mydhlapi/test/shipments/3663796592/tracking","packages":[{"referenceNumber":1,"trackingNumber":"JD014600005255753114","trackingUrl":"https://express.api.dhl.com/mydhlapi/test/shipments/3663796592/tracking?pieceTrackingNumber=JD014600005255753114"}],"documents":[{"imageFormat":"PDF","content":"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","typeCode":"label"}]}' + headers: + Connection: + - keep-alive + Content-Language: + - eng + Content-Length: + - '7543' + Content-Type: + - application/json + Date: + - Fri, 13 Feb 2026 16:09:36 GMT + Invocation-Id: + - 20260213160936_2f1b_dadc34ee-3509-4b16-984b-d6ff72d1a586 + Max-Forwards: + - '20' + Message-Reference: + - 698f4cc004ef69d010274d0a46f12d8a + Set-Cookie: + - BIGipServer~WSB~pl_wsb-express-chd.dhl.com_443=1181239461.21308.0000; path=/; + Httponly; Secure + - TS019fcce8=012d4839b39756520214edb91b083eb25b7d78c57b4163ef8c7619dda192315fa5b006d26edc6b9adbb6484b3bc5a05108c2ea08bef0d72ff3b5bb9d3c8e6efe75fcd5ff98; + Path=/; Secure; HttpOnly + Via: + - 1.1 czchols5949.prg-dc.dhl.com () + X-Content-Type-Options: + - nosniff + X-CorrelationID: + - Id-c04c8f694d847eff740cfcd9 0 + X-XSS-Protection: + - 1; mode=block + status: + code: 201 + message: '' +version: 1 diff --git a/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_dhl_express_label_zpl.yaml b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_dhl_express_label_zpl.yaml new file mode 100644 index 0000000..4d2acb5 --- /dev/null +++ b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_dhl_express_label_zpl.yaml @@ -0,0 +1,73 @@ +interactions: +- request: + body: '{"productCode": "N", "plannedShippingDateAndTime": "2026-02-13T00:00:00 + GMT+00:00", "pickup": {"isRequested": false}, "content": {"description": "N/A", + "incoterm": "DAP", "unitOfMeasurement": "metric", "packages": [{"weight": 1.2, + "dimensions": {"length": 10, "width": 10, "height": 10}, "customerReferences": + [{"value": "Parcel 1", "typeCode": "CU"}]}], "isCustomsDeclarable": false}, + "outputImageProperties": {"encodingFormat": "zpl", "imageOptions": [{"typeCode": + "label", "templateName": "ECOM26_64_002"}], "allDocumentsInOneImage": false, + "splitTransportAndWaybillDocLabels": true, "receiptAndLabelsInOneImage": false}, + "accounts": [{"number": "xxxx", "typeCode": "shipper"}], "customerReferences": + [{"value": "Ref1", "typeCode": "CU"}], "customerDetails": {"shipperDetails": + {"postalAddress": {"postalCode": "69100", "cityName": "Villeurbanne", "countryCode": + "FR", "addressLine1": "27 rue Henri Rolland", "addressLine2": "Batiment B"}, + "contactInformation": {"phone": "+33482538457", "mobilePhone": "+33482538457", + "companyName": "Akretion", "fullName": "Akretion"}}, "receiverDetails": {"postalAddress": + {"postalCode": "75004", "cityName": "Paris", "countryCode": "FR", "addressLine1": + "6 Place des Vosges"}, "contactInformation": {"phone": "+33600000000", "mobilePhone": + "+33600000000", "companyName": "Hugo", "fullName": "Hugo", "email": "hugo.victor@example.com"}}}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '1382' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.5 + x-version: + - 3.1.0 + method: POST + uri: https://express.api.dhl.com/mydhlapi/test/shipments + response: + body: + string: '{"shipmentTrackingNumber":"3663796614","trackingUrl":"https://express.api.dhl.com/mydhlapi/test/shipments/3663796614/tracking","packages":[{"referenceNumber":1,"trackingNumber":"JD014600005255753117","trackingUrl":"https://express.api.dhl.com/mydhlapi/test/shipments/3663796614/tracking?pieceTrackingNumber=JD014600005255753117"}],"documents":[{"imageFormat":"ZPL","content":"XlhBCl5GVDgsMzAKXkEwTiwsMjgKXkZERVhQUkVTUyBET01FU1RJQ15GUwpeRlQ4LDMwCl5BME4sLDI4Cl5GREVYUFJFU1MgRE9NRVNUSUNeRlMKXkZUOTEsNDkKXkEwTiwsMTEKXkZEMjAyNi0wMi0xMyBNeURITCBBUEkgMS4wIC8gKkdMUyBjZXJ0aWZpZWQgbGFiZWwqXkZTCl5GTzM4NiwtMQpeR0IxNTAsMCwyLEJeRlMKXkZPNTM1LDAKXkdCMCw1NSwyLEJeRlMKXkZPMzg2LDU0Cl5HQjE1MCwwLDIsQl5GUwpeRk8zODUsMApeR0IwLDU1LDIsQl5GUwpeRlQ0MDUsNDIKXkEwTiwsNDkKXkZERE9NXkZTCl5GVDQwNCw0MgpeQTBOLCw0OQpeRkRET01eRlMKXkZPNTc3LDExCl5HRkEsMTY4LDE2OCw4LDAwZmZmZmZmZmZmZmYwMDAwMWZmZmZmZmZmZmZmZjgwMDFmZmZmZmZmZmZmZmZlMDAzZmZmZmZmZmZmZmZmZjgwN2ZmZmZmZmZmZmZmZmZjMGZmZmZmZmZmZmZmZmZmYzBmZmZmZmZmZmZmZmZmZmUxZmZmZmZmZmZmZmZmZmZlM2ZmZmZmZmZmZmZmZmZmZTdmZmZmZmZmZmZmZmZmZmU3ZmZmZmZmZmZmZmZmZmZlMDAwMDAwMDAwMDFmZmZmZTAwMDAwMDAwMDAxZmZmZmUwMDAwMDAwMDAwM2ZmZmZjMDAwMDAwMDAwMDNmZmZmYzAwMDAwMDAwMDA3ZmZmZjgwMDAwMDAwMDAwZmZmZmYwMDAwMDAwMDAwMWZmZmZlMDAwMDAwMDAwMDFmZmZmZTAwMDAwMDAwMDAzZmZmZmMwMDAwMDAwMDAwMDAwMDAwMF5GUwpeRk81NzQsMjQKXkdGQSwxNzYsMTc2LDgsMDAwMGZmZmZjMDAwMDAwMDAwMDFmZmZmZjAwMDAwMDAwMDAzZmZmZmYwMDAwMDAwMDAwN2ZmZmZlMDAwMDAwMDAwMDdmZmZmYzAwMDAwMDAwMDBmZmZmZjgwMDAwMDAwMDAxZmZmZmY4MDAwMDAwMDAwMWZmZmZmMDAwMDAwMDAwMDNmZmZmZTAwMDAwMDAwMDA3ZmZmZmUwMDAwMDAwMDAwZmZmZmZmZmZmZmZmYzAwMGZmZmZmZmZmZmZmZjgwMDFmZmZmZmZmZmZmZmYwMDAzZmZmZmZmZmZmZmZlMDAwN2ZmZmZmZmZmZmZmYzAwMDdmZmZmZmZmZmZmZjgwMDBmZmZmZmZmZmZmZmYwMDAxZmZmZmZmZmZmZmZjMDAwM2ZmZmZmZmZmZmZmODAwMDNmZmZmZmZmZmZmYzAwMDA3ZmZmZmZmZmZmZTAwMDAwMDAwMDAwMDAwMDAwMDAwMF5GUwpeRk82MzEsMzMKXkdGQSw0OCw0OCw0LDAwN2ZmZmZjMDBmZmZmZmMwMWZmZmZmODAxZmZmZmYwMDNmZmZmZTAwN2ZmZmZlMDBmZmZmZmMwMGZmZmZmODAxZmZmZmY4MDNmZmZmZjAwN2ZmZmZlMDA3ZmZmZmMwMF5GUwpeRk82NDEsMTEKXkdGQSwxODksMTg5LDksMDAwMWZmZmZmODAzZmZmZmYwMDAwM2ZmZmZmMDAzZmZmZmUwMDAwM2ZmZmZlMDA3ZmZmZmUwMDAwN2ZmZmZjMDBmZmZmZmMwMDAwZmZmZmZjMDFmZmZmZjgwMDAxZmZmZmY4MDFmZmZmZjAwMDAxZmZmZmYwMDNmZmZmZjAwMDAzZmZmZmYwMDdmZmZmZTAwMDA3ZmZmZmUwMGZmZmZmYzAwMDBmZmZmZmMwMGZmZmZmODAwMDBmZmZmZjgwMWZmZmZmODAwMDFmZmZmZjgwM2ZmZmZmMDAwMDNmZmZmZmZmZmZmZmZlMDAwMDdmZmZmZmZmZmZmZmZjMDAwMDdmZmZmZmZmZmZmZmZjMDAwMGZmZmZmZmZmZmZmZmY4MDAwMWZmZmZmZmZmZmZmZmYwMDAwMWZmZmZmZmZmZmZmZmYwMDAwM2ZmZmZmZmZmZmZmZmUwMDAwN2ZmZmZmZmZmZmZmZmMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwXkZTCl5GTzY2MywzMwpeR0ZBLDQ4LDQ4LDQsMDA3ZmZmZmMwMGZmZmZmYzAxZmZmZmY4MDFmZmZmZjAwM2ZmZmZlMDA3ZmZmZmUwMGZmZmZmYzAwZmZmZmY4MDFmZmZmZjgwM2ZmZmZmMDA3ZmZmZmUwMDdmZmZmYzAwXkZTCl5GTzU0MywzNwpeR0ZBLDE1LDE1LDUsZmZmZmZmZmYwMGZmZmZmZmZmMDBmZmZmZmZmZTAwXkZTCl5GTzU0MywzMwpeR0ZBLDE1LDE1LDUsZmZmZmZmZmZmMGZmZmZmZmZmZTBmZmZmZmZmZmMwXkZTCl5GTzU0Myw0MgpeR0ZBLDEyLDEyLDQsZmZmZmZmZjhmZmZmZmZmMGZmZmZmZmYwXkZTCl5GTzc0OCwzNwpeR0ZBLDE1LDE1LDUsM2ZmZmZmZmYwMDNmZmZmZmZmMDA3ZmZmZmZmZjAwXkZTCl5GTzc0NCw0MgpeR0ZBLDE1LDE1LDUsM2ZmZmZmZmZmMDNmZmZmZmZmZjA3ZmZmZmZmZmYwXkZTCl5GTzc1MSwzMwpeR0ZBLDEyLDEyLDQsM2ZmZmZmZjgzZmZmZmZmODdmZmZmZmY4XkZTCl5GTzcwMSwxMQpeR0ZBLDEwNSwxMDUsNSwwMDAxZmZmZmZjMDAwM2ZmZmZmODAwMDNmZmZmZjAwMDA3ZmZmZmYwMDAwZmZmZmZlMDAwMWZmZmZmYzAwMDFmZmZmZmMwMDAzZmZmZmY4MDAwN2ZmZmZmMDAwMGZmZmZmZTAwMDBmZmZmZmUwMDAxZmZmZmZjMDAwM2ZmZmZmODAwMDdmZmZmZjAwMDA3ZmZmZmYwMDAwZmZmZmZlMDAwMWZmZmZmYzAwMDNmZmZmZjgwMDAzZmZmZmY4MDAwN2ZmZmZmMDAwMDAwMDAwMDAwMDBeRlMKXkZPNjk3LDMzCl5HRkEsODQsODQsNywxZmZmZmZmZmZmZmZmMDNmZmZmZmZmZmZmZmUwN2ZmZmZmZmZmZmZmZTA3ZmZmZmZmZmZmZmZjMGZmZmZmZmZmZmZmZjgwZmZmZmZmZmZmZmZmMDBmZmZmZmZmZmZmZmYwMGZmZmZmZmZmZmZmZTAwZmZmZmZmZmZmZmZjMDA3ZmZmZmZmZmZmZmMwMDFmZmZmZmZmZmZmODAwMDNmZmZmZmZmZmYwMDBeRlMKXkZPMCw1NApeR0I3ODAsMCwyLEJeRlMKXkZUMjIsNzkKXkEwTiwsMTUKXkZERnJvbSA6XkZTCl5GVDIyLDc5Cl5BME4sLDE1Cl5GREZyb20gOl5GUwpeRlQ4Nyw2NwpeQTBOLCwxMwpeRkRBa3JldGlvbl5GUwpeRlQ4Nyw4MgpeQTBOLCwxMwpeRkRBa3JldGlvbl5GUwpeRlQ4Nyw5NgpeQTBOLCwxMwpeRkQyNyBydWUgSGVucmkgUm9sbGFuZF5GUwpeRlQ4NywxMTEKXkEwTiwsMTMKXkZEQmF0aW1lbnQgQl5GUwpeRlQ4NywxMjUKXkEwTiwsMTMKXkZENjkxMDAgVmlsbGV1cmJhbm5lXkZTCl5GVDg3LDE0MApeQTBOLCwxMwpeRkRGUkFOQ0VeRlMKXkZUNjk3LDE0MwpeQTBOLCwyMApeRkRDb250YWN0Ol5GUwpeRlQ2OTMsNzkKXkEwTiwsMjIKXkZET3JpZ2luOl5GUwpeRlQ2OTQsMTE2Cl5BME4sLDQxCl5GRExZU15GUwpeRlQ2OTMsMTE2Cl5BME4sLDQxCl5GRExZU15GUwpeRk8wLDE0OQpeR0I3ODAsMCwyLEJeRlMKXkZUMjIsMTczCl5BME4sLDE1Cl5GRFRvIDpeRlMKXkZUMjIsMTczCl5BME4sLDE1Cl5GRFRvIDpeRlMKXkZPOSwxNTYKXkdCMCwzMSw1LEJeRlMKXkZPMTIsMTUzCl5HQjMxLDAsNSxCXkZTCl5GTzc2NSwxNTYKXkdCMCwzMSw1LEJeRlMKXkZPNzM2LDE1MwpeR0IzMSwwLDUsQl5GUwpeRk85LDI3NgpeR0IwLDMxLDUsQl5GUwpeRk8xMiwzMDUKXkdCMzEsMCw1LEJeRlMKXkZPNzY1LDI3NgpeR0IwLDMxLDUsQl5GUwpeRk83MzYsMzA1Cl5HQjMxLDAsNSxCXkZTCl5GVDg3LDE3MwpeQTBOLCwyNApeRkRIdWdvXkZTCl5GVDg3LDIwMQpeQTBOLCwyNApeRkRIdWdvXkZTCl5GVDg3LDIyOQpeQTBOLCwyNApeRkQ2IFBsYWNlIGRlcyBWb3NnZXNeRlMKXkZUODcsMjY1Cl5BME4sLDMzCl5GRDc1MDA0IFBhcmlzXkZTCl5GVDg3LDI2NQpeQTBOLCwzMwpeRkQ3NTAwNCBQYXJpc15GUwpeRlQ4NywzMDIKXkEwTiwsMzMKXkZERlJBTkNFXkZTCl5GVDg3LDMwMgpeQTBOLCwzMwpeRkRGUkFOQ0VeRlMKXkZUNjE0LDE2OQpeQTBOLCwxNwpeRkRDb250YWN0Ol5GUwpeRk8wLDMxNApeR0I3ODAsMCwyLEJeRlMKXkZUOCwzNjQKXkEwTiwsNjAKXkZERk9SWV5GUwpeRlQxOTYsMzY3Cl5BME4sLDY1Cl5GREZSLU9SWS1QU1ZeRlMKXkZUMTk1LDM2NwpeQTBOLCw2NQpeRkRGUi1PUlktUFNWXkZTCl5GVDY1MiwzNjcKXkEwTiwsNjcKXkZEUkVDXkZTCl5GTzAsMzc3Cl5HQjc4MCwwLDIsQl5GUwpeRk8wLDM3OApeR0ZBLDE3NCwxNzQsMyxmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDBmZmZmMDAwMDAwMDBeRlMKXkZPMTYsMzc4Cl5HQjU1MSw1Nyw1NyxCXkZTCl5GVDE3LDQyMApeQTBOLCw1MApeRlJeRkRBREleRlMKXkZUNzA5LDM5MgpeQTBOLCwxNwpeRkRUaW1lXkZTCl5GVDYzMCwzOTIKXkEwTiwsMTcKXkZERGF5XkZTCl5GTzAsNDM0Cl5HQjc4MCwwLDIsQl5GUwpeRlQxMiw0NTMKXkEwTiwsMjEKXkZEUmVmIE5vOiBSZWYxXkZTCl5GVDEyLDUwMgpeQTBOLCwyMQpeRkRDb250ZW50IDogTi9BXkZTCl5GVDUzNSw0NTMKXkEwTiwsMTcKXkZEUGNlL1NocHQgV2VpZ2h0XkZTCl5GVDU0Myw0ODYKXkEwTiwsMzkKXkZEMS4yIGtnXkZTCl5GVDU0Miw0ODYKXkEwTiwsMzkKXkZEMS4yIGtnXkZTCl5GVDcxMyw0NTMKXkEwTiwsMTcKXkZEUGllY2VeRlMKXkZUNzAzLDQ4OQpeQTBOLCw0MQpeRkQxLzFeRlMKXkZUNzAyLDQ4OQpeQTBOLCw0MQpeRkQxLzFeRlMKXkZPMCw1MDkKXkdCNzgwLDAsMixCXkZTCl5GTzM5LDU4NApeQlkzLDIuMyw5NApeQjNOLE4sOTQsTixOCl5GRDM2NjM3OTY2MTReRlMKXkZUMTY1LDcwNApeQTBOLCwyNgpeRkRXQVlCSUxMIDM2IDYzNzkgNjYxNF5GUwpeRk85OCw3MTMKXkJZMywyLjcsMTk5Cl5CQ04sMTk5LE4sTixOLEEKXkZEMkxGUjc1MDA0KzQ2MDAwMDAwXkZTCl5GVDI0MCw5MzQKXkEwTiwsMjYKXkZEKDJMKUZSNzUwMDQrNDYwMDAwMDBeRlMKXkZPMTMxLDk0MwpeQlkzLDMuMCwxOTkKXkJDTiwxOTksTixOLE4sQQpeRkRKSkQwMTQ2MDAwMDUyNTU3NTMxMTdeRlMKXkZUMjAyLDExNjIKXkEwTiwsMjYKXkZEKEopIEpEMDEgNDYwMCAwMDUyIDU1NzUgMzExN15GUwpeRk83OSwzOTkKXkdCNjE0LDE1NywxNTcsQl5GUwpeRlQxMTAsNTAwCl5BME4sLDgxCl5GUl5GRFNBTVBMRV5GUwpeWFoK","typeCode":"label"}]}' + headers: + Connection: + - keep-alive + Content-Language: + - eng + Content-Length: + - '6923' + Content-Type: + - application/json + Date: + - Fri, 13 Feb 2026 16:09:37 GMT + Invocation-Id: + - 20260213160937_2f1b_2c47e8e9-aea6-449a-a51c-162742eb3b60 + Max-Forwards: + - '20' + Message-Reference: + - 698f4cc10c3958ccc11a769667d6bc42 + Set-Cookie: + - BIGipServer~WSB~pl_wsb-express-chd.dhl.com_443=227362981.21308.0000; path=/; + Httponly; Secure + - TS019fcce8=012d4839b33e00ca870acdba1bee2a788a1466c241bbe22a4e1cd7376d119f8b55720089832842e4a6b70a1cd2dab999ea5397dd583b405ae42ed3b3be5da5bfce0527abd2; + Path=/; Secure; HttpOnly + Via: + - 1.1 czstlls0994.prg-dc.dhl.com () + X-Content-Type-Options: + - nosniff + X-CorrelationID: + - Id-c14c8f69dd87b118ba2c6341 0 + X-XSS-Protection: + - 1; mode=block + status: + code: 201 + message: '' +version: 1 diff --git a/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_dhl_express_multi_label.yaml b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_dhl_express_multi_label.yaml new file mode 100644 index 0000000..b9359ce --- /dev/null +++ b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_dhl_express_multi_label.yaml @@ -0,0 +1,75 @@ +interactions: +- request: + body: '{"productCode": "N", "plannedShippingDateAndTime": "2026-02-13T00:00:00 + GMT+00:00", "pickup": {"isRequested": false}, "content": {"description": "N/A", + "incoterm": "DAP", "unitOfMeasurement": "metric", "packages": [{"weight": 1.2, + "dimensions": {"length": 10, "width": 10, "height": 10}, "customerReferences": + [{"value": "Parcel 1", "typeCode": "CU"}]}, {"weight": 2.5, "dimensions": {"length": + 10, "width": 10, "height": 10}, "customerReferences": [{"value": "Parcel 2", + "typeCode": "CU"}]}], "isCustomsDeclarable": false}, "outputImageProperties": + {"encodingFormat": "pdf", "imageOptions": [{"typeCode": "label", "templateName": + "ECOM26_64_002"}], "allDocumentsInOneImage": false, "splitTransportAndWaybillDocLabels": + true, "receiptAndLabelsInOneImage": false}, "accounts": [{"number": "xxxx", + "typeCode": "shipper"}], "customerReferences": [{"value": "Ref1", "typeCode": + "CU"}], "customerDetails": {"shipperDetails": {"postalAddress": {"postalCode": + "69100", "cityName": "Villeurbanne", "countryCode": "FR", "addressLine1": "27 + rue Henri Rolland", "addressLine2": "Batiment B"}, "contactInformation": {"phone": + "+33482538457", "mobilePhone": "+33482538457", "companyName": "Akretion", "fullName": + "Akretion"}}, "receiverDetails": {"postalAddress": {"postalCode": "75004", "cityName": + "Paris", "countryCode": "FR", "addressLine1": "6 Place des Vosges"}, "contactInformation": + {"phone": "+33600000000", "mobilePhone": "+33600000000", "companyName": "Hugo", + "fullName": "Hugo", "email": "hugo.victor@example.com"}}}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '1521' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.5 + x-version: + - 3.1.0 + method: POST + uri: https://express.api.dhl.com/mydhlapi/test/shipments + response: + body: + string: '{"shipmentTrackingNumber":"3663796603","trackingUrl":"https://express.api.dhl.com/mydhlapi/test/shipments/3663796603/tracking","packages":[{"referenceNumber":1,"trackingNumber":"JD014600005255753115","trackingUrl":"https://express.api.dhl.com/mydhlapi/test/shipments/3663796603/tracking?pieceTrackingNumber=JD014600005255753115"},{"referenceNumber":2,"trackingNumber":"JD014600005255753116","trackingUrl":"https://express.api.dhl.com/mydhlapi/test/shipments/3663796603/tracking?pieceTrackingNumber=JD014600005255753116"}],"documents":[{"imageFormat":"PDF","content":"JVBERi0xLjQKJfbk/N8KMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovVmVyc2lvbiAvMS40Ci9QYWdlcyAyIDAgUgo+PgplbmRvYmoKMiAwIG9iago8PAovVHlwZSAvUGFnZXMKL0tpZHMgWzMgMCBSIDQgMCBSXQovQ291bnQgMgo+PgplbmRvYmoKMyAwIG9iago8PAovQ29udGVudHMgNSAwIFIKL1R5cGUgL1BhZ2UKL1Jlc291cmNlcyA8PAovUHJvY1NldCBbL1BERiAvVGV4dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSV0KL1hPYmplY3QgPDwKL1hmMSA2IDAgUgo+Pgo+PgovUGFyZW50IDIgMCBSCi9NZWRpYUJveCBbMCAwIDI4MC42MyA0MjIuMzZdCj4+CmVuZG9iago0IDAgb2JqCjw8Ci9Db250ZW50cyA3IDAgUgovVHlwZSAvUGFnZQovUmVzb3VyY2VzIDw8Ci9Qcm9jU2V0IFsvUERGIC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJXQovWE9iamVjdCA8PAovWGYxIDggMCBSCj4+Cj4+Ci9QYXJlbnQgMiAwIFIKL01lZGlhQm94IFswIDAgMjgwLjYzIDQyMi4zNl0KPj4KZW5kb2JqCjUgMCBvYmoKPDwKL0xlbmd0aCA1MQovRmlsdGVyIC9GbGF0ZURlY29kZQo+PgpzdHJlYW0NCnicK+RyCuEyUDAxMtIzNlMISeFyDeEK5CpUMFQwAEIImZyroB+RZqjgkq8QyAUA+7wKSg0KZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9MZW5ndGggNDEwNgovU3VidHlwZSAvRm9ybQovRmlsdGVyIC9GbGF0ZURlY29kZQovVHlwZSAvWE9iamVjdAovTWF0cml4IFsxIDAgMCAxIDAgMF0KL0Zvcm1UeXBlIDEKL1Jlc291cmNlcyA8PAovUHJvY1NldCBbL1BERiAvVGV4dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSV0KL0ZvbnQgPDwKL0YxIDkgMCBSCi9GMiAxMCAwIFIKPj4KPj4KL0JCb3ggWzAgMCAyODAuNjMgNDIyLjM2XQo+PgpzdHJlYW0NCnicpZtbcx23lYXfz6/oxzhTajXQuPpNtuTYLtnW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VhNAq6Xx0eVA2WokrS7ZUoJPIhH6WGVH1QtstETXU0nagRU52AjTgI8UDhdmOYFAXqjsrHqBTbYc38ot5AjpqHKwCRZrPgpsfLdosClaYh9ddla9wNYpl/PGN8sGq3Kwdc7l7Czxb7XEh35W2Vn1ApunXM4F1+smB5vnbOblB9pcoiX30WVn1Qts4ieXxsrPWYwVOVjxGkvLECzBjhcUaLPKzqoX2ArZ7LeN74CNTVvlQCtms+fVJwAaLLmPLvvGrV5gE5ckxhZcxZscrHiNhV2RS3osMlSO6gRb67Il/a38BijAhaocYIYZcS2/lvLA8rJk+dRkZ9ULbLKyhNnKx21jRQ42QdHCJbXHOcDvhcJ63mRn1QtssgLlVn5NlCKwGVbw5jWW1x4s/XarRI4uO6teYDOWH/JDDehnlYPNUwUiv9yANlOB4iArVI4SUrzAVkt6/T0K1CFNDrbCjNDiG3JKa21DI6SuWgf5m4U3rTH8TJSXNH0bNZccW+H99U+fLF8/5V/tUNW9bBst+jFmeWc9ffDSq72Lyy/JxcVnefteezYMfQzN+ae/4hF7lw+9jUubRp7fLHXcw3I0DO0V6KgvTF48+ebF8+md7f+mf/8Cv3+UDA0KZW5kc3RyZWFtCmVuZG9iago5IDAgb2JqCjw8Ci9TdWJ0eXBlIC9UeXBlMQovVHlwZSAvRm9udAovQmFzZUZvbnQgL0hlbHZldGljYS1Cb2xkCi9FbmNvZGluZyAvV2luQW5zaUVuY29kaW5nCj4+CmVuZG9iagoxMCAwIG9iago8PAovU3VidHlwZSAvVHlwZTEKL1R5cGUgL0ZvbnQKL0Jhc2VGb250IC9IZWx2ZXRpY2EKL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcKPj4KZW5kb2JqCjExIDAgb2JqCjw8Ci9TdWJ0eXBlIC9UeXBlMQovVHlwZSAvRm9udAovQmFzZUZvbnQgL0hlbHZldGljYS1Cb2xkCi9FbmNvZGluZyAvV2luQW5zaUVuY29kaW5nCj4+CmVuZG9iagoxMiAwIG9iago8PAovU3VidHlwZSAvVHlwZTEKL1R5cGUgL0ZvbnQKL0Jhc2VGb250IC9IZWx2ZXRpY2EKL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcKPj4KZW5kb2JqCnhyZWYKMCAxMwowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMTUgMDAwMDAgbg0KMDAwMDAwMDA3OCAwMDAwMCBuDQowMDAwMDAwMTQxIDAwMDAwIG4NCjAwMDAwMDAzMjMgMDAwMDAgbg0KMDAwMDAwMDUwNSAwMDAwMCBuDQowMDAwMDAwNjI5IDAwMDAwIG4NCjAwMDAwMDQ5OTYgMDAwMDAgbg0KMDAwMDAwNTEyMCAwMDAwMCBuDQowMDAwMDA5NDg4IDAwMDAwIG4NCjAwMDAwMDk1OTAgMDAwMDAgbg0KMDAwMDAwOTY4OCAwMDAwMCBuDQowMDAwMDA5NzkxIDAwMDAwIG4NCnRyYWlsZXIKPDwKL1Jvb3QgMSAwIFIKL0lEIFs8OTlBMUExNEIxOTlEMUNFNEQ5OTQ1NjkzQjk4Mzk2MEY+IDw5OUExQTE0QjE5OUQxQ0U0RDk5NDU2OTNCOTgzOTYwRj5dCi9TaXplIDEzCj4+CnN0YXJ0eHJlZgo5ODg5CiUlRU9GCg==","typeCode":"label"}]}' + headers: + Connection: + - keep-alive + Content-Language: + - eng + Content-Length: + - '14314' + Content-Type: + - application/json + Date: + - Fri, 13 Feb 2026 16:09:37 GMT + Invocation-Id: + - 20260213160936_2f1b_23f0cc3c-077b-43fb-8ccc-b1a1d1cc95cf + Max-Forwards: + - '20' + Message-Reference: + - 698f4cc007389de28ea15f7d1257bea3 + Set-Cookie: + - BIGipServer~WSB~pl_wsb-express-chd.dhl.com_443=1181239461.21308.0000; path=/; + Httponly; Secure + - TS019fcce8=012d4839b305b9a067dee115a3e52d4488ab6a9b8d23076f1fbbe48f5e70c1784812c791888a156e8919c95285a3820a101fb10c4ebb94692c828c87db85cf96372c1c6bce; + Path=/; Secure; HttpOnly + Via: + - 1.1 czchols5949.prg-dc.dhl.com () + X-Content-Type-Options: + - nosniff + X-CorrelationID: + - Id-c04c8f698884eb23ef26e3d0 0 + X-XSS-Protection: + - 1; mode=block + status: + code: 201 + message: '' +version: 1 diff --git a/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_full_customs_declarations.yaml b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_full_customs_declarations.yaml new file mode 100644 index 0000000..6d4fc55 --- /dev/null +++ b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_full_customs_declarations.yaml @@ -0,0 +1,83 @@ +interactions: +- request: + body: '{"productCode": "P", "plannedShippingDateAndTime": "2026-02-13T00:00:00 + GMT+00:00", "pickup": {"isRequested": false}, "content": {"description": "N/A", + "incoterm": "DAP", "unitOfMeasurement": "metric", "packages": [{"weight": 1.2, + "dimensions": {"length": 10, "width": 10, "height": 10}, "customerReferences": + [{"value": "Parcel 1", "typeCode": "CU"}]}], "exportDeclaration": {"invoice": + {"number": "INV-123", "date": "2026-02-13"}, "lineItems": [{"number": 1, "description": + "Printed circuits", "price": 1.0, "quantity": {"value": 2, "unitOfMeasurement": + "PCS"}, "commodityCodes": [{"typeCode": "outbound", "value": "853400"}, {"typeCode": + "inbound", "value": "853400"}], "exportReasonType": "commercial_purpose_or_sale", + "manufacturerCountry": "FR", "weight": {"netValue": 0.5}}], "additionalCharges": + [{"caption": "Freight", "value": 123.0, "typeCode": "freight"}]}, "isCustomsDeclarable": + true, "declaredValueCurrency": "EUR", "declaredValue": 1155.0}, "outputImageProperties": + {"encodingFormat": "pdf", "imageOptions": [{"typeCode": "label", "templateName": + "ECOM26_64_002"}, {"templateName": "COMMERCIAL_INVOICE_P_10", "invoiceType": + "commercial", "isRequested": false, "typeCode": "invoice"}], "allDocumentsInOneImage": + false, "splitTransportAndWaybillDocLabels": true, "receiptAndLabelsInOneImage": + false}, "accounts": [{"number": "xxxx", "typeCode": "shipper"}], "customerReferences": + [{"value": "Ref1", "typeCode": "CU"}], "customerDetails": {"shipperDetails": + {"postalAddress": {"postalCode": "69100", "cityName": "Villeurbanne", "countryCode": + "FR", "addressLine1": "27 rue Henri Rolland", "addressLine2": "Batiment B"}, + "contactInformation": {"phone": "+33482538457", "mobilePhone": "+33482538457", + "companyName": "Akretion", "fullName": "Akretion"}}, "receiverDetails": {"postalAddress": + {"postalCode": "97100", "cityName": "Paris", "countryCode": "GP", "addressLine1": + "6 Place des Vosges"}, "contactInformation": {"phone": "+33600000000", "mobilePhone": + "+33600000000", "companyName": "Hugo", "fullName": "Hugo", "email": "hugo.victor@example.com"}}}, + "documentImages": [{"imageFormat": "PDF", "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", + "typeCode": "INV"}]}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '2242' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.5 + x-version: + - 3.1.0 + method: POST + uri: https://express.api.dhl.com/mydhlapi/test/shipments + response: + body: + string: '{"shipmentTrackingNumber":"3663796636","trackingUrl":"https://express.api.dhl.com/mydhlapi/test/shipments/3663796636/tracking","packages":[{"referenceNumber":1,"trackingNumber":"JD014600005255753119","trackingUrl":"https://express.api.dhl.com/mydhlapi/test/shipments/3663796636/tracking?pieceTrackingNumber=JD014600005255753119"}],"documents":[{"imageFormat":"PDF","content":"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","typeCode":"label"},{"imageFormat":"PDF","content":"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","typeCode":"label"}]}' + headers: + Connection: + - keep-alive + Content-Language: + - eng + Content-Length: + - '19565' + Content-Type: + - application/json + Date: + - Fri, 13 Feb 2026 16:09:39 GMT + Invocation-Id: + - 20260213160939_2f1b_da2a1ba6-2d8c-462f-ae56-7857b8348eac + Max-Forwards: + - '20' + Message-Reference: + - 698f4cc3d82773c9bb74d5188d603715 + Set-Cookie: + - BIGipServer~WSB~pl_wsb-express-chd.dhl.com_443=963135653.21308.0000; path=/; + Httponly; Secure + - TS019fcce8=012d4839b3a28c713822f50367eaae6a0ce70a57fd09fd29888a3b5a848b23dfde9379db3534bcd2e109cbe17fd9c362394c0b431aad82c27fb4b0040020519e4c10e8e692; + Path=/; Secure; HttpOnly + Via: + - 1.1 czchols5948.prg-dc.dhl.com () + X-Content-Type-Options: + - nosniff + X-CorrelationID: + - Id-c34c8f69db9bfb520b0fcac7 0 + X-XSS-Protection: + - 1; mode=block + status: + code: 201 + message: '' +version: 1 diff --git a/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_pickup_ok.yaml b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_pickup_ok.yaml new file mode 100644 index 0000000..fd67d94 --- /dev/null +++ b/roulier/carriers/dhl_express/tests/cassettes/test_dhl_express/test_pickup_ok.yaml @@ -0,0 +1,73 @@ +interactions: +- request: + body: '{"productCode": "N", "plannedShippingDateAndTime": "2026-02-13T00:00:00 + GMT+00:00", "pickup": {"isRequested": true, "location": "On the floor"}, "content": + {"description": "N/A", "incoterm": "DAP", "unitOfMeasurement": "metric", "packages": + [{"weight": 1.2, "dimensions": {"length": 10, "width": 10, "height": 10}, "customerReferences": + [{"value": "Parcel 1", "typeCode": "CU"}]}], "isCustomsDeclarable": false}, + "outputImageProperties": {"encodingFormat": "pdf", "imageOptions": [{"typeCode": + "label", "templateName": "ECOM26_64_002"}], "allDocumentsInOneImage": false, + "splitTransportAndWaybillDocLabels": true, "receiptAndLabelsInOneImage": false}, + "accounts": [{"number": "xxxx", "typeCode": "shipper"}], "customerReferences": + [{"value": "Ref1", "typeCode": "CU"}], "customerDetails": {"shipperDetails": + {"postalAddress": {"postalCode": "69100", "cityName": "Villeurbanne", "countryCode": + "FR", "addressLine1": "27 rue Henri Rolland", "addressLine2": "Batiment B"}, + "contactInformation": {"phone": "+33482538457", "mobilePhone": "+33482538457", + "companyName": "Akretion", "fullName": "Akretion"}}, "receiverDetails": {"postalAddress": + {"postalCode": "75004", "cityName": "Paris", "countryCode": "FR", "addressLine1": + "6 Place des Vosges"}, "contactInformation": {"phone": "+33600000000", "mobilePhone": + "+33600000000", "companyName": "Hugo", "fullName": "Hugo", "email": "hugo.victor@example.com"}}}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '1409' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.5 + x-version: + - 3.1.0 + method: POST + uri: https://express.api.dhl.com/mydhlapi/test/shipments + response: + body: + string: '{"shipmentTrackingNumber":"3663796625","cancelPickupUrl":"https://express.api.dhl.com/mydhlapi/test/pickups/PRG260213001724","trackingUrl":"https://express.api.dhl.com/mydhlapi/test/shipments/3663796625/tracking","dispatchConfirmationNumber":"PRG260213001724","packages":[{"referenceNumber":1,"trackingNumber":"JD014600005255753118","trackingUrl":"https://express.api.dhl.com/mydhlapi/test/shipments/3663796625/tracking?pieceTrackingNumber=JD014600005255753118"}],"documents":[{"imageFormat":"PDF","content":"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","typeCode":"label"}]}' + headers: + Connection: + - keep-alive + Content-Language: + - eng + Content-Length: + - '7688' + Content-Type: + - application/json + Date: + - Fri, 13 Feb 2026 16:09:39 GMT + Invocation-Id: + - 20260213160937_2f1b_e0b8480e-dc08-4b82-a883-2fa13cc13797 + Max-Forwards: + - '20' + Message-Reference: + - 698f4cc1e9f6ca1664e270a29c18a7ba + Set-Cookie: + - BIGipServer~WSB~pl_wsb-express-chd.dhl.com_443=1181239461.21308.0000; path=/; + Httponly; Secure + - TS019fcce8=012d4839b32fa0a8cc217058caef51f444da3376d3a9ace4e06c27039b30cb910cd5d35f1836eb944ec0dbd4b30cff6234368772f6b2b7c030beb4e150cb488615095709fd; + Path=/; Secure; HttpOnly + Via: + - 1.1 czchols5949.prg-dc.dhl.com () + X-Content-Type-Options: + - nosniff + X-CorrelationID: + - Id-c14c8f69f984a03a7fd568c4 0 + X-XSS-Protection: + - 1; mode=block + status: + code: 201 + message: '' +version: 1 diff --git a/roulier/carriers/dhl_express/tests/conftest.py b/roulier/carriers/dhl_express/tests/conftest.py new file mode 100644 index 0000000..b922f4f --- /dev/null +++ b/roulier/carriers/dhl_express/tests/conftest.py @@ -0,0 +1,6 @@ +# Copyright 2026 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from ....tests.conftest import * # noqa diff --git a/roulier/carriers/dhl_express/tests/test_dhl_express.py b/roulier/carriers/dhl_express/tests/test_dhl_express.py new file mode 100644 index 0000000..6715b5e --- /dev/null +++ b/roulier/carriers/dhl_express/tests/test_dhl_express.py @@ -0,0 +1,197 @@ +# Copyright 2026 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import json +from datetime import datetime + +import pytest + +from roulier import roulier + +from ....exception import CarrierError, InvalidApiInput +from ....helpers import merge +from ....tests.helpers import assert_data_type + +shipping_date = datetime(2026, 2, 13) + + +@pytest.fixture +def get_label_data(credentials, base_get_label_data): + data = merge( + credentials["dhl_express"], + base_get_label_data, + { + "service": { + "product": "N", + "shippingDate": shipping_date, + "shipment_description": "Test DHL Express", + "reference1": "Ref1", + } + }, + ) + for parcel in data["parcels"]: + parcel["width"] = 10 + parcel["length"] = 10 + parcel["height"] = 10 + return data + + +def before_record_request(request): + if request.body and request.headers.get("Content-Type") == "application/json": + body = json.loads(request.body.decode("utf-8")) + if "accounts" in body: + for account in body["accounts"]: + if "number" in account: + account["number"] = "xxxx" + request.body = json.dumps(body).encode("utf-8") + return request + + +def assert_label(rv, label_type="PDF", i=0): + assert "parcels" in rv + assert rv["parcels"][i]["id"] + + assert "label" in rv["parcels"][i] + label = rv["parcels"][i]["label"] + assert label["name"] == "label" + assert label["type"] == f"{label_type}" + assert_data_type(label["data"], label_type[:3]) + + +@pytest.mark.vcr( + filter_headers=["Authorization"], + before_record_request=before_record_request, +) +def test_dhl_express_label(get_label_data): + rv = roulier.get("dhl_express", "get_label", get_label_data) + assert_label(rv) + assert rv["parcels"][0]["reference"] == get_label_data["parcels"][0]["reference"] + + +@pytest.mark.vcr( + filter_headers=["Authorization"], + before_record_request=before_record_request, + ignore_localhost=True, +) +def test_dhl_express_multi_label(get_label_data): + data = get_label_data + data["parcels"].append( + { + "weight": 2.5, + "reference": "Parcel 2", + "width": 10, + "length": 10, + "height": 10, + }, + ) + rv = roulier.get("dhl_express", "get_label", data) + assert_label(rv) + assert_label(rv, i=1) + assert rv["parcels"][0]["reference"] == "Parcel 1" + assert rv["parcels"][1]["reference"] == "Parcel 2" + + +@pytest.mark.vcr( + filter_headers=["Authorization"], + before_record_request=before_record_request, +) +def test_dhl_express_label_zpl(get_label_data): + get_label_data["service"]["labelFormat"] = "ZPL" + rv = roulier.get("dhl_express", "get_label", get_label_data) + assert_label(rv, "ZPL") + + +@pytest.mark.vcr( + filter_headers=["Authorization"], + before_record_request=before_record_request, +) +def test_dhl_express_bad_product(get_label_data): + get_label_data["service"]["product"] = "?" + with pytest.raises(CarrierError) as excinfo: + roulier.get("dhl_express", "get_label", get_label_data) + + assert excinfo.value.args[0][0]["id"] == 8007 + assert excinfo.value.args[0][0]["message"] == ( + "Bad request (Error getting Product details from GREF)" + ) + + +@pytest.mark.vcr( + filter_headers=["Authorization"], + before_record_request=before_record_request, +) +def test_common_failed_get_label_1(get_label_data): + get_label_data["parcels"][0]["weight"] = 0 + with pytest.raises(CarrierError, match="0.0 is not greater or equal to 0.001"): + roulier.get("dhl_express", "get_label", get_label_data) + + +@pytest.mark.vcr( + filter_headers=["Authorization"], + before_record_request=before_record_request, +) +def test_common_failed_get_label_2(get_label_data): + del get_label_data["from_address"]["country"] + with pytest.raises(InvalidApiInput) as excinfo: + roulier.get("dhl_express", "get_label", get_label_data) + + assert "Invalid input data" in str(excinfo.value) + assert "from_address.country\n Field required" in str(excinfo.value) + + +@pytest.mark.vcr( + filter_headers=["Authorization"], + before_record_request=before_record_request, +) +def test_common_failed_get_label_3(get_label_data): + del get_label_data["to_address"]["country"] + with pytest.raises(InvalidApiInput) as excinfo: + roulier.get("dhl_express", "get_label", get_label_data) + + assert "Invalid input data" in str(excinfo.value) + assert "to_address.country\n Field required" in str(excinfo.value) + + +@pytest.mark.vcr( + filter_headers=["Authorization"], + before_record_request=before_record_request, +) +def test_pickup_ok(get_label_data): + get_label_data["service"]["pickupLocationId"] = "On the floor" + rv = roulier.get("dhl_express", "get_label", get_label_data) + assert_label(rv) + + +@pytest.mark.vcr( + filter_headers=["Authorization"], + before_record_request=before_record_request, +) +def test_full_customs_declarations(get_label_data): + """Complete customsDeclarations""" + get_label_data["service"]["product"] = "P" + get_label_data["to_address"]["country"] = "GP" # Guadeloupe + get_label_data["to_address"]["zip"] = "97100" # Basse-Terre + + get_label_data["customs"] = { + "invoice": { + "number": "INV-123", + "date": shipping_date.date(), + "type": "gif", + "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", + }, + "articles": [ + { + "description": "Printed circuits", + "quantity": 2, + "weight": 0.5, + "value": 1.0, + "hsCode": "853400", + "originCountry": "FR", + } + ], + "vat": 0, + "delivery": 123, + } + + rv = roulier.get("dhl_express", "get_label", get_label_data) + assert_label(rv) diff --git a/roulier/helpers.py b/roulier/helpers.py index 66aa7c3..41332bf 100644 --- a/roulier/helpers.py +++ b/roulier/helpers.py @@ -1,8 +1,9 @@ # Copyright 2024 Akretion (http://www.akretion.com). # @author Florian Mounier # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from typing import ClassVar import unicodedata +from typing import ClassVar + from .schema import LabelInput, LabelOutput REMOVED = ClassVar[None] # Hack to remove a field from inherited class @@ -52,6 +53,8 @@ def merge(*dicts): for k, v in d.items(): if isinstance(v, dict): result[k] = merge(result.get(k, {}), v) + elif isinstance(v, list): + result[k] = result.get(k, []) + v else: if not v and result.get(k): # Do not override value with empty value