Helpers¶
Polytropos provides helper functions for Odoo addon development.
Installation¶
To use helpers in your addon, add polytropos to your external dependencies:
Usage¶
Import the polytropos.ph module to use the helper functions:
from polytropos import ph
if ph.rel(">=18.0"):
# Code specific to Odoo 18+
do_something()
else:
# Code for Odoo 17 and earlier
do_something_else()
This module requires Odoo to be installed. If Odoo is not available, importing this
module will raise ModuleNotFoundError.
Logging¶
Polytropos uses Python's standard logging system. You can control the log level using
the POLYTROPOS_LOGGING environment variable:
# Set log level to DEBUG
export POLYTROPOS_LOGGING=DEBUG
# Set log level to INFO
export POLYTROPOS_LOGGING=INFO
# Set log level to WARNING (default)
export POLYTROPOS_LOGGING=WARNING
This is useful for debugging conditional code execution and version checks at build time.
At run time, you can also set its value using standard Odoo logging handlers:
Version-conditional method overrides¶
When a method's behavior changes between Odoo releases, you can implement both variants
in a single class using @ph.override and .when():
from odoo import models
from polytropos import ph
class ResPartner(models.Model):
_inherit = "res.partner"
@ph.override
def write(self, vals):
"""Normalize email on write (Odoo 19+)."""
if "email" in vals:
vals["email"] = vals["email"].strip().lower()
return super().write(vals)
@write.when("<19")
def write(self, vals):
"""Also normalize name in older releases."""
if "name" in vals:
vals["name"] = vals["name"].strip()
return self.write.default(self, vals)
At class creation time, the current Odoo release is checked:
- If the current release is Odoo 19+, the default implementation is kept.
- If the current release is Odoo 18 or older, the variant is kept.
- The non-matching variant is discarded — it does not pollute the class.
How it works¶
@ph.override wraps the default method in an OverrideResolver descriptor. Each
.when(spec) call registers a version-conditional variant. When the class is created,
__set_name__ evaluates the Odoo release against each specifier in declaration order
(first match wins) and replaces the descriptor with the resolved function.
The resolved function has the correct __name__ and __qualname__, so super(),
method resolution order (MRO), and Odoo's metaclass all work naturally.
Unlimited variants¶
You can define as many variants as needed:
class MyModel(models.Model):
@ph.override
def compute_price(self, product):
return product.list_price # default for >=19
@compute_price.when(">=18,<19")
def compute_price(self, product):
return product.list_price * 1.21 # VAT included in 18
@compute_price.when("<18")
def compute_price(self, product):
return product.price # different field in 17
Variants are evaluated in declaration order; the first matching specifier wins. If no variant matches, the default implementation is used.
Inheritance¶
Subclasses inherit the resolved method naturally via MRO. A subclass can define its own
@ph.override + variants, which resolve independently without affecting the parent.
Calling the default from a variant¶
When a variant only needs to add a pre/post-processing step for a specific release while
keeping the core logic from the default, call the default via
self.method.default(self, ...):
@write.when("<19")
def write(self, vals):
if "name" in vals:
vals["name"] = vals["name"].strip()
return self.write.default(self, vals)
The resolved function (whether default or variant) always exposes a .default attribute
pointing to the original default implementation. It is an unbound function, so you must
pass self explicitly.
You can also use .default for introspection or testing:
partner = self.env["res.partner"].browse(42)
# The method that's actually used for this release
partner.write({"email": " Foo@Bar.COM "})
# The original default implementation (unbound)
partner.write.default(partner, {"email": " Foo@Bar.COM "})
Infinite recursion risk
The default's own .default points to itself. Calling
self.method.default(self, ...) from within the default implementation causes
infinite recursion. Only call .default from within a .when() variant.
When .default does NOT work¶
Delegating to the default only works when the method signature is the same across
releases. If the parent method's signature changed (e.g. name_search's second
parameter changed from args to domain), the default's super() call would fail on
older releases. You have two options:
Option A — Helper method¶
Extract shared logic to a helper method, each variant calls super() independently:
from odoo import models
from polytropos import ph
class ResPartner(models.Model):
_inherit = "res.partner"
@ph.override
def name_search(self, name, domain=None, operator="ilike", limit=100):
domain = list(domain or [])
self._name_search_domain(domain)
return super().name_search(name, domain=domain, operator=operator, limit=limit)
@name_search.when("<19")
def name_search(self, name, args=None, operator="ilike", limit=100):
args = list(args or [])
self._name_search_domain(args)
return super().name_search(name, args=args, operator=operator, limit=limit)
def _name_search_domain(self, domain):
"""Add extra domain conditions for name_search."""
domain.append(("loves_polytropos", "=", True))
Option B — Dynamic kwargs¶
The default uses ph.rel() to build the correct keyword argument for super()
dynamically. This allows the variant to still delegate to the default:
from odoo import models
from polytropos import ph
class ResCompany(models.Model):
_inherit = "res.company"
@ph.override
def name_search(self, name, domain=None, operator="ilike", limit=100):
domain = list(domain or [])
domain.append(("active", "=", True))
super_kwargs = {"args" if ph.rel("<19") else "domain": domain}
return super().name_search(name, operator=operator, limit=limit, **super_kwargs)
@name_search.when("<19")
def name_search(self, name, args=None, operator="ilike", limit=100):
return self.name_search.default(
self, name, domain=args, operator=operator, limit=limit
)
Rule of thumb: use .default when the difference is a pre/post-processing step or a
decorator. When the parent's signature changed, use a helper method or dynamic kwargs.
What if Odoo is not installed?¶
If odoo.release.series is not available (e.g. in a standalone script), the default
implementation is always used.