Odoo_dev_Patterns Components - DEV Community
Odoo_dev_Patterns Components - DEV Community
9 2
Introduction to Odoo Components
#odoo #python #oca
One of the greatest strength of Odoo, besides its active community, is the extensibility
provided through modules.
At the core of this extensibility is the model inheritance, with a concept of re-openable
models.
Models extensions
A simplified definition of the partner model:
from odoo import models
class ResPartner(models.Model)
_name = "res.partner"
name = fields.Char(required=True)
# ... other fields
def _address_fields(self):
"""Returns the list of address fields that are synced from the parent."
return list(ADDRESS_FIELDS)
Any module can extend the model, add fields or methods. Here, the module
partner_address_street3 which adds a third street field:
class ResPartner(models.Model):
_inherit = "res.partner"
def _address_fields(self):
fields = super()._address_fields()[:]
fields.append('street3')
return fields
If you are familiar with Odoo development, you probably already know this. If this is
new to you and are interested in more details, you may start from the beginning.
Behind the scene
The extensions possibilities are powerful, but they also have a weakness, encouraged
by the framework, to grow what some would call fat models. Every module adds their
own methods and logic in the models, which can lead to issues like namespace
collisions, bad readability due to mixing of responsibities or duplicated logic.
I like the single responsibility principle and the idea of composition over inheritance
when it makes sense. Well decoupled classes are easier to test as well.
That being said, we can say that what I am looking for is a way to define "Service
classes", classes which can do one thing and do it well. Using a bare Python class
would not be a solution, as it would not be extensible by other Odoo modules. But...
Odoo has this already.
It is not widely used for this purpose, but the classmodels.AbstractModel can be
described as a model which is not backed by a database table, therefore, it has no
fields, only pure Python logic. Its name can be confusing, because it is not "abstract"
in the pure OOP definition of a class that cannot be instantiated. It really is about the
database storage.
The two main purposes I have seen for Abstract Models are:
"Real" abstract models which are then included in normal Models to share and
provide additional fields or logic (mixin / multiple inheritance's way)
Services, which is related the topic of this article
Here is how a service based on an Abstract Model could be defined:
class SettlePaymentService(models.AbstractModel):
_name = "settle.payment.service"
# ...
def button_settle(self):
self.env["settle.payment.service"].settle(self)
If I'm speaking about services, this is because this is what Components are about. Are
Abstract Models enough for this use case? Yes, they are. No, they aren't. "It depends."
is often a pretty good answer to a technical question. I'll describe what they can do,
and let you judge if you need them or not.
Let's begin with components
A bit of context
The Components are provided by an Odoo Community Association module. You can
find it on GitHub
OCA / connector
Odoo generic connector framework (jobs queue, asynchronous tasks, channels)
class SettlePayment(Component)
_name = "settle.payment.service"
You will have noted that until there, it looks pretty similar to the examples with Models
above, with reason. The classes and inheritance mechanism are the same but:
The Component classes never have any database table.
There is no _inherits as it only makes sense for models with tables. _inherit
works the same way.
There is no equivalent to TransientModel , it would not make any sense without
table.
There is an AbstractComponent class, which can not be instanciated, it is only used
as a "base interface / base implementation" for other Components to depend on.
You can get a component by its name from a Model, a bit like when you get a Model
from self.env :
class PaymentTransaction(models.Model):
_inherit = "payment.transaction"
def button_settle(self):
with self.env["payment.collection"].work_on(self._name) as work:
settle_service = work.component_by_name("settle.payment.service")
settle_service.settle(self)
def button_export(self):
self.ensure_one()
with self.env["my.collection"].work_on(self._name) as work:
work.component(usage="record.exporter").export(self)
Why is it better to find my component by usage rather than by name? Because the
name is fixed and is what is used to define the dependencies. If a module that I don't
control defines base components and they are get by name in the middle of a chunk
of code, I'm stuck. If they are get by usage, I can use the inheritance (
_inherit ) to
modify the usage of the initial component, and create an entirely new or variant of the
component with the expected usage ( _usage ). This is also what allows dynamic
dispatching of components, as we will see in the next section.
In less words: the name defines the inheritance, the usage defines which component
is used at runtime.
Want more?
I keep using the generic exporter defined above, but I will add a different exporter for
products:
class CSVRecordExporter(Component):
_name = "csv.record.exporter"
_inherit = "generic.record.exporter"
_usage = "record.exporter"
_apply_on = ["res.partner"]
The export method here builds a CSV and pushes it to a sFTP. The _inheritis not
strictly mandatory, but often makes sense. The real addition for the example is
_apply_on .
And the caller of the component... didn't change:
class ResPartner(models.Model):
_inherit = "res.partner"
def button_export(self):
self.ensure_one()
with self.env["my.collection"].work_on(self._name) as work:
work.component(usage="record.exporter").export(self)
The code is 100% the same as the code we had for Users right? And yet, when we call
button_export , the data will only be logged for users, and be exported as CSV for
partners.
The _name of the 2 exporters is different, but we didn't had to change the caller of the
service. Hopefully, you should start to see the potential for code sharing and
extensibility (in a real example,button_export could then be added in an abstract
model and included both in the users and the partner models, then the same method
returns the appropriate component for each model).
Want more?
🌈 you will pretty often see the words collection and backend used
interchangeably, because historically, the Connector module was using solely the
term "Backend". The latter did not really made sense for the more generic
Component system, so it became Collection.
What then from this collection? It relates to a Model, which will be the reference for
this collection. It can be a Model or an AbstractModel depending of the need. The
benefit of a Model is that it can store fields, such as the URL of the service, the
credentials, ...
class FooCollection(models.Model):
_name = 'foo.collection'
_inherit = 'collection.base'
The _name of the model has to match the _collection of its components, and the
model has to _inherit from collection.base .
Now to the work_on context manager. It is made available by collection.base . It
initializes and returns a WorkContext , which is the container for the current context
(collection, model, env and arbitrary values) that makes the glue between the current
"Context" (not the Odoo context, but the collection, model and values we want to use
in components).
✨ The WorkContext is a bit like the of Odoo but for Components.
env
work_on() bridges the gap between Odoo Models and Components. From a model,
this is needed to be able to reach components.
class FooCollection(models.Model):
_name = 'foo.collection'
_inherit = 'collection.base'
def sync_from_foo(self):
with self.env["foo.collection"].work_on("res.partner") as work:
work.component(usage="sync").run_sync(self)
This minimal component shows that we can split the work in small services, with their
own responsibilities. Here, we have one service reading data from a webservice, and a
second service which maps the external data to a dictionary that we can feed to
write() .
Want more?
Summary
Components are used to build service classes. They are registered in Collections
(registries of components). They have an inheritance system similar as the Model
classes.
To be able to work with components, we use the context manager work_on() on the
Collection. When we ask a component for a given usage, the collection's registry will
match the components in this order:
Find a matching Component for the given usage ( _usage) for the current collection
(
_collection ) and model ( _apply_on )
Find a matching Component for the given usage ( _usage) for the current collection
(
_collection ) and any model (no _apply_on on component)
Find a matching Component for the given usage ( _usage) for any collection (no
_collection on component) and any model (no _apply_on on component)
In any of the steps above, in case of multiple candidates, the method
_component_match is called on each candidate to restrict further the match.
AbstractComponent are never returned by the dispatch mechanism, they can be used
to share code in concrete components.
Next?
Maybe you are wondering what is "Connector" then? Maybe I should write about it one
day, but in a few words: the Connector module is a set of pre-defined components
specialized to implement connectors with external services.
Thank you for reading this far!
Top comments (3)
Guewen Baconnier • 21 juin 21
Glad it helped!
Now, this article is about "bare" components, connectors should be the matter of
another entire post ;)
Also the idea I want to promote is that eventually the design of a connector or other
module based on components should be adapted to the use case and the way you see
it. An implementation should not be distorted only to fit in a pattern. Hence,
understanding the components is the most useful thing, then you can pick or not some
pieces of the connector module if they help, or write your own.
Coding Dodo • 21 juin 21
Great in-depth article !👏 👏
Code of Conduct • Report abuse
Tidelift PROMOTED
Guewen Baconnier
500 Internal Error
LOCATION
Switzerland
WORK
Software Developer
JOINED
8 juin 2020
Tidelift PROMOTED