Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions delivery_dhl_parcel_de/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <lockerID>``, 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
-------------

Expand Down
66 changes: 51 additions & 15 deletions delivery_dhl_parcel_de/models/delivery_carrier.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import binascii
import json
import logging
import re

import requests

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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")
Expand Down
1 change: 1 addition & 0 deletions delivery_dhl_parcel_de/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_packstation
39 changes: 39 additions & 0 deletions delivery_dhl_parcel_de/tests/test_packstation.py
Original file line number Diff line number Diff line change
@@ -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"
)
Loading