diff --git a/delivery_dhl_parcel_de/README.rst b/delivery_dhl_parcel_de/README.rst index f9ea7428..e3e85c54 100644 --- a/delivery_dhl_parcel_de/README.rst +++ b/delivery_dhl_parcel_de/README.rst @@ -9,9 +9,25 @@ Features * Create DHL Parcel DE shipments directly from Odoo * Update tracking information in Odoo from DHL * Generate and print shipping labels in Odoo +* Deliver to DHL Packstations (lockers) Note that shipping rates are not retrieved from DHL but need to be configured. +Packstation (Locker) Delivery +------------------------------ + +To ship to a DHL Packstation, set the recipient's address fields as follows: + +* **Street 1**: The recipient's DHL post number (6–10 digits). +* **Street 2**: ``Packstation ``, e.g. ``Packstation 183`` or + ``Packstation: 183`` (case-insensitive). The locker ID is a 3-digit integer + (100–999) identifying the Packstation. +* **ZIP / City / Country**: The Packstation's postal address. + +The module detects the keyword ``Packstation`` in Street 2 and automatically +sends a locker-addressed shipment request to DHL instead of a regular contact +address. + Configuration ------------- diff --git a/delivery_dhl_parcel_de/models/delivery_carrier.py b/delivery_dhl_parcel_de/models/delivery_carrier.py index 3f9c4412..00641300 100644 --- a/delivery_dhl_parcel_de/models/delivery_carrier.py +++ b/delivery_dhl_parcel_de/models/delivery_carrier.py @@ -1,6 +1,7 @@ import binascii import json import logging +import re import requests @@ -88,6 +89,27 @@ class DeliveryCarrier(models.Model): help="Obtained via Get Access! (app creation) and manually approved by DHL.", ) + @staticmethod + def _is_packstation(street2): + """Return True if street2 indicates a DHL Packstation (locker) delivery.""" + return bool(street2 and "packstation" in street2.lower()) + + @staticmethod + def _get_packstation_locker_id(street2): + """Extract the locker ID from a street2 field. + + Removes the keyword 'Packstation' (case-insensitive), any adjacent + colons and surrounding whitespace, returning only the numeric locker ID. + + Examples:: + + "Packstation 123" -> "123" + "Packstation: 456" -> "456" + "PACKSTATION : 789" -> "789" + """ + locker_id = re.sub(r"(?i)\s*packstation\s*:?\s*", "", street2).strip() + return locker_id + def _calculate_package_insurance(self, picking, package_weight, total_weight): """ Calculate insurance value for a package based on weight distribution. @@ -293,6 +315,34 @@ def dhl_parcel_de_provider_get_package_info(self, picking, insurance_value): + self.dhl_participation_no ) + if self._is_packstation(receiver_street2): + locker_id = self._get_packstation_locker_id(receiver_street2) + consignee = { + "name1": receiver_company or recipient_address_id.name, + "name2": receiver_street, + "addressStreet": "Packstation", + "addressHouse": locker_id, + "postalCode": receiver_zip, + "city": receiver_city, + "country": receiver_country_code, + "email": receiver_email, + "phone": receiver_phone, + } + else: + consignee = { + "name1": receiver_company or recipient_address_id.name, + "name2": recipient_address_id.name + if receiver_company + else receiver_street2, + "name3": receiver_street2 if receiver_company else "", + "addressStreet": receiver_street, + "postalCode": receiver_zip, + "city": receiver_city, + "country": receiver_country_code, + "email": receiver_email, + "phone": receiver_phone, + } + package_data = { "product": self.dhl_services_name, "billingNumber": billingNumber, @@ -307,21 +357,7 @@ def dhl_parcel_de_provider_get_package_info(self, picking, insurance_value): "email": sender_email, "phone": sender_phone, }, - "consignee": { - "name1": receiver_company - if receiver_company - else recipient_address_id.name, - "name2": recipient_address_id.name - if receiver_company - else receiver_street2, - "name3": receiver_street2 if receiver_company else "", - "addressStreet": receiver_street, - "postalCode": receiver_zip, - "city": receiver_city, - "country": receiver_country_code, - "email": receiver_email, - "phone": receiver_phone, - }, + "consignee": consignee, } europe_group_id = self.env.ref("base.europe") diff --git a/delivery_dhl_parcel_de/tests/__init__.py b/delivery_dhl_parcel_de/tests/__init__.py new file mode 100644 index 00000000..ba10de5d --- /dev/null +++ b/delivery_dhl_parcel_de/tests/__init__.py @@ -0,0 +1 @@ +from . import test_packstation diff --git a/delivery_dhl_parcel_de/tests/test_packstation.py b/delivery_dhl_parcel_de/tests/test_packstation.py new file mode 100644 index 00000000..42e37592 --- /dev/null +++ b/delivery_dhl_parcel_de/tests/test_packstation.py @@ -0,0 +1,39 @@ +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + +from odoo.addons.delivery_dhl_parcel_de.models.delivery_carrier import DeliveryCarrier + + +@tagged("post_install", "-at_install") +class TestPackstation(TransactionCase): + """Unit tests for DHL Packstation (locker) address parsing helpers.""" + + def test_is_packstation(self): + """Street2 containing 'Packstation' (any case) is detected as a locker.""" + self.assertTrue(DeliveryCarrier._is_packstation("Packstation 123")) + self.assertTrue(DeliveryCarrier._is_packstation("PACKSTATION 456")) + self.assertTrue(DeliveryCarrier._is_packstation("packstation 789")) + + def test_is_packstation_false(self): + """Normal street2 values, empty strings and None are not Packstations.""" + self.assertFalse(DeliveryCarrier._is_packstation("Apartment 2B")) + self.assertFalse(DeliveryCarrier._is_packstation("")) + self.assertFalse(DeliveryCarrier._is_packstation(None)) + + def test_get_locker_id_simple(self): + """Locker ID is extracted from 'Packstation NNN' formats.""" + self.assertEqual( + DeliveryCarrier._get_packstation_locker_id("Packstation 123"), "123" + ) + self.assertEqual( + DeliveryCarrier._get_packstation_locker_id("PACKSTATION 456"), "456" + ) + + def test_get_locker_id_with_colon(self): + """Locker ID is extracted when a colon separates the keyword from the ID.""" + self.assertEqual( + DeliveryCarrier._get_packstation_locker_id("Packstation: 456"), "456" + ) + self.assertEqual( + DeliveryCarrier._get_packstation_locker_id("PACKSTATION : 789"), "789" + )