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:

  1. Inherit the base class. Create a model that inherits from delivery.carrier, so your provider becomes part of the delivery system.
  2. Declare your provider. Extend the selection options of the delivery_type field. This field acts as the dispatcher, telling Odoo which set of methods to use for a given carrier.
  3. Implement the specific steps. Add the methods your provider needs, following the naming convention <provider_name>_<method_name>. The core delivery.carrier model calls these methods dynamically based on the selected delivery_type, using Python’s hasattr and getattr.

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: inherits delivery.carrier, adds cttexpress to delivery_type and implements the cttexpress_* methods, such as cttexpress_send_shipping, cttexpress_cancel_shipment and cttexpress_get_label.

This approach keeps the code clean and modular, and new carriers can be added without touching Odoo’s core code.