Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,44 @@
# Components Events
Odoo addon: component_event
## Installation
```bash
pip install odoo-bringout-oca-connector-component_event
```
## Dependencies
This addon depends on:
- component
## Manifest Information
- **Name**: Components Events
- **Version**: 16.0.1.0.1
- **Category**: Generic Modules
- **License**: LGPL-3
- **Installable**: True
## Source
Based on [OCA/connector](https://github.com/OCA/connector) branch 16.0, addon `component_event`.
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,141 @@
=================
Components Events
=================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:b1bf7560d845dddfd31dd556b137084d31b15f146451998947fdfa0a60669f21
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fconnector-lightgray.png?logo=github
:target: https://github.com/OCA/connector/tree/16.0/component_event
:alt: OCA/connector
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/connector-16-0/connector-16-0-component_event
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/connector&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module implements an event system (`Observer pattern`_) and is a
base block for the Connector Framework. It can be used without
using the full Connector though. It is built upon the ``component`` module.
Documentation: http://odoo-connector.com/
.. _Observer pattern: https://en.wikipedia.org/wiki/Observer_pattern
**Table of contents**
.. contents::
:local:
Usage
=====
As a developer, you have access to a events system. You can find the
documentation in the code or on http://odoo-connector.com
In a nutshell, you can create trigger events::
class Base(models.AbstractModel):
_inherit = 'base'
@api.model
def create(self, vals):
record = super(Base, self).create(vals)
self._event('on_record_create').notify(record, fields=vals.keys())
return record
And subscribe listeners to the events::
from odoo.addons.component.core import Component
from odoo.addons.component_event import skip_if
class MagentoListener(Component):
_name = 'magento.event.listener'
_inherit = 'base.connector.listener'
@skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
def on_record_create(self, record, fields=None):
""" Called when a record is created """
record.with_delay().export_record(fields=fields)
This module triggers 3 events:
* ``on_record_create(record, fields=None)``
* ``on_record_write(record, fields=None)``
* ``on_record_unlink(record)``
Changelog
=========
.. [ The change log. The goal of this file is to help readers
understand changes between version. The primary audience is
end users and integrators. Purely technical changes such as
code refactoring must not be mentioned here.
This file may contain ONE level of section titles, underlined
with the ~ (tilde) character. Other section markers are
forbidden and will likely break the structure of the README.rst
or other documents where this fragment is included. ]
Next
~~~~
12.0.1.0.0 (2018-11-26)
~~~~~~~~~~~~~~~~~~~~~~~
* [MIGRATION] from 12.0 branched at rev. 324e006
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/connector/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/connector/issues/new?body=module:%20component_event%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Camptocamp
Contributors
~~~~~~~~~~~~
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/connector <https://github.com/OCA/connector/tree/16.0/component_event>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,6 @@
from . import core
from . import components
from . import models
# allow public API 'from odoo.addons.component_event import skip_if'
from .components.event import skip_if # noqa

View file

@ -0,0 +1,15 @@
# Copyright 2019 Camptocamp SA
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
{
"name": "Components Events",
"version": "16.0.1.0.1",
"author": "Camptocamp," "Odoo Community Association (OCA)",
"website": "https://github.com/OCA/connector",
"license": "LGPL-3",
"category": "Generic Modules",
"depends": ["component"],
"external_dependencies": {"python": ["cachetools"]},
"data": [],
"installable": True,
}

View file

@ -0,0 +1 @@
from . import event

View file

@ -0,0 +1,298 @@
# Copyright 2017 Camptocamp SA
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
"""
Events
======
Events are a notification system.
On one side, one or many listeners await for an event to happen. On
the other side, when such event happen, a notification is sent to
the listeners.
An example of event is: 'when a record has been created'.
The event system allows to write the notification code in only one place, in
one Odoo addon, and to write as many listeners as we want, in different places,
different addons.
We'll see below how the ``on_record_create`` is implemented.
Notifier
--------
The first thing is to find where/when the notification should be sent.
For the creation of a record, it is in :meth:`odoo.models.BaseModel.create`.
We can inherit from the `'base'` model to add this line:
::
class Base(models.AbstractModel):
_inherit = 'base'
@api.model
def create(self, vals):
record = super(Base, self).create(vals)
self._event('on_record_create').notify(record, fields=vals.keys())
return record
The :meth:`..models.base.Base._event` method has been added to the `'base'`
model, so an event can be notified from any model. The
:meth:`CollectedEvents.notify` method triggers the event and forward the
arguments to the listeners.
This should be done only once. See :class:`..models.base.Base` for a list of
events that are implemented in the `'base'` model.
Listeners
---------
Listeners are Components that respond to the event names.
The components must have a ``_usage`` equals to ``'event.listener'``, but it
doesn't to be set manually if the component inherits from
``'base.event.listener'``
Here is how we would log something each time a record is created::
class MyEventListener(Component):
_name = 'my.event.listener'
_inherit = 'base.event.listener'
def on_record_create(self, record, fields=None):
_logger.info("%r has been created", record)
Many listeners such as this one could be added for the same event.
Collection and models
---------------------
In the example above, the listeners is global. It will be executed for any
model and collection. You can also restrict a listener to only a collection or
model, using the ``_collection`` or ``_apply_on`` attributes.
::
class MyEventListener(Component):
_name = 'my.event.listener'
_inherit = 'base.event.listener'
_collection = 'magento.backend'
def on_record_create(self, record, fields=None):
_logger.info("%r has been created", record)
class MyModelEventListener(Component):
_name = 'my.event.listener'
_inherit = 'base.event.listener'
_apply_on = ['res.users']
def on_record_create(self, record, fields=None):
_logger.info("%r has been created", record)
If you want an event to be restricted to a collection, the
notification must also precise the collection, otherwise all listeners
will be executed::
collection = self.env['magento.backend']
self._event('on_foo_created', collection=collection).notify(record, vals)
An event can be skipped based on a condition evaluated from the notified
arguments. See :func:`skip_if`
"""
import logging
import operator
from collections import defaultdict
from functools import wraps
# pylint: disable=W7950
from odoo.addons.component.core import AbstractComponent, Component
_logger = logging.getLogger(__name__)
try:
from cachetools import LRUCache, cachedmethod
except ImportError:
_logger.debug("Cannot import 'cachetools'.")
__all__ = ["skip_if"]
# Number of items we keep in LRU cache when we collect the events.
# 1 item means: for an event name, model_name, collection, return
# the event methods
DEFAULT_EVENT_CACHE_SIZE = 512
def skip_if(cond):
"""Decorator allowing to skip an event based on a condition
The condition is a python lambda expression, which takes the
same arguments than the event.
Example::
@skip_if(lambda self, *args, **kwargs:
self.env.context.get('connector_no_export'))
def on_record_write(self, record, fields=None):
_logger('I'll delay a job, but only if we didn't disabled '
' the export with a context key')
record.with_delay().export_record()
@skip_if(lambda self, record, kind: kind == 'complete')
def on_record_write(self, record, kind):
_logger("I'll delay a job, but only if the kind is 'complete'")
record.with_delay().export_record()
"""
def skip_if_decorator(func):
@wraps(func)
def func_wrapper(*args, **kwargs):
if cond(*args, **kwargs):
return
else:
return func(*args, **kwargs)
return func_wrapper
return skip_if_decorator
class CollectedEvents:
"""Event methods ready to be notified
This is a rather internal class. An instance of this class
is prepared by the :class:`EventCollecter` when we need to notify
the listener that the event has been triggered.
:meth:`EventCollecter.collect_events` collects the events,
feed them to the instance, so we can use the :meth:`notify` method
that will forward the arguments and keyword arguments to the
listeners of the event.
::
>>> # collecter is an instance of CollectedEvents
>>> collecter.collect_events('on_record_create').notify(something)
"""
def __init__(self, events):
self.events = events
def notify(self, *args, **kwargs):
"""Forward the arguments to every listeners of an event"""
for event in self.events:
event(*args, **kwargs)
class EventCollecter(Component):
"""Component that collects the event from an event name
For doing so, it searches all the components that respond to the
``event.listener`` ``_usage`` and having an event of the same
name.
Then it feeds the events to an instance of :class:`EventCollecter`
and return it to the caller.
It keeps the results in a cache, the Component is rebuilt when
the Odoo's registry is rebuilt, hence the cache is cleared as well.
An event always starts with ``on_``.
Note that the special
:class:`odoo.addons.component_event.core.EventWorkContext` class should be
used for this Component, because it can work
without a collection.
It is used by :meth:`odoo.addons.component_event.models.base.Base._event`.
"""
_name = "base.event.collecter"
@classmethod
def _complete_component_build(cls):
"""Create a cache on the class when the component is built"""
super(EventCollecter, cls)._complete_component_build()
# the _cache being on the component class, which is
# dynamically rebuild when odoo registry is rebuild, we
# are sure that the result is always the same for a lookup
# until the next rebuild of odoo's registry
cls._cache = LRUCache(maxsize=DEFAULT_EVENT_CACHE_SIZE)
return
def _collect_events(self, name):
collection_name = None
if self.work._collection is not None:
collection_name = self.work.collection._name
return self._collect_events_cached(collection_name, self.work.model_name, name)
@cachedmethod(operator.attrgetter("_cache"))
def _collect_events_cached(self, collection_name, model_name, name):
events = defaultdict(set)
component_classes = self.work.components_registry.lookup(
collection_name=collection_name,
usage="event.listener",
model_name=model_name,
)
for cls in component_classes:
if cls.has_event(name):
events[cls].add(name)
return events
def _init_collected_events(self, class_events):
events = set()
for cls, names in class_events.items():
for name in names:
component = cls(self.work)
events.add(getattr(component, name))
return events
def collect_events(self, name):
"""Collect the events of a given name"""
if not name.startswith("on_"):
raise ValueError("an event name always starts with 'on_'")
events = self._init_collected_events(self._collect_events(name))
return CollectedEvents(events)
class EventListener(AbstractComponent):
"""Base Component for the Event listeners
Events must be methods starting with ``on_``.
Example: :class:`RecordsEventListener`
"""
_name = "base.event.listener"
_usage = "event.listener"
@classmethod
def has_event(cls, name):
"""Indicate if the class has an event of this name"""
return name in cls._events
@classmethod
def _build_event_listener_component(cls):
"""Make a list of events listeners for this class"""
events = set()
if not cls._abstract:
for attr_name in dir(cls):
if attr_name.startswith("on_"):
events.add(attr_name)
cls._events = events
@classmethod
def _complete_component_build(cls):
super(EventListener, cls)._complete_component_build()
cls._build_event_listener_component()
return

View file

@ -0,0 +1,160 @@
# Copyright 2017 Camptocamp SA
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
"""
Events Internals
================
Core classes for the events system.
"""
from odoo.addons.component.core import WorkContext
class EventWorkContext(WorkContext):
"""Work context used by the Events internals
Should not be used outside of the events internals.
The work context to use generally is
:class:`odoo.addons.component.core.WorkContext` or your own
subclass.
The events are a special kind of components because they are
not attached to any collection (they can but not the main use case).
So the work context must not need to have a collection, but when
it has no collection, it must at least have an 'env'.
When no collection is provided, the methods to get the Components
cannot be used, but :meth:`work_on` can be used to switch back to
a :class:`odoo.addons.component.core.WorkContext` with collection.
This is needed when one want to get a component for a collection
from inside an event listener.
"""
def __init__(
self,
model_name=None,
collection=None,
env=None,
components_registry=None,
**kwargs
):
if not (collection is not None or env):
raise ValueError("collection or env is required")
if collection and env:
# when a collection is used, the env will be the one of
# the collection
raise ValueError("collection and env cannot both be provided")
self.env = env
super(EventWorkContext, self).__init__(
model_name=model_name,
collection=collection,
components_registry=components_registry,
**kwargs
)
if self._env:
self._propagate_kwargs.remove("collection")
self._propagate_kwargs.append("env")
@property
def env(self):
"""Return the current Odoo env"""
if self._env:
return self._env
return super(EventWorkContext, self).env
@env.setter
def env(self, value):
self._env = value
@property
def collection(self):
"""Return the current Odoo env"""
if self._collection is not None:
return self._collection
raise ValueError("No collection, it is optional for EventWorkContext")
@collection.setter
def collection(self, value):
self._collection = value
def work_on(self, model_name=None, collection=None):
"""Create a new work context for another model keeping attributes
Used when one need to lookup components for another model.
Used on an EventWorkContext, it switch back to a normal
WorkContext. It means we are inside an event listener, and
we want to get a component. We need to set a collection
to be able to get components.
"""
if self._collection is None and collection is None:
raise ValueError("you must provide a collection to work with")
if collection is not None:
if self.env is not collection.env:
raise ValueError(
"the Odoo env of the collection must be "
"the same than the current one"
)
kwargs = {
attr_name: getattr(self, attr_name) for attr_name in self._propagate_kwargs
}
kwargs.pop("env", None)
if collection is not None:
kwargs["collection"] = collection
if model_name is not None:
kwargs["model_name"] = model_name
return WorkContext(**kwargs)
def component_by_name(self, name, model_name=None):
if self._collection is not None:
# switch to a normal WorkContext
work = self.work_on(collection=self._collection, model_name=model_name)
else:
raise TypeError(
"Can't be used on an EventWorkContext without collection. "
"The collection must be known to find components.\n"
"Hint: you can set the collection and get a component with:\n"
">>> work.work_on(collection=self.env[...].browse(...))\n"
">>> work.component_by_name(name, model_name=model_name)"
)
return work.component_by_name(name, model_name=model_name)
def component(self, usage=None, model_name=None):
if self._collection is not None:
# switch to a normal WorkContext
work = self.work_on(collection=self._collection, model_name=model_name)
else:
raise TypeError(
"Can't be used on an EventWorkContext without collection. "
"The collection must be known to find components.\n"
"Hint: you can set the collection and get a component with:\n"
">>> work.work_on(collection=self.env[...].browse(...))\n"
">>> work.component(usage=usage, model_name=model_name)"
)
return work.component(usage=usage, model_name=model_name)
def many_components(self, usage=None, model_name=None):
if self._collection is not None:
# switch to a normal WorkContext
work = self.work_on(collection=self._collection, model_name=model_name)
else:
raise TypeError(
"Can't be used on an EventWorkContext without collection. "
"The collection must be known to find components.\n"
"Hint: you can set the collection and get a component with:\n"
">>> work.work_on(collection=self.env[...].browse(...))\n"
">>> work.many_components(usage=usage, model_name=model_name)"
)
return work.component(usage=usage, model_name=model_name)
def __str__(self):
return "EventWorkContext({},{})".format(
repr(self._env or self._collection), self.model_name
)

View file

@ -0,0 +1,19 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * component_event
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: component_event
#: model:ir.model,name:component_event.model_base
msgid "Base"
msgstr "Osnova"

View file

@ -0,0 +1,19 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * component_event
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: component_event
#: model:ir.model,name:component_event.model_base
msgid "Base"
msgstr ""

View file

@ -0,0 +1,22 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * component_event
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2023-08-02 13:09+0000\n"
"Last-Translator: Ivorra78 <informatica@totmaterial.es>\n"
"Language-Team: none\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: component_event
#: model:ir.model,name:component_event.model_base
msgid "Base"
msgstr "Base"

View file

@ -0,0 +1,27 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * component_event
#
# Translators:
# Nicolas JEUDY <njeudy@panda-chi.io>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-01 01:48+0000\n"
"PO-Revision-Date: 2018-02-01 01:48+0000\n"
"Last-Translator: Nicolas JEUDY <njeudy@panda-chi.io>, 2018\n"
"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. module: component_event
#: model:ir.model,name:component_event.model_base
msgid "Base"
msgstr ""
#~ msgid "base"
#~ msgstr "base"

View file

@ -0,0 +1,22 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * component_event
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-05-06 14:34+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: component_event
#: model:ir.model,name:component_event.model_base
msgid "Base"
msgstr "Base"

View file

@ -0,0 +1,22 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * component_event
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2019-09-01 06:14+0000\n"
"Last-Translator: 黎伟杰 <674416404@qq.com>\n"
"Language-Team: none\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 3.8\n"
#. module: component_event
#: model:ir.model,name:component_event.model_base
msgid "Base"
msgstr "基础"

View file

@ -0,0 +1 @@
from . import base

View file

@ -0,0 +1,119 @@
# Copyright 2017 Camptocamp SA
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
"""
Base Model
==========
Extend the 'base' Odoo Model to add Events related features.
"""
from odoo import api, models
from odoo.addons.component.core import _component_databases
from ..components.event import CollectedEvents
from ..core import EventWorkContext
class Base(models.AbstractModel):
"""The base model, which is implicitly inherited by all models.
Add an :meth:`_event` method to all Models. This method allows to
trigger events.
It also notifies the following events:
* ``on_record_create(self, record, fields=None)``
* ``on_record_write(self, record, fields=none)``
* ``on_record_unlink(self, record)``
``on_record_unlink`` is notified just *before* the unlink is done.
"""
_inherit = "base"
def _event(self, name, collection=None, components_registry=None):
"""Collect events for notifications
Usage::
def button_do_something(self):
for record in self:
# do something
self._event('on_do_something').notify('something')
With this line, every listener having a ``on_do_something`` method
with be called and receive 'something' as argument.
See: :mod:`..components.event`
:param name: name of the event, start with 'on_'
:param collection: optional collection to filter on, only
listeners with similar ``_collection`` will be
notified
:param components_registry: component registry for lookups,
mainly used for tests
:type components_registry:
:class:`odoo.addons.components.core.ComponentRegistry`
"""
dbname = self.env.cr.dbname
components_registry = self.env.context.get(
"components_registry", components_registry
)
comp_registry = components_registry or _component_databases.get(dbname)
if not comp_registry or not comp_registry.ready:
# No event should be triggered before the registry has been loaded
# This is a very special case, when the odoo registry is being
# built, it calls odoo.modules.loading.load_modules().
# This function might trigger events (by writing on records, ...).
# But at this point, the component registry is not guaranteed
# to be ready, and anyway we should probably not trigger events
# during the initialization. Hence we return an empty list of
# events, the 'notify' calls will do nothing.
return CollectedEvents([])
if not comp_registry.get("base.event.collecter"):
return CollectedEvents([])
model_name = self._name
if collection is not None:
work = EventWorkContext(
collection=collection,
model_name=model_name,
components_registry=components_registry,
)
else:
work = EventWorkContext(
env=self.env,
model_name=model_name,
components_registry=components_registry,
)
collecter = work._component_class_by_name("base.event.collecter")(work)
return collecter.collect_events(name)
@api.model_create_multi
def create(self, vals_list):
records = super(Base, self).create(vals_list)
for idx, vals in enumerate(vals_list):
fields = list(vals.keys())
self._event("on_record_create").notify(records[idx], fields=fields)
return records
def write(self, vals):
result = super(Base, self).write(vals)
fields = list(vals.keys())
for record in self:
self._event("on_record_write").notify(record, fields=fields)
return result
def unlink(self):
for record in self:
self._event("on_record_unlink").notify(record)
result = super(Base, self).unlink()
return result

View file

@ -0,0 +1 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>

View file

@ -0,0 +1,7 @@
This module implements an event system (`Observer pattern`_) and is a
base block for the Connector Framework. It can be used without
using the full Connector though. It is built upon the ``component`` module.
Documentation: http://odoo-connector.com/
.. _Observer pattern: https://en.wikipedia.org/wiki/Observer_pattern

View file

@ -0,0 +1,17 @@
.. [ The change log. The goal of this file is to help readers
understand changes between version. The primary audience is
end users and integrators. Purely technical changes such as
code refactoring must not be mentioned here.
This file may contain ONE level of section titles, underlined
with the ~ (tilde) character. Other section markers are
forbidden and will likely break the structure of the README.rst
or other documents where this fragment is included. ]
Next
~~~~
12.0.1.0.0 (2018-11-26)
~~~~~~~~~~~~~~~~~~~~~~~
* [MIGRATION] from 12.0 branched at rev. 324e006

View file

@ -0,0 +1,34 @@
As a developer, you have access to a events system. You can find the
documentation in the code or on http://odoo-connector.com
In a nutshell, you can create trigger events::
class Base(models.AbstractModel):
_inherit = 'base'
@api.model
def create(self, vals):
record = super(Base, self).create(vals)
self._event('on_record_create').notify(record, fields=vals.keys())
return record
And subscribe listeners to the events::
from odoo.addons.component.core import Component
from odoo.addons.component_event import skip_if
class MagentoListener(Component):
_name = 'magento.event.listener'
_inherit = 'base.connector.listener'
@skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
def on_record_create(self, record, fields=None):
""" Called when a record is created """
record.with_delay().export_record(fields=fields)
This module triggers 3 events:
* ``on_record_create(record, fields=None)``
* ``on_record_write(record, fields=None)``
* ``on_record_unlink(record)``

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,487 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Components Events</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="components-events">
<h1 class="title">Components Events</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:b1bf7560d845dddfd31dd556b137084d31b15f146451998947fdfa0a60669f21
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/connector/tree/16.0/component_event"><img alt="OCA/connector" src="https://img.shields.io/badge/github-OCA%2Fconnector-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/connector-16-0/connector-16-0-component_event"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/connector&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module implements an event system (<a class="reference external" href="https://en.wikipedia.org/wiki/Observer_pattern">Observer pattern</a>) and is a
base block for the Connector Framework. It can be used without
using the full Connector though. It is built upon the <tt class="docutils literal">component</tt> module.</p>
<p>Documentation: <a class="reference external" href="http://odoo-connector.com/">http://odoo-connector.com/</a></p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#changelog" id="toc-entry-2">Changelog</a><ul>
<li><a class="reference internal" href="#next" id="toc-entry-3">Next</a></li>
<li><a class="reference internal" href="#section-1" id="toc-entry-4">12.0.1.0.0 (2018-11-26)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-5">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-6">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-7">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-8">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-9">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>As a developer, you have access to a events system. You can find the
documentation in the code or on <a class="reference external" href="http://odoo-connector.com">http://odoo-connector.com</a></p>
<p>In a nutshell, you can create trigger events:</p>
<pre class="literal-block">
class Base(models.AbstractModel):
_inherit = 'base'
&#64;api.model
def create(self, vals):
record = super(Base, self).create(vals)
self._event('on_record_create').notify(record, fields=vals.keys())
return record
</pre>
<p>And subscribe listeners to the events:</p>
<pre class="literal-block">
from odoo.addons.component.core import Component
from odoo.addons.component_event import skip_if
class MagentoListener(Component):
_name = 'magento.event.listener'
_inherit = 'base.connector.listener'
&#64;skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
def on_record_create(self, record, fields=None):
&quot;&quot;&quot; Called when a record is created &quot;&quot;&quot;
record.with_delay().export_record(fields=fields)
</pre>
<p>This module triggers 3 events:</p>
<ul class="simple">
<li><tt class="docutils literal">on_record_create(record, fields=None)</tt></li>
<li><tt class="docutils literal">on_record_write(record, fields=None)</tt></li>
<li><tt class="docutils literal">on_record_unlink(record)</tt></li>
</ul>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-2">Changelog</a></h1>
<!-- [ The change log. The goal of this file is to help readers
understand changes between version. The primary audience is
end users and integrators. Purely technical changes such as
code refactoring must not be mentioned here.
This file may contain ONE level of section titles, underlined
with the ~ (tilde) character. Other section markers are
forbidden and will likely break the structure of the README.rst
or other documents where this fragment is included. ] -->
<div class="section" id="next">
<h2><a class="toc-backref" href="#toc-entry-3">Next</a></h2>
</div>
<div class="section" id="section-1">
<h2><a class="toc-backref" href="#toc-entry-4">12.0.1.0.0 (2018-11-26)</a></h2>
<ul class="simple">
<li>[MIGRATION] from 12.0 branched at rev. 324e006</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-5">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/connector/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/connector/issues/new?body=module:%20component_event%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-6">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-7">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-8">Contributors</a></h2>
<ul class="simple">
<li>Guewen Baconnier &lt;<a class="reference external" href="mailto:guewen.baconnier&#64;camptocamp.com">guewen.baconnier&#64;camptocamp.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-9">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/connector/tree/16.0/component_event">OCA/connector</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1 @@
from . import test_event

View file

@ -0,0 +1,461 @@
# Copyright 2017 Camptocamp SA
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
import unittest
from unittest import mock
from odoo.tests.common import MetaCase, tagged
from odoo.addons.component.core import Component
from odoo.addons.component.tests.common import (
ComponentRegistryCase,
TransactionComponentRegistryCase,
)
from odoo.addons.component_event.components.event import skip_if
from odoo.addons.component_event.core import EventWorkContext
@tagged("standard", "at_install")
class TestEventWorkContext(unittest.TestCase, MetaCase("DummyCase", (), {})):
"""Test Events Components"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.test_sequence = 0
def setUp(self):
super().setUp()
self.env = mock.MagicMock(name="env")
self.record = mock.MagicMock(name="record")
self.components_registry = mock.MagicMock(name="ComponentRegistry")
def test_env(self):
"""WorkContext with env"""
work = EventWorkContext(
model_name="res.users",
env=self.env,
components_registry=self.components_registry,
)
self.assertEqual(self.env, work.env)
self.assertEqual("res.users", work.model_name)
with self.assertRaises(ValueError):
# pylint: disable=W0104
work.collection # noqa
def test_collection(self):
"""WorkContext with collection"""
env = mock.MagicMock(name="env")
collection = mock.MagicMock(name="collection")
collection.env = env
work = EventWorkContext(
model_name="res.users",
collection=collection,
components_registry=self.components_registry,
)
self.assertEqual(collection, work.collection)
self.assertEqual(env, work.env)
self.assertEqual("res.users", work.model_name)
def test_env_and_collection(self):
"""WorkContext with collection and env is forbidden"""
env = mock.MagicMock(name="env")
collection = mock.MagicMock(name="collection")
collection.env = env
with self.assertRaises(ValueError):
EventWorkContext(
model_name="res.users",
collection=collection,
env=env,
components_registry=self.components_registry,
)
def test_missing(self):
"""WorkContext with collection and env is forbidden"""
with self.assertRaises(ValueError):
EventWorkContext(
model_name="res.users", components_registry=self.components_registry
)
def test_env_work_on(self):
"""WorkContext propagated through work_on"""
env = mock.MagicMock(name="env")
collection = mock.MagicMock(name="collection")
collection.env = env
work = EventWorkContext(
env=env,
model_name="res.users",
components_registry=self.components_registry,
)
work2 = work.work_on(model_name="res.partner", collection=collection)
self.assertEqual("WorkContext", work2.__class__.__name__)
self.assertEqual(env, work2.env)
self.assertEqual("res.partner", work2.model_name)
self.assertEqual(self.components_registry, work2.components_registry)
with self.assertRaises(ValueError):
# pylint: disable=W0104
work.collection # noqa
def test_collection_work_on(self):
"""WorkContext propagated through work_on"""
env = mock.MagicMock(name="env")
collection = mock.MagicMock(name="collection")
collection.env = env
work = EventWorkContext(
collection=collection,
model_name="res.users",
components_registry=self.components_registry,
)
work2 = work.work_on(model_name="res.partner")
self.assertEqual("WorkContext", work2.__class__.__name__)
self.assertEqual(collection, work2.collection)
self.assertEqual(env, work2.env)
self.assertEqual("res.partner", work2.model_name)
self.assertEqual(self.components_registry, work2.components_registry)
def test_collection_work_on_collection(self):
"""WorkContext collection changed with work_on"""
env = mock.MagicMock(name="env")
collection = mock.MagicMock(name="collection")
collection.env = env
work = EventWorkContext(
model_name="res.users",
env=env,
components_registry=self.components_registry,
)
work2 = work.work_on(collection=collection)
# when work_on is used inside an event component, we want
# to switch back to a normal WorkContext, because we don't
# need anymore the EventWorkContext
self.assertEqual("WorkContext", work2.__class__.__name__)
self.assertEqual(collection, work2.collection)
self.assertEqual(env, work2.env)
self.assertEqual("res.users", work2.model_name)
self.assertEqual(self.components_registry, work2.components_registry)
class TestEvent(ComponentRegistryCase):
"""Test Events Components"""
def setUp(self):
super(TestEvent, self).setUp()
self._setup_registry(self)
self._load_module_components("component_event")
# get the collecter to notify the event
# we don't mind about the collection and the model here,
# the events we test are global
env = mock.MagicMock()
self.work = EventWorkContext(
model_name="res.users", env=env, components_registry=self.comp_registry
)
self.collecter = self.comp_registry["base.event.collecter"](self.work)
def test_event(self):
class MyEventListener(Component):
_name = "my.event.listener"
_inherit = "base.event.listener"
def on_record_create(self, recipient, something, fields=None):
recipient.append(("OK", something, fields))
MyEventListener._build_component(self.comp_registry)
something = object()
fields = ["name", "code"]
# as there is no return value by the event, we
# modify this recipient to check it has been called
recipient = []
# collect the event and notify it
self.collecter.collect_events("on_record_create").notify(
recipient, something, fields=fields
)
self.assertEqual([("OK", something, fields)], recipient)
def test_collect_several(self):
class MyEventListener(Component):
_name = "my.event.listener"
_inherit = "base.event.listener"
def on_record_create(self, recipient, something, fields=None):
recipient.append(("OK", something, fields))
class MyOtherEventListener(Component):
_name = "my.other.event.listener"
_inherit = "base.event.listener"
def on_record_create(self, recipient, something, fields=None):
recipient.append(("OK", something, fields))
MyEventListener._build_component(self.comp_registry)
MyOtherEventListener._build_component(self.comp_registry)
something = object()
fields = ["name", "code"]
# as there is no return value by the event, we
# modify this recipient to check it has been called
recipient = []
# collect the event and notify them
collected = self.collecter.collect_events("on_record_create")
self.assertEqual(2, len(collected.events))
collected.notify(recipient, something, fields=fields)
self.assertEqual(
[("OK", something, fields), ("OK", something, fields)], recipient
)
def test_event_cache(self):
class MyEventListener(Component):
_name = "my.event.listener"
_inherit = "base.event.listener"
def on_record_create(self):
pass
MyEventListener._build_component(self.comp_registry)
# collect the event
collected = self.collecter.collect_events("on_record_create")
# CollectedEvents.events contains the collected events
self.assertEqual(1, len(collected.events))
event = list(collected.events)[0]
self.assertEqual(self.work, event.__self__.work)
self.assertEqual(self.work.env, event.__self__.work.env)
# build and register a new listener
class MyOtherEventListener(Component):
_name = "my.other.event.listener"
_inherit = "base.event.listener"
def on_record_create(self):
pass
MyOtherEventListener._build_component(self.comp_registry)
# get a new collecter and check that we it finds the same
# events even if we built a new one: it means the cache works
env = mock.MagicMock()
work = EventWorkContext(
model_name="res.users", env=env, components_registry=self.comp_registry
)
collecter = self.comp_registry["base.event.collecter"](work)
collected = collecter.collect_events("on_record_create")
# CollectedEvents.events contains the collected events
self.assertEqual(1, len(collected.events))
event = list(collected.events)[0]
self.assertEqual(work, event.__self__.work)
self.assertEqual(env, event.__self__.work.env)
# if we empty the cache, as it on the class, both collecters
# should now find the 2 events
collecter._cache.clear()
self.comp_registry._cache.clear()
# CollectedEvents.events contains the collected events
self.assertEqual(2, len(collecter.collect_events("on_record_create").events))
self.assertEqual(
2, len(self.collecter.collect_events("on_record_create").events)
)
def test_event_cache_collection(self):
class MyEventListener(Component):
_name = "my.event.listener"
_inherit = "base.event.listener"
def on_record_create(self):
pass
MyEventListener._build_component(self.comp_registry)
# collect the event
collected = self.collecter.collect_events("on_record_create")
# CollectedEvents.events contains the collected events
self.assertEqual(1, len(collected.events))
# build and register a new listener
class MyOtherEventListener(Component):
_name = "my.other.event.listener"
_inherit = "base.event.listener"
_collection = "base.collection"
def on_record_create(self):
pass
MyOtherEventListener._build_component(self.comp_registry)
# get a new collecter and check that we it finds the same
# events even if we built a new one: it means the cache works
collection = mock.MagicMock(name="base.collection")
collection._name = "base.collection"
collection.env = mock.MagicMock()
work = EventWorkContext(
model_name="res.users",
collection=collection,
components_registry=self.comp_registry,
)
collecter = self.comp_registry["base.event.collecter"](work)
collected = collecter.collect_events("on_record_create")
# for a different collection, we should not have the same
# cache entry
self.assertEqual(2, len(collected.events))
def test_event_cache_model_name(self):
class MyEventListener(Component):
_name = "my.event.listener"
_inherit = "base.event.listener"
def on_record_create(self):
pass
MyEventListener._build_component(self.comp_registry)
# collect the event
collected = self.collecter.collect_events("on_record_create")
# CollectedEvents.events contains the collected events
self.assertEqual(1, len(collected.events))
# build and register a new listener
class MyOtherEventListener(Component):
_name = "my.other.event.listener"
_inherit = "base.event.listener"
_apply_on = ["res.country"]
def on_record_create(self):
pass
MyOtherEventListener._build_component(self.comp_registry)
# get a new collecter and check that we it finds the same
# events even if we built a new one: it means the cache works
env = mock.MagicMock()
work = EventWorkContext(
model_name="res.country", env=env, components_registry=self.comp_registry
)
collecter = self.comp_registry["base.event.collecter"](work)
collected = collecter.collect_events("on_record_create")
# for a different collection, we should not have the same
# cache entry
self.assertEqual(2, len(collected.events))
def test_skip_if(self):
class MyEventListener(Component):
_name = "my.event.listener"
_inherit = "base.event.listener"
def on_record_create(self, msg):
pass
class MyOtherEventListener(Component):
_name = "my.other.event.listener"
_inherit = "base.event.listener"
@skip_if(lambda self, msg: msg == "foo")
def on_record_create(self, msg):
raise AssertionError()
self._build_components(MyEventListener, MyOtherEventListener)
# collect the event and notify it
collected = self.collecter.collect_events("on_record_create")
self.assertEqual(2, len(collected.events))
collected.notify("foo")
class TestEventFromModel(TransactionComponentRegistryCase):
"""Test Events Components from Models"""
def setUp(self):
super(TestEventFromModel, self).setUp()
self._setup_registry(self)
self._load_module_components("component_event")
def test_event_from_model(self):
class MyEventListener(Component):
_name = "my.event.listener"
_inherit = "base.event.listener"
def on_foo(self, record, name):
record.name = name
MyEventListener._build_component(self.comp_registry)
partner = self.env["res.partner"].create({"name": "test"})
# Normally you would not pass a components_registry,
# this is for the sake of the test, letting it empty
# will use the global registry.
# In a real code it would look like:
# partner._event('on_foo').notify('bar')
events = partner._event("on_foo", components_registry=self.comp_registry)
events.notify(partner, "bar")
self.assertEqual("bar", partner.name)
def test_event_filter_on_model(self):
class GlobalListener(Component):
_name = "global.event.listener"
_inherit = "base.event.listener"
def on_foo(self, record, name):
record.name = name
class PartnerListener(Component):
_name = "partner.event.listener"
_inherit = "base.event.listener"
_apply_on = ["res.partner"]
def on_foo(self, record, name):
record.ref = name
class UserListener(Component):
_name = "user.event.listener"
_inherit = "base.event.listener"
_apply_on = ["res.users"]
def on_foo(self, record, name):
raise AssertionError()
self._build_components(GlobalListener, PartnerListener, UserListener)
partner = self.env["res.partner"].create({"name": "test"})
partner._event("on_foo", components_registry=self.comp_registry).notify(
partner, "bar"
)
self.assertEqual("bar", partner.name)
self.assertEqual("bar", partner.ref)
def test_event_filter_on_collection(self):
class GlobalListener(Component):
_name = "global.event.listener"
_inherit = "base.event.listener"
def on_foo(self, record, name):
record.name = name
class PartnerListener(Component):
_name = "partner.event.listener"
_inherit = "base.event.listener"
_collection = "collection.base"
def on_foo(self, record, name):
record.ref = name
class UserListener(Component):
_name = "user.event.listener"
_inherit = "base.event.listener"
_collection = "magento.backend"
def on_foo(self, record, name):
raise AssertionError()
self._build_components(GlobalListener, PartnerListener, UserListener)
partner = self.env["res.partner"].create({"name": "test"})
events = partner._event(
"on_foo",
collection=self.env["collection.base"],
components_registry=self.comp_registry,
)
events.notify(partner, "bar")
self.assertEqual("bar", partner.name)
self.assertEqual("bar", partner.ref)

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Component_event Module - component_event
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for component_event. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,5 @@
# Dependencies
This addon depends on:
- [component](../../odoo-bringout-oca-connector-component)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon component_event or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-connector-component_event"
# or
uv pip install odoo-bringout-oca-connector-component_event"
```

View file

@ -0,0 +1,12 @@
# Models
Detected core models and extensions in component_event.
```mermaid
classDiagram
class base
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: component_event. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon component_event
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,8 @@
# Security
This module does not define custom security rules or access controls beyond Odoo defaults.
Default Odoo security applies:
- Base user access through standard groups
- Model access inherited from dependencies
- No custom row-level security rules

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon component_event
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

@ -0,0 +1,42 @@
[project]
name = "odoo-bringout-oca-connector-component_event"
version = "16.0.0"
description = "Components Events - Odoo addon"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-connector-component>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["component_event"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]