What the Template Method pattern is and how Odoo's delivery module uses it to add new shipping carriers without changing core code.
Imagine managing shipping in Odoo with every delivery carrier’s logic crammed into one place. For each action (getting a rate, sending a package or tracking it), you would face a long if/elif/else block, and adding a new carrier would mean changing that code and risking the integrations that already work. It would be hard to scale, fragile and difficult to maintain.
This is where the Template Method pattern comes in. In this post, we look at the pattern and at how Odoo’s delivery module implements it.
What Is the Template Method Pattern?
The Template Method is a behavioral design pattern that defines the skeleton of an algorithm in a base class and lets subclasses override specific steps without changing the overall structure (see Refactoring.Guru ). In simpler terms, it gives you a template for how something is done and lets you fill in the details later. The base class calls a series of methods, some of them abstract or with a default implementation, and subclasses provide the concrete implementation of those methods.
How Odoo Implements the Pattern in the Delivery Module
Odoo uses the delivery.carrier model as the template. It holds the main logic for handling shipments. To add a new shipping provider, you don’t modify the core delivery module. Instead, you create a new module and follow three steps:
- Inherit the base class. Create a model that inherits from
delivery.carrier, so your provider becomes part of the delivery system. - Declare your provider. Extend the selection options of the
delivery_typefield. This field acts as the dispatcher, telling Odoo which set of methods to use for a given carrier. - Implement the specific steps. Add the methods your provider needs, following the naming convention
<provider_name>_<method_name>. The coredelivery.carriermodel calls these methods dynamically based on the selecteddelivery_type, using Python’shasattrandgetattr.
As a result, the main send_shipping method in delivery.carrier contains no provider-specific logic. It looks at the carrier’s delivery_type and calls the matching method, such as dhl_send_shipping or fedex_send_shipping. In Odoo 17.0, this method is defined in the stock_delivery module:
def send_shipping(self, pickings):
''' Send the package to the service provider
:param pickings: A recordset of pickings
:return list: A list of dictionaries (one per picking) containing of the form::
{ 'exact_price': price,
'tracking_number': number }
# TODO missing labels per package
# TODO missing currency
# TODO missing success, error, warnings
'''
self.ensure_one()
if hasattr(self, '%s_send_shipping' % self.delivery_type):
return getattr(self, '%s_send_shipping' % self.delivery_type)(pickings)
A Real-World Example in OCA
The OCA delivery_cttexpress
module shows the pattern in practice. Like most delivery connector modules, it is organized around two model files:
cttexpress_request.py: the interface between Odoo and the carrier’s API, here the CTT Express SOAP API.delivery_carrier.py: inheritsdelivery.carrier, addscttexpresstodelivery_typeand implements thecttexpress_*methods, such ascttexpress_send_shipping,cttexpress_cancel_shipmentandcttexpress_get_label.
This approach keeps the code clean and modular, and new carriers can be added without touching Odoo’s core code.