Imagine trying to manage shipping in Odoo if every delivery carrier's logic was crammed into one place. For every action (getting a rate, sending a package, or tracking it), you'd face a monstrous if/elif/else block. Adding a new carrier would mean diving into this complex code, risking breaking existing integrations. It would be a developer's worst nightmare: unscalable, fragile, and a headache to maintain.
That’s when Template Method pattern comes into play. In this post, we take a closer look at the pattern and how Odoo’s delivery module implements it.
What is Template Method pattern?
The Template Method is a behavioral design pattern that defines the skeleton of an algorithm in a base class but lets sub-classes override specific steps of the algorithm without changing its overall structure (see Refactoring.Guru)
In simpler terms, it provides a template for how something should be done, but allows you to fill in the specific details later. The main class calls a series of methods, some of which are abstract or have a default implementation, while sub-classes provide the concrete implementation for those specific methods.How Odoo implements the pattern in delivery module?
Odoo uses the delivery.carrier model as the template. This model contains the main logic for handling shipments. When you want to add a new shipping provider, you don't modify the core delivery module. Instead, you create a new module and follow a clear, structured process.
Inherit the Base Class: Create your model that inherits from delivery.carrier. This makes your new provider a part of the delivery system.
Declare Your Provider: Extend the selection options for the delivery_type field. This field acts as the dispatcher, telling Odoo which set of methods to use for a given carrier.
Implement the Specific Steps: Add the required methods for your provider, following a strict naming convention: <provider_name>_<method_name>. The core delivery.carrier model will dynamically call these methods based on the delivery_type you selected, thanks to powerful Python functions: hasattr and getattr.
With this pattern, the main send_shipping method in delivery.carrier doesn't contain any provider-specific logic. It simply looks at the carrier's delivery_type and calls the corresponding method (e.g., dhl_send_shipping, fedex_send_shipping…).
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)
Real-world implementation in OCA
delivery_cttexpress module is a perfect example of this pattern in action. In most delivery connector modules, we usually see these common model files:1. cttexpress_request.py: an interface between client (CTT Express SOAP API in this case) and Odoo
2. delivery_carrier.py: inherited DeliveryCarrier to extends the delivery_type, and implements the cttexpress_* methods.
This approach keeps the code clean, modular, and easy to extend without ever touching Odoo's core code.