diff --git a/delivery_roulier/models/stock_quant_package.py b/delivery_roulier/models/stock_quant_package.py index 92df5db6da..7d679e3bdd 100644 --- a/delivery_roulier/models/stock_quant_package.py +++ b/delivery_roulier/models/stock_quant_package.py @@ -11,8 +11,9 @@ _logger = logging.getLogger(__name__) try: - from roulier import roulier from roulier.exception import CarrierError, InvalidApiInput + + from roulier import roulier except ImportError: _logger.debug("Cannot `import roulier`.") diff --git a/delivery_roulier/tests/test_delivery_roulier.py b/delivery_roulier/tests/test_delivery_roulier.py index 8a6cfa572a..864c21f2d5 100644 --- a/delivery_roulier/tests/test_delivery_roulier.py +++ b/delivery_roulier/tests/test_delivery_roulier.py @@ -2,10 +2,11 @@ from unittest.mock import MagicMock, patch from odoo_test_helper import FakeModelLoader -from roulier import roulier from odoo.tests.common import SavepointCase +from roulier import roulier + roulier_ret = { "parcels": [ { diff --git a/delivery_roulier_dhl_express/README.rst b/delivery_roulier_dhl_express/README.rst new file mode 100644 index 0000000000..b23dfdcf96 --- /dev/null +++ b/delivery_roulier_dhl_express/README.rst @@ -0,0 +1,84 @@ +============================ +Delivery Carrier DHL Express +============================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:b265053a9e222e1f48fc77481db992870e3d8a768298b1743b6c57b6db6ae08a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fdelivery--carrier-lightgray.png?logo=github + :target: https://github.com/OCA/delivery-carrier/tree/14.0/delivery_roulier_dhl_express + :alt: OCA/delivery-carrier +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/delivery-carrier-14-0/delivery-carrier-14-0-delivery_roulier_dhl_express + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/delivery-carrier&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Send and track parcels with DHL Express. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion + +Contributors +------------ + +- Florian Mounier florian.mounier@akretion.com + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px + :target: https://github.com/paradoxxxzero + :alt: paradoxxxzero + +Current `maintainer `__: + +|maintainer-paradoxxxzero| + +This module is part of the `OCA/delivery-carrier `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/delivery_roulier_dhl_express/__init__.py b/delivery_roulier_dhl_express/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/delivery_roulier_dhl_express/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/delivery_roulier_dhl_express/__manifest__.py b/delivery_roulier_dhl_express/__manifest__.py new file mode 100644 index 0000000000..2e59b67625 --- /dev/null +++ b/delivery_roulier_dhl_express/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2026 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Delivery Carrier DHL Express", + "version": "14.0.1.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "summary": "DHL Express integration through Roulier API", + "category": "Warehouse", + "depends": [ + "delivery_roulier_option", # for customs specific roulier code (yeah...) + "stock_quant_package_dimension", # dhl requires package dimensions + "intrastat_base", # for customs declaration + ], + "website": "https://github.com/OCA/delivery-carrier", + "data": [ + "views/carrier_account_views.xml", + "data/product.product.xml", + "data/delivery_carrier.xml", + ], + "maintainers": ["paradoxxxzero"], + "demo": [ + "demo/carrier_account.xml", + ], + "installable": True, + "license": "AGPL-3", +} diff --git a/delivery_roulier_dhl_express/data/delivery_carrier.xml b/delivery_roulier_dhl_express/data/delivery_carrier.xml new file mode 100644 index 0000000000..df597ca6bd --- /dev/null +++ b/delivery_roulier_dhl_express/data/delivery_carrier.xml @@ -0,0 +1,115 @@ + + + + + + + DHL Domestic Express France (DOM) + N + + dhl_express + + + + DHL Domestic Express 12:00 France (DOT) + 1 + + dhl_express + + + + DHL Domestic Express 10:30 France (DOL) + O + + dhl_express + + + + DHL Express Worldwide EU UE hors France (ECX) + U + + dhl_express + + + + DHL Express 12:00 DOC Monde hors France (TDT) + T + + dhl_express + + + + DHL Express 10:30 DOC Monde hors France (TDL) + L + + dhl_express + + + + DHL Express 9:00 DOC Monde hors France (TDK) + K + + dhl_express + + + + DHL Economy Select (EU) UE hors France, Malte et Chypre (ESU) + W + + dhl_express + + + + DHL Express Worldwide DOC Monde hors UE hors FR (DOX) + D + + dhl_express + + + + DHL Express Worldwide Non DOC Monde hors UE hors FR (WPX) + P + + dhl_express + + + + DHL Express 12:00 Non DOC Monde hors UE hors FR (TDY) + Y + + dhl_express + + + + DHL Express 10:30 Non DOC Monde hors UE hors FR (TDM) + M + + dhl_express + + + + DHL Express 9:00 Non DOC Monde hors UE hors FR (TDE) + E + + dhl_express + + + + DHL Economy Select (Non DOC) Uniquement Norvège, Suisse et Royaume-Uni (ESI) + H + + dhl_express + + + + diff --git a/delivery_roulier_dhl_express/data/product.product.xml b/delivery_roulier_dhl_express/data/product.product.xml new file mode 100644 index 0000000000..fa244965db --- /dev/null +++ b/delivery_roulier_dhl_express/data/product.product.xml @@ -0,0 +1,13 @@ + + + + + SHIP_DHL_EXPRESS + service + Coûts de livraison - DHL Express + + diff --git a/delivery_roulier_dhl_express/demo/carrier_account.xml b/delivery_roulier_dhl_express/demo/carrier_account.xml new file mode 100644 index 0000000000..52a3c8b5f6 --- /dev/null +++ b/delivery_roulier_dhl_express/demo/carrier_account.xml @@ -0,0 +1,14 @@ + + + + + DHL Express default + Demo Account + Demo password + dhl_express + + diff --git a/delivery_roulier_dhl_express/models/__init__.py b/delivery_roulier_dhl_express/models/__init__.py new file mode 100644 index 0000000000..099b83ee07 --- /dev/null +++ b/delivery_roulier_dhl_express/models/__init__.py @@ -0,0 +1,4 @@ +from . import stock_picking +from . import delivery_carrier +from . import stock_quant_package +from . import carrier_account diff --git a/delivery_roulier_dhl_express/models/carrier_account.py b/delivery_roulier_dhl_express/models/carrier_account.py new file mode 100644 index 0000000000..fec702224b --- /dev/null +++ b/delivery_roulier_dhl_express/models/carrier_account.py @@ -0,0 +1,22 @@ +# Copyright 2026 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class CarrierAccount(models.Model): + _inherit = "carrier.account" + + dhl_express_account_number = fields.Char() + dhl_express_file_format = fields.Selection( + selection=[ + ("PDF", "PDF"), + ("ZPL", "ZPL"), + ("DPL", "LP2"), + ("EPL", "EPL"), + ], + string="DHL Express File Format", + help="Default format of the carrier's label you want to print", + ) diff --git a/delivery_roulier_dhl_express/models/delivery_carrier.py b/delivery_roulier_dhl_express/models/delivery_carrier.py new file mode 100644 index 0000000000..fcd78a6c58 --- /dev/null +++ b/delivery_roulier_dhl_express/models/delivery_carrier.py @@ -0,0 +1,14 @@ +# Copyright 2026 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class DeliveryCarrier(models.Model): + _inherit = "delivery.carrier" + + delivery_type = fields.Selection( + selection_add=[("dhl_express", "DHL Express")], + ondelete={"dhl_express": "set default"}, + ) diff --git a/delivery_roulier_dhl_express/models/stock_picking.py b/delivery_roulier_dhl_express/models/stock_picking.py new file mode 100644 index 0000000000..66461688fb --- /dev/null +++ b/delivery_roulier_dhl_express/models/stock_picking.py @@ -0,0 +1,37 @@ +# Copyright 2026 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + def _dhl_express_get_service(self, account, package=None): + service = self._roulier_get_service(account, package=package) + service.update( + { + "product": self.carrier_code, + "customerId": account.dhl_express_account_number, + "shipment_description": f"Shipment {self.name} from " + f"{self.partner_id.name} from Odoo", + "reference1": self.partner_id.name[:35] + if self.partner_id.name + else "/", + } + ) + return service + + @api.model + def _dhl_express_convert_address(self, partner): + address = self._roulier_convert_address(partner) or {} + # Use get_split_adress from partner_helper module + # to split the address on 3 lines + streets = partner._get_split_address(3, 45) + ( + address["street1"], + address["street2"], + address["street3"], + ) = streets + return address diff --git a/delivery_roulier_dhl_express/models/stock_quant_package.py b/delivery_roulier_dhl_express/models/stock_quant_package.py new file mode 100644 index 0000000000..a4661cc667 --- /dev/null +++ b/delivery_roulier_dhl_express/models/stock_quant_package.py @@ -0,0 +1,125 @@ +# 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 odoo import _, models +from odoo.exceptions import UserError + +CUSTOMS_MAP = { + "gift": "gift", + "sample": "sample", + "commercial": "commercial_purpose_or_sale", + "other": "personal_belongings_or_personal_use", + "return": "return", +} + + +class StockQuantPackage(models.Model): + _inherit = "stock.quant.package" + + def _dhl_express_get_tracking_link(self): + return ( + "https://www.dhl.com/fr-en/home/tracking.html?tracking-id=%s&submit=1" + % self.parcel_tracking + ) + + def _dhl_express_should_include_customs(self, picking): + rv = self._roulier_should_include_customs(picking) + if not rv and picking.carrier_code in "PYMEH": + raise UserError( + _( + "Customs declaration is required for Non DOC DHL Express shipments. " + "Please check the delivery carrier configuration for picking %s" + ) + % picking.id + ) + if rv: + return rv + + def _dhl_express_before_call(self, picking, payload): + # For DHL customs are declared at root level + if self._should_include_customs(picking): + payload["customs"] = self._get_customs(picking) + return payload + + def _dhl_express_get_customs(self, picking): + customs = self._roulier_get_customs(picking) + for article in customs.get("articles", []): + article["exportType"] = CUSTOMS_MAP.get(picking.customs_category) + + # We now need the linked invoice for customs declaration + linked_so = picking.sale_id or picking.move_lines.mapped( + "sale_line_id.order_id" + ) + if len(linked_so) != 1: + raise UserError( + _( + "Cannot determine the sales order linked to picking %s. " + "Please make sure there is only one sales order linked to " + "the picking for international shipments." + ) + % picking.id + ) + + if linked_so.invoice_status == "no": + raise UserError( + _( + "The sales order %s linked to picking %s has no invoice. " + "Please make sure there is an invoice linked to the sales order " + "for international shipments." + ) + % (linked_so.id, picking.id) + ) + + # Should we? + # if linked_so.invoice_status == "to invoice": + # new_invoice = linked_so._create_invoices() + # new_invoice.action_post() + + invoice = linked_so.invoice_ids.filtered(lambda inv: inv.state == "posted") + if not invoice: + raise UserError( + _( + "The sales order %s linked to picking %s has no valid invoice. " + "Please make sure there is a valid invoice linked to the sales order " + "for international shipments." + ) + % (linked_so.id, picking.id) + ) + + if len(invoice) > 1: + raise UserError( + _( + "The sales order %s linked to picking %s has multiple valid invoices. " + "Please make sure there is only one valid invoice linked to " + "the sales order " + "for international shipments." + ) + % (linked_so.id, picking.id) + ) + + report = self.env.ref("account.account_invoices").with_user(1) + content, _filename = report._render( + [invoice.id], {"report_type": report.report_type} + ) + + customs["invoice"] = { + "number": invoice.name, + "date": invoice.invoice_date.isoformat(), + "content": b64encode(content), + } + + customs["vat"] = invoice.amount_tax + # delivery, insurance? + + return customs + + def _dhl_express_get_parcel(self, picking): + return { + **self._roulier_get_parcel(picking), + "length": self.pack_length, + "width": self.width, + "height": self.height, + } diff --git a/delivery_roulier_dhl_express/readme/CONTRIBUTORS.md b/delivery_roulier_dhl_express/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..328a37da87 --- /dev/null +++ b/delivery_roulier_dhl_express/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Florian Mounier diff --git a/delivery_roulier_dhl_express/readme/DESCRIPTION.md b/delivery_roulier_dhl_express/readme/DESCRIPTION.md new file mode 100644 index 0000000000..d1cdc82fb0 --- /dev/null +++ b/delivery_roulier_dhl_express/readme/DESCRIPTION.md @@ -0,0 +1 @@ +Send and track parcels with DHL Express. diff --git a/delivery_roulier_dhl_express/static/description/index.html b/delivery_roulier_dhl_express/static/description/index.html new file mode 100644 index 0000000000..2019249d69 --- /dev/null +++ b/delivery_roulier_dhl_express/static/description/index.html @@ -0,0 +1,425 @@ + + + + + +Delivery Carrier DHL Express + + + +
+

Delivery Carrier DHL Express

+ + +

Beta License: AGPL-3 OCA/delivery-carrier Translate me on Weblate Try me on Runboat

+

Send and track parcels with DHL Express.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

paradoxxxzero

+

This module is part of the OCA/delivery-carrier project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/delivery_roulier_dhl_express/views/carrier_account_views.xml b/delivery_roulier_dhl_express/views/carrier_account_views.xml new file mode 100644 index 0000000000..1b801e89fa --- /dev/null +++ b/delivery_roulier_dhl_express/views/carrier_account_views.xml @@ -0,0 +1,27 @@ + + + + + carrier.account + + + + + + + + + diff --git a/setup/delivery_roulier_dhl_express/odoo/addons/delivery_roulier_dhl_express b/setup/delivery_roulier_dhl_express/odoo/addons/delivery_roulier_dhl_express new file mode 120000 index 0000000000..68ad651b83 --- /dev/null +++ b/setup/delivery_roulier_dhl_express/odoo/addons/delivery_roulier_dhl_express @@ -0,0 +1 @@ +../../../../delivery_roulier_dhl_express \ No newline at end of file diff --git a/setup/delivery_roulier_dhl_express/setup.py b/setup/delivery_roulier_dhl_express/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/delivery_roulier_dhl_express/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)