diff --git a/README.md b/README.md index 7c40e0b6b..abc535c5c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,13 @@ connector [//]: # (addons) -This part will be replaced when running the oca-gen-addons-table script from OCA/maintainer-tools. +Available addons +---------------- +addon | version | maintainers | summary +--- | --- | --- | --- +[component](component/) | 18.0.1.0.0 | [![guewen](https://github.com/guewen.png?size=30px)](https://github.com/guewen) | Add capabilities to register and use decoupled components, as an alternative to model classes +[component_event](component_event/) | 18.0.1.0.0 | | Components Events +[test_component](test_component/) | 18.0.1.0.0 | [![guewen](https://github.com/guewen.png?size=30px)](https://github.com/guewen) | Automated tests for Components, do not install. [//]: # (end addons) diff --git a/component/README.rst b/component/README.rst new file mode 100644 index 000000000..bf2b73fc8 --- /dev/null +++ b/component/README.rst @@ -0,0 +1,167 @@ +========== +Components +========== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2785951ba7cf6288c667291264099df031ca3d90d9c79c04a2d5cddec6c85641 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |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/18.0/component + :alt: OCA/connector +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/connector-18-0/connector-18-0-component + :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=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module implements a component system and is a base block for the +Connector Framework. It can be used without using the full Connector +though. + +Documentation: http://odoo-connector.com/ + +You may also want to check the `Introduction to Odoo +Components `__ +by @guewen. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +As a developer, you have access to a component system. You can find the +documentation in the code or on http://odoo-connector.com + +In a nutshell, you can create components: + +:: + + from odoo.addons.component.core import Component + + class MagentoPartnerAdapter(Component): + _name = 'magento.partner.adapter' + _inherit = 'magento.adapter' + + _usage = 'backend.adapter' + _collection = 'magento.backend' + _apply_on = ['res.partner'] + +And later, find the component you need at runtime (dynamic dispatch at +component level): + +:: + + def run(self, external_id): + backend_adapter = self.component(usage='backend.adapter') + external_data = backend_adapter.read(external_id) + +In order for tests using components to work, you will need to use the +base class provided by \`odoo.addons.component.tests.common\`: + +- TransactionComponentCase + +There are also some specific base classes for testing the component +registry, using the ComponentRegistryCase as a base class. See the +docstrings in tests/common.py. + +Changelog +========= + +16.0.1.0.0 (2022-10-04) +----------------------- + +- [MIGRATION] from 15.0 + +15.0.1.0.0 (2021-11-25) +----------------------- + +- [MIGRATION] from 14.0 + +14.0.1.0.0 (2020-10-22) +----------------------- + +- [MIGRATION] from 13.0 + +13.0.1.0.0 (2019-10-23) +----------------------- + +- [MIGRATION] from 12.0 + +12.0.1.0.0 (2018-10-02) +----------------------- + +- [MIGRATION] from 11.0 branched at rev. 324e006 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- Guewen Baconnier +- Laurent Mignon +- Simone Orsi +- Thien Vo + +Other credits +------------- + +The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp. + +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. + +.. |maintainer-guewen| image:: https://github.com/guewen.png?size=40px + :target: https://github.com/guewen + :alt: guewen + +Current `maintainer `__: + +|maintainer-guewen| + +This module is part of the `OCA/connector `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/component/__init__.py b/component/__init__.py new file mode 100644 index 000000000..70fab2841 --- /dev/null +++ b/component/__init__.py @@ -0,0 +1,5 @@ +from . import core + +from . import components +from . import builder +from . import models diff --git a/component/__manifest__.py b/component/__manifest__.py new file mode 100644 index 000000000..e180eece9 --- /dev/null +++ b/component/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2017 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +{ + "name": "Components", + "summary": "Add capabilities to register and use decoupled components," + " as an alternative to model classes", + "version": "18.0.1.0.0", + "author": "Camptocamp," "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/connector", + "license": "LGPL-3", + "category": "Generic Modules", + "depends": ["base"], + "external_dependencies": { + "python": [ + "cachetools", + ] + }, + "installable": True, + "development_status": "Production/Stable", + "maintainers": ["guewen"], +} diff --git a/component/builder.py b/component/builder.py new file mode 100644 index 000000000..913739159 --- /dev/null +++ b/component/builder.py @@ -0,0 +1,97 @@ +# Copyright 2019 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +""" + +Components Builder +================== + +Build the components at the build of a registry. + +""" + +import odoo +from odoo import models + +from .core import DEFAULT_CACHE_SIZE, ComponentRegistry, _component_databases + + +class ComponentBuilder(models.AbstractModel): + """Build the component classes + + And register them in a global registry. + + Every time an Odoo registry is built, the know components are cleared and + rebuilt as well. The Component classes are built using the same mechanism + than Odoo's Models: a final class is created, taking every Components with + a ``_name`` and applying Components with an ``_inherits`` upon them. + + The final Component classes are registered in global registry. + + This class is an Odoo model, allowing us to hook the build of the + components at the end of the Odoo's registry loading, using + ``_register_hook``. This method is called after all modules are loaded, so + we are sure that we have all the components Classes and in the correct + order. + + """ + + _name = "component.builder" + _description = "Component Builder" + + _components_registry_cache_size = DEFAULT_CACHE_SIZE + + def _register_hook(self): + # This method is called by Odoo when the registry is built, + # so in case the registry is rebuilt (cache invalidation, ...), + # we have to to rebuild the components. We use a new + # registry so we have an empty cache and we'll add components in it. + components_registry = self._init_global_registry() + self.build_registry(components_registry) + components_registry.ready = True + + def _init_global_registry(self): + components_registry = ComponentRegistry( + cachesize=self._components_registry_cache_size + ) + _component_databases[self.env.cr.dbname] = components_registry + return components_registry + + def build_registry(self, components_registry, states=None, exclude_addons=None): + if not states: + states = ("installed", "to upgrade") + # lookup all the installed (or about to be) addons and generate + # the graph, so we can load the components following the order + # of the addons' dependencies + graph = odoo.modules.graph.Graph() + graph.add_module(self.env.cr, "base") + + query = "SELECT name " "FROM ir_module_module " "WHERE state IN %s " + params = [tuple(states)] + if exclude_addons: + query += " AND name NOT IN %s " + params.append(tuple(exclude_addons)) + self.env.cr.execute(query, params) + + module_list = [name for (name,) in self.env.cr.fetchall() if name not in graph] + graph.add_modules(self.env.cr, module_list) + + for module in graph: + self.load_components(module.name, components_registry=components_registry) + + def load_components(self, module, components_registry=None): + """Build every component known by MetaComponent for an odoo module + + The final component (composed by all the Component classes in this + module) will be pushed into the registry. + + :param module: the name of the addon for which we want to load + the components + :type module: str | unicode + :param registry: the registry in which we want to put the Component + :type registry: :py:class:`~.core.ComponentRegistry` + """ + components_registry = ( + components_registry or _component_databases[self.env.cr.dbname] + ) + components_registry.load_components(module) diff --git a/component/components/__init__.py b/component/components/__init__.py new file mode 100644 index 000000000..0e4444933 --- /dev/null +++ b/component/components/__init__.py @@ -0,0 +1 @@ +from . import base diff --git a/component/components/base.py b/component/components/base.py new file mode 100644 index 000000000..0996ac03d --- /dev/null +++ b/component/components/base.py @@ -0,0 +1,15 @@ +# Copyright 2017 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from ..core import AbstractComponent + + +class BaseComponent(AbstractComponent): + """This is the base component for every component + + It is implicitely inherited by all components. + + All your base are belong to us + """ + + _name = "base" diff --git a/component/core.py b/component/core.py new file mode 100644 index 000000000..695189b2f --- /dev/null +++ b/component/core.py @@ -0,0 +1,939 @@ +# Copyright 2017 Camptocamp SA +# Copyright 2017 Odoo +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +""" + +Core +==== + +Core classes for the components. +The most common classes used publicly are: + +* :class:`Component` +* :class:`AbstractComponent` +* :class:`WorkContext` + +""" + +import logging +import operator +from collections import OrderedDict, defaultdict + +from odoo import models +from odoo.tools.misc import LastOrderedSet, OrderedSet + +from .exception import NoComponentError, RegistryNotReadyError, SeveralComponentError + +_logger = logging.getLogger(__name__) + +try: + from cachetools import LRUCache, cachedmethod +except ImportError: + _logger.debug("Cannot import 'cachetools'.") + + +# The Cache size represents the number of items, so the number +# of components (include abstract components) we will keep in the LRU +# cache. We would need stats to know what is the average but this is a bit +# early. +DEFAULT_CACHE_SIZE = 512 + + +# this is duplicated from odoo.models.MetaModel._get_addon_name() which we +# unfortunately can't use because it's an instance method and should have been +# a @staticmethod +def _get_addon_name(full_name): + # The (Odoo) module name can be in the ``odoo.addons`` namespace + # or not. For instance, module ``sale`` can be imported as + # ``odoo.addons.sale`` (the right way) or ``sale`` (for backward + # compatibility). + module_parts = full_name.split(".") + if len(module_parts) > 2 and module_parts[:2] == ["odoo", "addons"]: + addon_name = full_name.split(".")[2] + else: + addon_name = full_name.split(".")[0] + return addon_name + + +class ComponentDatabases(dict): + """Holds a registry of components for each database""" + + +class ComponentRegistry: + """Store all the components and allow to find them using criteria + + The key is the ``_name`` of the components. + + This is an OrderedDict, because we want to keep the registration order of + the components, addons loaded first have their components found first. + + The :attr:`ready` attribute must be set to ``True`` when all the components + are loaded. + + """ + + def __init__(self, cachesize=DEFAULT_CACHE_SIZE): + self._cache = LRUCache(maxsize=cachesize) + self._components = OrderedDict() + self._loaded_modules = set() + self.ready = False + + def __getitem__(self, key): + return self._components[key] + + def __setitem__(self, key, value): + self._components[key] = value + + def __contains__(self, key): + return key in self._components + + def get(self, key, default=None): + return self._components.get(key, default) + + def __iter__(self): + return iter(self._components) + + def load_components(self, module): + if module in self._loaded_modules: + return + for component_class in MetaComponent._modules_components[module]: + component_class._build_component(self) + self._loaded_modules.add(module) + + @cachedmethod(operator.attrgetter("_cache")) + def lookup(self, collection_name=None, usage=None, model_name=None): + """Find and return a list of components for a usage + + If a component is not registered in a particular collection (no + ``_collection``), it will be returned in any case (as far as + the ``usage`` and ``model_name`` match). This is useful to share + generic components across different collections. + + If no collection name is given, components from any collection + will be returned. + + Then, the components of a collection are filtered by usage and/or + model. The ``_usage`` is mandatory on the components. When the + ``_model_name`` is empty, it means it can be used for every models, + and it will ignore the ``model_name`` argument. + + The abstract components are never returned. + + This is a rather low-level function, usually you will use the + high-level :meth:`AbstractComponent.component`, + :meth:`AbstractComponent.many_components` or even + :meth:`AbstractComponent.component_by_name`. + + :param collection_name: the name of the collection the component is + registered into. + :param usage: the usage of component we are looking for + :param model_name: filter on components that apply on this model + + """ + + # keep the order so addons loaded first have components used first + candidates = ( + component + for component in self._components.values() + if not component._abstract + ) + + if collection_name is not None: + candidates = ( + component + for component in candidates + if ( + component._collection == collection_name + or component._collection is None + ) + ) + + if usage is not None: + candidates = ( + component for component in candidates if component._usage == usage + ) + + if model_name is not None: + candidates = ( + c + for c in candidates + if c.apply_on_models is None or model_name in c.apply_on_models + ) + + return list(candidates) + + +# We will store a ComponentRegistry per database here, +# it will be cleared and updated when the odoo's registry is rebuilt +_component_databases = ComponentDatabases() + + +class WorkContext: + """Transport the context required to work with components + + It is propagated through all the components, so any + data or instance (like a random RPC client) that need + to be propagated transversally to the components + should be kept here. + + Including: + + .. attribute:: model_name + + Name of the model we are working with. It means that any lookup for a + component will be done for this model. It also provides a shortcut + as a `model` attribute to use directly with the Odoo model from + the components + + .. attribute:: collection + + The collection we are working with. The collection is an Odoo + Model that inherit from 'collection.base'. The collection attribute + can be a record or an "empty" model. + + .. attribute:: model + + Odoo Model for ``model_name`` with the same Odoo + :class:`~odoo.api.Environment` than the ``collection`` attribute. + + This is also the entrypoint to work with the components. + + :: + + collection = self.env['my.collection'].browse(1) + work = WorkContext(model_name='res.partner', collection=collection) + component = work.component(usage='record.importer') + + Usually you will use the context manager on the ``collection.base`` Model: + + :: + + collection = self.env['my.collection'].browse(1) + with collection.work_on('res.partner') as work: + component = work.component(usage='record.importer') + + It supports any arbitrary keyword arguments that will become attributes of + the instance, and be propagated throughout all the components. + + :: + + collection = self.env['my.collection'].browse(1) + with collection.work_on('res.partner', hello='world') as work: + assert work.hello == 'world' + + When you need to work on a different model, a new work instance will be + created for you when you are using the high-level API. This is what + happens under the hood: + + :: + + collection = self.env['my.collection'].browse(1) + with collection.work_on('res.partner', hello='world') as work: + assert work.model_name == 'res.partner' + assert work.hello == 'world' + work2 = work.work_on('res.users') + # => spawn a new WorkContext with a copy of the attributes + assert work2.model_name == 'res.users' + assert work2.hello == 'world' + + """ + + def __init__( + self, model_name=None, collection=None, components_registry=None, **kwargs + ): + self.collection = collection + self.model_name = model_name + self.model = self.env[model_name] + # Allow propagation of custom component registry via context + if collection: + custom_registry = collection.env.context.get("components_registry") + if custom_registry: + components_registry = custom_registry + # lookup components in an alternative registry, used by the tests + if components_registry is not None: + self.components_registry = components_registry + else: + dbname = self.env.cr.dbname + try: + self.components_registry = _component_databases[dbname] + except KeyError as exc: + msg = ( + "No component registry for database %s. " + "Probably because the Odoo registry has not been built " + "yet." + ) + _logger.error( + msg, + dbname, + ) + raise RegistryNotReadyError(msg) from exc + self._propagate_kwargs = ["collection", "model_name", "components_registry"] + for attr_name, value in kwargs.items(): + setattr(self, attr_name, value) + self._propagate_kwargs.append(attr_name) + + @property + def env(self): + """Return the current Odoo env + + This is the environment of the current collection. + """ + return self.collection.env + + 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. + """ + kwargs = { + attr_name: getattr(self, attr_name) for attr_name in self._propagate_kwargs + } + if collection is not None: + kwargs["collection"] = collection + if model_name is not None: + kwargs["model_name"] = model_name + return self.__class__(**kwargs) + + def _component_class_by_name(self, name): + components_registry = self.components_registry + component_class = components_registry.get(name) + if not component_class: + raise NoComponentError(f"No component with name '{name}' found.") + return component_class + + def component_by_name(self, name, model_name=None): + """Return a component by its name + + If the component exists, an instance of it will be returned, + initialized with the current :class:`WorkContext`. + + A :exc:`odoo.addons.component.exception.NoComponentError` is raised + if: + + * no component with this name exists + * the ``_apply_on`` of the found component does not match + with the current working model + + In the latter case, it can be an indication that you need to switch to + a different model, you can do so by providing the ``model_name`` + argument. + + """ + if isinstance(model_name, models.BaseModel): + model_name = model_name._name + component_class = self._component_class_by_name(name) + work_model = model_name or self.model_name + if ( + component_class._collection + and self.collection._name != component_class._collection + ): + raise NoComponentError( + f"""Component with name '{name}' can't be used for collection + '{self.collection._name}'.""" + ) + + if ( + component_class.apply_on_models + and work_model not in component_class.apply_on_models + ): + if len(component_class.apply_on_models) == 1: + hint_models = f"'{component_class.apply_on_models[0]}'" + else: + hint_models = f"" + raise NoComponentError( + f"Component with name '{name}' can't be used for model '{work_model}'." + f"\nHint: you might want to use: " + f"component_by_name('{name}', model_name={hint_models})" + ) + + if work_model == self.model_name: + work_context = self + else: + work_context = self.work_on(model_name) + return component_class(work_context) + + def _lookup_components(self, usage=None, model_name=None, **kw): + component_classes = self.components_registry.lookup( + self.collection._name, usage=usage, model_name=model_name + ) + matching_components = [] + for cls in component_classes: + try: + matching = cls._component_match( + self, usage=usage, model_name=model_name, **kw + ) + except TypeError as err: + # Backward compat + _logger.info(str(err)) + _logger.info( + "The signature of %s._component_match has changed. " + "Please, adapt your code as " + "(self, usage=usage, model_name=model_name, **kw)", + cls.__name__, + ) + matching = cls._component_match(self) + if matching: + matching_components.append(cls) + return matching_components + + def _filter_components_by_collection(self, component_classes): + return [c for c in component_classes if c._collection == self.collection._name] + + def _filter_components_by_model(self, component_classes, model_name): + return [ + c + for c in component_classes + if c.apply_on_models and model_name in c.apply_on_models + ] + + def _ensure_model_name(self, model_name): + """Make sure model name is a string or fallback to current ctx value.""" + if isinstance(model_name, models.BaseModel): + model_name = model_name._name + return model_name or self.model_name + + def _matching_components(self, usage=None, model_name=None, **kw): + """Retrieve matching components and their work context.""" + component_classes = self._lookup_components( + usage=usage, model_name=model_name, **kw + ) + if model_name == self.model_name: + work_context = self + else: + work_context = self.work_on(model_name) + return component_classes, work_context + + def component(self, usage=None, model_name=None, **kw): + """Find a component by usage and model for the current collection + + It searches a component using the rules of + :meth:`ComponentRegistry.lookup`. When a component is found, + it initialize it with the current :class:`WorkContext` and returned. + + A component with a ``_apply_on`` matching the asked ``model_name`` + takes precedence over a generic component without ``_apply_on``. + A component with a ``_collection`` matching the current collection + takes precedence over a generic component without ``_collection``. + This behavior allows to define generic components across collections + and/or models and override them only for a particular collection and/or + model. + + A :exc:`odoo.addons.component.exception.SeveralComponentError` is + raised if more than one component match for the provided + ``usage``/``model_name``. + + A :exc:`odoo.addons.component.exception.NoComponentError` is raised + if no component is found for the provided ``usage``/``model_name``. + + """ + model_name = self._ensure_model_name(model_name) + component_classes, work_context = self._matching_components( + usage=usage, model_name=model_name, **kw + ) + if not component_classes: + raise NoComponentError( + f"No component found for collection '{self.collection._name}', " + f"usage '{usage}', model_name '{model_name}'." + ) + elif len(component_classes) > 1: + # If we have more than one component, try to find the one + # specifically linked to the collection... + component_classes = self._filter_components_by_collection(component_classes) + if len(component_classes) > 1: + # ... or try to find the one specifically linked to the model + component_classes = self._filter_components_by_model( + component_classes, model_name + ) + if len(component_classes) != 1: + raise SeveralComponentError( + "Several components found for collection '{}', " + "usage '{}', model_name '{}'. Found: {}".format( + self.collection._name, + usage or "", + model_name or "", + component_classes, + ) + ) + return component_classes[0](work_context) + + def many_components(self, usage=None, model_name=None, **kw): + """Find many components by usage and model for the current collection + + It searches a component using the rules of + :meth:`ComponentRegistry.lookup`. When components are found, they + initialized with the current :class:`WorkContext` and returned as a + list. + + If no component is found, an empty list is returned. + + """ + model_name = self._ensure_model_name(model_name) + component_classes, work_context = self._matching_components( + usage=usage, model_name=model_name, **kw + ) + return [comp(work_context) for comp in component_classes] + + def __str__(self): + return f"WorkContext({self.model_name}, {repr(self.collection)})" + + __repr__ = __str__ + + +class MetaComponent(type): + """Metaclass for Components + + Every new :class:`Component` will be added to ``_modules_components``, + that will be used by the component builder. + + """ + + _modules_components = defaultdict(list) + + def __init__(cls, name, bases, attrs): + if not cls._register: + cls._register = True + super().__init__(name, bases, attrs) + return + + # If components are declared in tests, exclude them from the + # "components of the addon" list. If not, when we use the + # "load_components" method, all the test components would be loaded. + # This should never be an issue when running the app normally, as the + # Python tests should never be executed. But this is an issue when a + # test creates a test components for the purpose of the test, then a + # second tests uses the "load_components" to load all the addons of the + # module: it will load the component of the previous test. + if "tests" in cls.__module__.split("."): + return + + if not hasattr(cls, "_module"): + cls._module = _get_addon_name(cls.__module__) + + cls._modules_components[cls._module].append(cls) + + @property + def apply_on_models(cls): + # None means all models + if cls._apply_on is None: + return None + # always return a list, used for the lookup + elif isinstance(cls._apply_on, str): + return [cls._apply_on] + return cls._apply_on + + +class AbstractComponent(metaclass=MetaComponent): + """Main Component Model + + All components have a Python inheritance either on + :class:`AbstractComponent` or either on :class:`Component`. + + Abstract Components will not be returned by lookups on components, however + they can be used as a base for other Components through inheritance (using + ``_inherit``). + + Inheritance mechanism + The inheritance mechanism is like the Odoo's one for Models. Each + component has a ``_name``. This is the absolute minimum in a Component + class. + + :: + + class MyComponent(Component): + _name = 'my.component' + + def speak(self, message): + print message + + Every component implicitly inherit from the `'base'` component. + + There are two close but distinct inheritance types, which look + familiar if you already know Odoo. The first uses ``_inherit`` with + an existing name, the name of the component we want to extend. With + the following example, ``my.component`` is now able to speak and to + yell. + + :: + + class MyComponent(Component): # name of the class does not matter + _inherit = 'my.component' + + def yell(self, message): + print message.upper() + + The second has a different ``_name``, it creates a new component, + including the behavior of the inherited component, but without + modifying it. In the following example, ``my.component`` is still able + to speak and to yell (brough by the previous inherit), but not to + sing. ``another.component`` is able to speak, to yell and to sing. + + :: + + class AnotherComponent(Component): + _name = 'another.component' + _inherit = 'my.component' + + def sing(self, message): + print message.upper() + + Registration and lookups + It is handled by 3 attributes on the class: + + _collection + The name of the collection where we want to register the + component. This is not strictly mandatory as a component can be + shared across several collections. But usually, you want to set a + collection to segregate the components for a domain. A collection + can be for instance ``magento.backend``. It is also the name of a + model that inherits from ``collection.base``. See also + :class:`~WorkContext` and + :class:`~odoo.addons.component.models.collection.Collection`. + + _apply_on + List of names or name of the Odoo model(s) for which the component + can be used. When not set, the component can be used on any model. + + _usage + The collection and the model (``_apply_on``) will help to filter + the candidate components according to our working context (e.g. I'm + working on ``magento.backend`` with the model + ``magento.res.partner``). The usage will define **what** kind of + task the component we are looking for serves to. For instance, it + might be ``record.importer``, ``export.mapper```... but you can be + as creative as you want. + + Now, to get a component, you'll likely use + :meth:`WorkContext.component` when you start to work with components + in your flow, but then from within your components, you are more + likely to use one of: + + * :meth:`component` + * :meth:`many_components` + * :meth:`component_by_name` (more rarely though) + + Declaration of some Components can look like:: + + class FooBar(models.Model): + _name = 'foo.bar.collection' + _inherit = 'collection.base' # this inherit is required + + + class FooBarBase(AbstractComponent): + _name = 'foo.bar.base' + _collection = 'foo.bar.collection' # name of the model above + + + class Foo(Component): + _name = 'foo' + _inherit = 'foo.bar.base' # we will inherit the _collection + _apply_on = 'res.users' + _usage = 'speak' + + def utter(self, message): + print message + + + class Bar(Component): + _name = 'bar' + _inherit = 'foo.bar.base' # we will inherit the _collection + _apply_on = 'res.users' + _usage = 'yell' + + def utter(self, message): + print message.upper() + '!!!' + + + class Vocalizer(Component): + _name = 'vocalizer' + _inherit = 'foo.bar.base' + _usage = 'vocalizer' + # can be used for any model + + def vocalize(action, message): + self.component(usage=action).utter(message) + + + And their usage:: + + >>> coll = self.env['foo.bar.collection'].browse(1) + >>> with coll.work_on('res.users') as work: + ... vocalizer = work.component(usage='vocalizer') + ... vocalizer.vocalize('speak', 'hello world') + ... + hello world + ... vocalizer.vocalize('yell', 'hello world') + HELLO WORLD!!! + + Hints: + + * If you want to create components without ``_apply_on``, choose a + ``_usage`` that will not conflict other existing components. + * Unless this is what you want and in that case you use + :meth:`many_components` which will return all components for a usage + with a matching or a not set ``_apply_on``. + * It is advised to namespace the names of the components (e.g. + ``magento.xxx``) to prevent conflicts between addons. + + """ + + _register = False + _abstract = True + + # used for inheritance + _name = None #: Name of the component + + #: Name or list of names of the component(s) to inherit from + _inherit = None + + #: name of the collection to subscribe in + _collection = None + + #: List of models on which the component can be applied. + #: None means any Model, can be a list ['res.users', ...] + _apply_on = None + + #: Component purpose ('import.mapper', ...). + _usage = None + + def __init__(self, work_context): + super().__init__() + self.work = work_context + + @classmethod + def _component_match(cls, work, usage=None, model_name=None, **kw): + """Evaluated on candidate components + + When a component lookup is done and candidate(s) have + been found for a usage, a final call is done on this method. + If the method return False, the candidate component is ignored. + + It can be used for instance to dynamically choose a component + according to a value in the :class:`WorkContext`. + + Beware, if the lookups from usage, model and collection are + cached, the calls to :meth:`_component_match` are executed + each time we get components. Heavy computation should be + avoided. + + :param work: the :class:`WorkContext` we are working with + + """ + return True + + @property + def collection(self): + """Collection we are working with""" + return self.work.collection + + @property + def env(self): + """Current Odoo environment, the one of the collection record""" + return self.work.env + + @property + def model(self): + """The model instance we are working with""" + return self.work.model + + def component_by_name(self, name, model_name=None): + """Return a component by its name + + Shortcut to meth:`~WorkContext.component_by_name` + """ + return self.work.component_by_name(name, model_name=model_name) + + def component(self, usage=None, model_name=None, **kw): + """Return a component + + Shortcut to meth:`~WorkContext.component` + """ + return self.work.component(usage=usage, model_name=model_name, **kw) + + def many_components(self, usage=None, model_name=None, **kw): + """Return several components + + Shortcut to meth:`~WorkContext.many_components` + """ + return self.work.many_components(usage=usage, model_name=model_name, **kw) + + def __str__(self): + return f"Component({self._name})" + + __repr__ = __str__ + + @classmethod + def _build_component(cls, registry): + """Instantiate a given Component in the components registry. + + This method is called at the end of the Odoo's registry build. The + caller is :meth:`component.builder.ComponentBuilder.load_components`. + + It generates new classes, which will be the Component classes we will + be using. The new classes are generated following the inheritance + of ``_inherit``. It ensures that the ``__bases__`` of the generated + Component classes follow the ``_inherit`` chain. + + Once a Component class is created, it adds it in the Component Registry + (:class:`ComponentRegistry`), so it will be available for + lookups. + + At the end of new class creation, a hook method + :meth:`_complete_component_build` is called, so you can customize + further the created components. An example can be found in + :meth:`odoo.addons.connector.components.mapper.Mapper._complete_component_build` + + The following code is roughly the same than the Odoo's one for + building Models. + + """ + + # In the simplest case, the component's registry class inherits from + # cls and the other classes that define the component in a flat + # hierarchy. The registry contains the instance ``component`` (on the + # left). Its class, ``ComponentClass``, carries inferred metadata that + # is shared between all the component's instances for this registry + # only. + # + # class A1(Component): Component + # _name = 'a' / | \ + # A3 A2 A1 + # class A2(Component): \ | / + # _inherit = 'a' ComponentClass + # + # class A3(Component): + # _inherit = 'a' + # + # When a component is extended by '_inherit', its base classes are + # modified to include the current class and the other inherited + # component classes. + # Note that we actually inherit from other ``ComponentClass``, so that + # extensions to an inherited component are immediately visible in the + # current component class, like in the following example: + # + # class A1(Component): + # _name = 'a' Component + # / / \ \ + # class B1(Component): / A2 A1 \ + # _name = 'b' / \ / \ + # B2 ComponentA B1 + # class B2(Component): \ | / + # _name = 'b' \ | / + # _inherit = ['b', 'a'] \ | / + # ComponentB + # class A2(Component): + # _inherit = 'a' + + # determine inherited components + parents = cls._inherit + if isinstance(parents, str): + parents = [parents] + elif parents is None: + parents = [] + + if cls._name in registry and not parents: + raise TypeError( + f"Component {cls._name} (in class {cls}) already exists. " + "Consider using _inherit instead of _name " + "or using a different _name." + ) + + # determine the component's name + name = cls._name or (len(parents) == 1 and parents[0]) + + if not name: + raise TypeError(f"Component {cls} must have a _name") + + # all components except 'base' implicitly inherit from 'base' + if name != "base": + parents = list(parents) + ["base"] + + # create or retrieve the component's class + if name in parents: + if name not in registry: + raise TypeError(f"Component {name} does not exist in registry.") + ComponentClass = registry[name] + ComponentClass._build_component_check_base(cls) + check_parent = ComponentClass._build_component_check_parent + else: + ComponentClass = type( + name, + (AbstractComponent,), + { + "_name": name, + "_register": False, + # names of children component + "_inherit_children": OrderedSet(), + }, + ) + check_parent = cls._build_component_check_parent + + # determine all the classes the component should inherit from + bases = LastOrderedSet([cls]) + for parent in parents: + if parent not in registry: + raise TypeError( + f"Component {name} inherits from non-existing component {parent}." + ) + parent_class = registry[parent] + if parent == name: + for base in parent_class.__bases__: + bases.add(base) + else: + check_parent(cls, parent_class) + bases.add(parent_class) + parent_class._inherit_children.add(name) + ComponentClass.__bases__ = tuple(bases) + + ComponentClass._complete_component_build() + + registry[name] = ComponentClass + + return ComponentClass + + @classmethod + def _build_component_check_base(cls, extend_cls): + """Check whether ``cls`` can be extended with ``extend_cls``.""" + if cls._abstract and not extend_cls._abstract: + msg = ( + "%s transforms the abstract component %r into a " + "non-abstract component. " + "That class should either inherit from AbstractComponent, " + "or set a different '_name'." + ) + raise TypeError(msg % (extend_cls, cls._name)) + + @classmethod + def _build_component_check_parent(component_class, cls, parent_class): # noqa: B902 + """Check whether ``model_class`` can inherit from ``parent_class``.""" + if component_class._abstract and not parent_class._abstract: + msg = ( + "In %s, the abstract Component %r cannot inherit " + "from the non-abstract Component %r." + ) + raise TypeError(msg % (cls, component_class._name, parent_class._name)) + + @classmethod + def _complete_component_build(cls): + """Complete build of the new component class + + After the component has been built from its bases, this method is + called, and can be used to customize the class before it can be used. + + Nothing is done in the base Component, but a Component can inherit + the method to add its own behavior. + """ + + +class Component(AbstractComponent): + """Concrete Component class + + This is the class you inherit from when you want your component to + be registered in the component collections. + + Look in :class:`AbstractComponent` for more details. + + """ + + _register = False + _abstract = False diff --git a/component/exception.py b/component/exception.py new file mode 100644 index 000000000..cb4c67853 --- /dev/null +++ b/component/exception.py @@ -0,0 +1,18 @@ +# Copyright 2017 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + + +class ComponentException(Exception): + """Base Exception for the components""" + + +class NoComponentError(ComponentException): + """No component has been found""" + + +class SeveralComponentError(ComponentException): + """More than one component have been found""" + + +class RegistryNotReadyError(ComponentException): + """Component registry not ready yet for given DB.""" diff --git a/component/i18n/am.po b/component/i18n/am.po new file mode 100644 index 000000000..a4252cda4 --- /dev/null +++ b/component/i18n/am.po @@ -0,0 +1,32 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Amharic (https://www.transifex.com/oca/teams/23907/am/)\n" +"Language: am\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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#~ msgid "ID" +#~ msgstr "ID" diff --git a/component/i18n/ca.po b/component/i18n/ca.po new file mode 100644 index 000000000..e0912afb8 --- /dev/null +++ b/component/i18n/ca.po @@ -0,0 +1,32 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Catalan (https://www.transifex.com/oca/teams/23907/ca/)\n" +"Language: ca\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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#~ msgid "ID" +#~ msgstr "ID" diff --git a/component/i18n/component.pot b/component/i18n/component.pot new file mode 100644 index 000000000..10c3b41cc --- /dev/null +++ b/component/i18n/component.pot @@ -0,0 +1,24 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" diff --git a/component/i18n/de.po b/component/i18n/de.po new file mode 100644 index 000000000..90e817b58 --- /dev/null +++ b/component/i18n/de.po @@ -0,0 +1,38 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: German (https://www.transifex.com/oca/teams/23907/de/)\n" +"Language: de\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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#~ msgid "Display Name" +#~ msgstr "Anzeigebezeichnung" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "Zuletzt aktualisiert am" diff --git a/component/i18n/el_GR.po b/component/i18n/el_GR.po new file mode 100644 index 000000000..34877acbb --- /dev/null +++ b/component/i18n/el_GR.po @@ -0,0 +1,33 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Greek (Greece) (https://www.transifex.com/oca/teams/23907/" +"el_GR/)\n" +"Language: el_GR\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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#~ msgid "ID" +#~ msgstr "Κωδικός" diff --git a/component/i18n/es.po b/component/i18n/es.po new file mode 100644 index 000000000..a95ceef75 --- /dev/null +++ b/component/i18n/es.po @@ -0,0 +1,39 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2023-08-02 13:09+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "Colección abstracta de base" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "Constructor de componentes" + +#~ msgid "Display Name" +#~ msgstr "Nombre mostrado" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "Última modificación el" diff --git a/component/i18n/es_ES.po b/component/i18n/es_ES.po new file mode 100644 index 000000000..9b339546e --- /dev/null +++ b/component/i18n/es_ES.po @@ -0,0 +1,33 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Spanish (Spain) (https://www.transifex.com/oca/teams/23907/" +"es_ES/)\n" +"Language: es_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" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#~ msgid "ID" +#~ msgstr "ID" diff --git a/component/i18n/fi.po b/component/i18n/fi.po new file mode 100644 index 000000000..ed847137f --- /dev/null +++ b/component/i18n/fi.po @@ -0,0 +1,38 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Finnish (https://www.transifex.com/oca/teams/23907/fi/)\n" +"Language: fi\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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#~ msgid "Display Name" +#~ msgstr "Nimi" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "Viimeksi muokattu" diff --git a/component/i18n/fr.po b/component/i18n/fr.po new file mode 100644 index 000000000..0ee0ec5a3 --- /dev/null +++ b/component/i18n/fr.po @@ -0,0 +1,40 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +# Nicolas JEUDY , 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-06-28 07:13+0000\n" +"Last-Translator: Guewen Baconnier \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" +"X-Generator: Weblate 3.0.1\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "Abstract Model inital pour une collection" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "Constructeur de composants" + +#~ msgid "Display Name" +#~ msgstr "Nom affiché" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "Dernière modification le" diff --git a/component/i18n/gl.po b/component/i18n/gl.po new file mode 100644 index 000000000..522dc6234 --- /dev/null +++ b/component/i18n/gl.po @@ -0,0 +1,32 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Galician (https://www.transifex.com/oca/teams/23907/gl/)\n" +"Language: gl\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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#~ msgid "ID" +#~ msgstr "ID" diff --git a/component/i18n/it.po b/component/i18n/it.po new file mode 100644 index 000000000..569854e12 --- /dev/null +++ b/component/i18n/it.po @@ -0,0 +1,39 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2024-02-26 09:41+0000\n" +"Last-Translator: mymage \n" +"Language-Team: Italian (https://www.transifex.com/oca/teams/23907/it/)\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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "Raccolta astratta base" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "Costruttore componente" + +#~ msgid "Display Name" +#~ msgstr "Nome da visualizzare" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" diff --git a/component/i18n/pt.po b/component/i18n/pt.po new file mode 100644 index 000000000..b56a934da --- /dev/null +++ b/component/i18n/pt.po @@ -0,0 +1,32 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Portuguese (https://www.transifex.com/oca/teams/23907/pt/)\n" +"Language: pt\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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#~ msgid "ID" +#~ msgstr "ID" diff --git a/component/i18n/pt_BR.po b/component/i18n/pt_BR.po new file mode 100644 index 000000000..69926da9f --- /dev/null +++ b/component/i18n/pt_BR.po @@ -0,0 +1,40 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2020-08-12 20:00+0000\n" +"Last-Translator: Rodrigo Macedo \n" +"Language-Team: Portuguese (Brazil) (https://www.transifex.com/oca/" +"teams/23907/pt_BR/)\n" +"Language: pt_BR\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 3.10\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "Coleção Base Abstrata" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "Construtor de Componentes" + +#~ msgid "Display Name" +#~ msgstr "Exibir Nome" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "Última modificação em" diff --git a/component/i18n/pt_PT.po b/component/i18n/pt_PT.po new file mode 100644 index 000000000..a63f8c9f2 --- /dev/null +++ b/component/i18n/pt_PT.po @@ -0,0 +1,33 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Portuguese (Portugal) (https://www.transifex.com/oca/" +"teams/23907/pt_PT/)\n" +"Language: pt_PT\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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#~ msgid "ID" +#~ msgstr "ID" diff --git a/component/i18n/sl.po b/component/i18n/sl.po new file mode 100644 index 000000000..cb3d8a00d --- /dev/null +++ b/component/i18n/sl.po @@ -0,0 +1,39 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Slovenian (https://www.transifex.com/oca/teams/23907/sl/)\n" +"Language: sl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || " +"n%100==4 ? 2 : 3);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#~ msgid "Display Name" +#~ msgstr "Prikazni naziv" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "Zadnjič spremenjeno" diff --git a/component/i18n/tr.po b/component/i18n/tr.po new file mode 100644 index 000000000..90c594d4c --- /dev/null +++ b/component/i18n/tr.po @@ -0,0 +1,32 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Turkish (https://www.transifex.com/oca/teams/23907/tr/)\n" +"Language: tr\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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#~ msgid "ID" +#~ msgstr "ID" diff --git a/component/i18n/zh_CN.po b/component/i18n/zh_CN.po new file mode 100644 index 000000000..47a55bd74 --- /dev/null +++ b/component/i18n/zh_CN.po @@ -0,0 +1,36 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +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 +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "基础抽象集合" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "组件构建器" + +#~ msgid "Display Name" +#~ msgstr "显示名称" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "最后修改时间" diff --git a/component/models/__init__.py b/component/models/__init__.py new file mode 100644 index 000000000..97ad61232 --- /dev/null +++ b/component/models/__init__.py @@ -0,0 +1 @@ +from . import collection diff --git a/component/models/collection.py b/component/models/collection.py new file mode 100644 index 000000000..a1e93a09d --- /dev/null +++ b/component/models/collection.py @@ -0,0 +1,96 @@ +# Copyright 2017 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +""" + +Collection Model +================ + +This is the base Model shared by all the Collections. +In the context of the Connector, a collection is the Backend. +The `_name` given to the Collection Model will be the name +to use in the `_collection` of the Components usable for the Backend. + +""" + +from contextlib import contextmanager + +from odoo import models + +from ..core import WorkContext + + +class Collection(models.AbstractModel): + """The model on which components are subscribed + + It would be for instance the ``backend`` for the connectors. + + Example:: + + class MagentoBackend(models.Model): + _name = 'magento.backend' # name of the collection + _inherit = 'collection.base' + + + class MagentoSaleImporter(Component): + _name = 'magento.sale.importer' + _apply_on = 'magento.sale.order' + _collection = 'magento.backend' # name of the collection + + def run(self, magento_id): + mapper = self.component(usage='import.mapper') + extra_mappers = self.many_components( + usage='import.mapper.extra', + ) + # ... + + Use it:: + + >>> backend = self.env['magento.backend'].browse(1) + >>> with backend.work_on('magento.sale.order') as work: + ... importer = work.component(usage='magento.sale.importer') + ... importer.run(1) + + See also: :class:`odoo.addons.component.core.WorkContext` + + + """ + + _name = "collection.base" + _description = "Base Abstract Collection" + + @contextmanager + def work_on(self, model_name, **kwargs): + """Entry-point for the components, context manager + + Start a work using the components on the model. + Any keyword argument will be assigned to the work context. + See documentation of :class:`odoo.addons.component.core.WorkContext`. + + It is a context manager, so you can attach objects and clean them + at the end of the work session, such as:: + + @contextmanager + def work_on(self, model_name, **kwargs): + self.ensure_one() + magento_location = MagentoLocation( + self.location, + self.username, + self.password, + ) + # We create a Magento Client API here, so we can create the + # client once (lazily on the first use) and propagate it + # through all the sync session, instead of recreating a client + # in each backend adapter usage. + with MagentoAPI(magento_location) as magento_api: + _super = super(MagentoBackend, self) + # from the components we'll be able to do: + # self.work.magento_api + with _super.work_on( + model_name, magento_api=magento_api, **kwargs + ) as work: + yield work + + """ + self.ensure_one() + yield WorkContext(model_name=model_name, collection=self, **kwargs) diff --git a/component/pyproject.toml b/component/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/component/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/component/readme/CONTRIBUTORS.md b/component/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..41ad809a8 --- /dev/null +++ b/component/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- Guewen Baconnier \<\> +- Laurent Mignon \<\> +- Simone Orsi \<\> +- Thien Vo \<\> diff --git a/component/readme/CREDITS.md b/component/readme/CREDITS.md new file mode 100644 index 000000000..83b3ec91f --- /dev/null +++ b/component/readme/CREDITS.md @@ -0,0 +1 @@ +The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp. diff --git a/component/readme/DESCRIPTION.md b/component/readme/DESCRIPTION.md new file mode 100644 index 000000000..8ac7a8552 --- /dev/null +++ b/component/readme/DESCRIPTION.md @@ -0,0 +1,9 @@ +This module implements a component system and is a base block for the +Connector Framework. It can be used without using the full Connector +though. + +Documentation: + +You may also want to check the [Introduction to Odoo +Components](https://dev.to/guewen/introduction-to-odoo-components-bn0) +by @guewen. diff --git a/component/readme/HISTORY.md b/component/readme/HISTORY.md new file mode 100644 index 000000000..e98452fda --- /dev/null +++ b/component/readme/HISTORY.md @@ -0,0 +1,19 @@ +## 16.0.1.0.0 (2022-10-04) + +- \[MIGRATION\] from 15.0 + +## 15.0.1.0.0 (2021-11-25) + +- \[MIGRATION\] from 14.0 + +## 14.0.1.0.0 (2020-10-22) + +- \[MIGRATION\] from 13.0 + +## 13.0.1.0.0 (2019-10-23) + +- \[MIGRATION\] from 12.0 + +## 12.0.1.0.0 (2018-10-02) + +- \[MIGRATION\] from 11.0 branched at rev. 324e006 diff --git a/component/readme/USAGE.md b/component/readme/USAGE.md new file mode 100644 index 000000000..92816e93e --- /dev/null +++ b/component/readme/USAGE.md @@ -0,0 +1,30 @@ +As a developer, you have access to a component system. You can find the +documentation in the code or on + +In a nutshell, you can create components: + + from odoo.addons.component.core import Component + + class MagentoPartnerAdapter(Component): + _name = 'magento.partner.adapter' + _inherit = 'magento.adapter' + + _usage = 'backend.adapter' + _collection = 'magento.backend' + _apply_on = ['res.partner'] + +And later, find the component you need at runtime (dynamic dispatch at +component level): + + def run(self, external_id): + backend_adapter = self.component(usage='backend.adapter') + external_data = backend_adapter.read(external_id) + +In order for tests using components to work, you will need to use the +base class provided by \`odoo.addons.component.tests.common\`: + +- TransactionComponentCase + +There are also some specific base classes for testing the component +registry, using the ComponentRegistryCase as a base class. See the +docstrings in tests/common.py. diff --git a/component/static/description/icon.png b/component/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/component/static/description/icon.png differ diff --git a/component/static/description/index.html b/component/static/description/index.html new file mode 100644 index 000000000..c5cfa327b --- /dev/null +++ b/component/static/description/index.html @@ -0,0 +1,514 @@ + + + + + +Components + + + +
+

Components

+ + +

Production/Stable License: LGPL-3 OCA/connector Translate me on Weblate Try me on Runboat

+

This module implements a component system and is a base block for the +Connector Framework. It can be used without using the full Connector +though.

+

Documentation: http://odoo-connector.com/

+

You may also want to check the Introduction to Odoo +Components +by @guewen.

+

Table of contents

+ +
+

Usage

+

As a developer, you have access to a component system. You can find the +documentation in the code or on http://odoo-connector.com

+

In a nutshell, you can create components:

+
+from odoo.addons.component.core import Component
+
+class MagentoPartnerAdapter(Component):
+    _name = 'magento.partner.adapter'
+    _inherit = 'magento.adapter'
+
+    _usage = 'backend.adapter'
+    _collection = 'magento.backend'
+    _apply_on = ['res.partner']
+
+

And later, find the component you need at runtime (dynamic dispatch at +component level):

+
+def run(self, external_id):
+    backend_adapter = self.component(usage='backend.adapter')
+    external_data = backend_adapter.read(external_id)
+
+

In order for tests using components to work, you will need to use the +base class provided by `odoo.addons.component.tests.common`:

+
    +
  • TransactionComponentCase
  • +
+

There are also some specific base classes for testing the component +registry, using the ComponentRegistryCase as a base class. See the +docstrings in tests/common.py.

+
+
+

Changelog

+
+

16.0.1.0.0 (2022-10-04)

+
    +
  • [MIGRATION] from 15.0
  • +
+
+
+

15.0.1.0.0 (2021-11-25)

+
    +
  • [MIGRATION] from 14.0
  • +
+
+
+

14.0.1.0.0 (2020-10-22)

+
    +
  • [MIGRATION] from 13.0
  • +
+
+
+

13.0.1.0.0 (2019-10-23)

+
    +
  • [MIGRATION] from 12.0
  • +
+
+
+

12.0.1.0.0 (2018-10-02)

+
    +
  • [MIGRATION] from 11.0 branched at rev. 324e006
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp.

+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

Current maintainer:

+

guewen

+

This module is part of the OCA/connector project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/component/tests/__init__.py b/component/tests/__init__.py new file mode 100644 index 000000000..29c286e26 --- /dev/null +++ b/component/tests/__init__.py @@ -0,0 +1,5 @@ +from . import test_build_component +from . import test_component +from . import test_lookup +from . import test_work_on +from . import test_utils diff --git a/component/tests/common.py b/component/tests/common.py new file mode 100644 index 000000000..f7cf5d1fe --- /dev/null +++ b/component/tests/common.py @@ -0,0 +1,212 @@ +# Copyright 2017 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +import copy +from contextlib import contextmanager + +import odoo +from odoo import api +from odoo.tests import common + +from odoo.addons.component.core import ComponentRegistry, MetaComponent, _get_addon_name + + +@contextmanager +def new_rollbacked_env(): + registry = odoo.modules.registry.Registry(common.get_db_name()) + uid = odoo.SUPERUSER_ID + cr = registry.cursor() + try: + yield api.Environment(cr, uid, {}) + finally: + cr.rollback() # we shouldn't have to commit anything + cr.close() + + +class ComponentMixin: + @classmethod + def setUpComponent(cls): + with new_rollbacked_env() as env: + builder = env["component.builder"] + # build the components of every installed addons + comp_registry = builder._init_global_registry() + cls._components_registry = comp_registry + # ensure that we load only the components of the 'installed' + # modules, not 'to install', which means we load only the + # dependencies of the tested addons, not the siblings or + # children addons + builder.build_registry(comp_registry, states=("installed",)) + # build the components of the current tested addon + current_addon = _get_addon_name(cls.__module__) + env["component.builder"].load_components(current_addon) + if hasattr(cls, "env"): + cls.env.context = dict( + cls.env.context, components_registry=cls._components_registry + ) + + # pylint: disable=W8106 + def setUp(self): + # should be ready only during tests, never during installation + # of addons + self._components_registry.ready = True + + @self.addCleanup + def notready(): + self._components_registry.ready = False + + +class TransactionComponentCase(common.TransactionCase, ComponentMixin): + """A TransactionCase that loads all the components + + It it used like an usual Odoo's TransactionCase, but it ensures + that all the components of the current addon and its dependencies + are loaded. + + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.setUpComponent() + + # pylint: disable=W8106 + def setUp(self): + # resolve an inheritance issue (common.TransactionCase does not call + # super) + common.TransactionCase.setUp(self) + ComponentMixin.setUp(self) + # There's no env on setUpClass of TransactionCase, must do it here. + self.env.context = dict( + self.env.context, components_registry=self._components_registry + ) + + +class ComponentRegistryCase: + """This test case can be used as a base for writings tests on components + + This test case is meant to test components in a special component registry, + where you want to have maximum control on which components are loaded + or not, or when you want to create additional components in your tests. + + If you only want to *use* the components of the tested addon in your tests, + then consider using: + + * :class:`TransactionComponentCase` + + This test case creates a special + :class:`odoo.addons.component.core.ComponentRegistry` for the purpose of + the tests. By default, it loads all the components of the dependencies, but + not the components of the current addon (which you have to handle + manually). In your tests, you can add more components in 2 manners. + + All the components of an Odoo module:: + + self._load_module_components('connector') + + Only specific components:: + + self._build_components(MyComponent1, MyComponent2) + + Note: for the lookups of the components, the default component + registry is a global registry for the database. Here, you will + need to explicitly pass ``self.comp_registry`` in the + :class:`~odoo.addons.component.core.WorkContext`:: + + work = WorkContext(model_name='res.users', + collection='my.collection', + components_registry=self.comp_registry) + + Or:: + + collection_record = self.env['my.collection'].browse(1) + with collection_record.work_on( + 'res.partner', + components_registry=self.comp_registry) as work: + + """ + + @staticmethod + def _setup_registry(class_or_instance): + # keep the original classes registered by the metaclass + # so we'll restore them at the end of the tests, it avoid + # to pollute it with Stub / Test components + class_or_instance._original_components = copy.deepcopy( + MetaComponent._modules_components + ) + + # it will be our temporary component registry for our test session + class_or_instance.comp_registry = ComponentRegistry() + + # it builds the 'final component' for every component of the + # 'component' addon and push them in the component registry + class_or_instance.comp_registry.load_components("component") + # build the components of every installed addons already installed + # but the current addon (when running with pytest/nosetest, we + # simulate the --test-enable behavior by excluding the current addon + # which is in 'to install' / 'to upgrade' with --test-enable). + current_addon = _get_addon_name(class_or_instance.__module__) + with new_rollbacked_env() as env: + env["component.builder"].build_registry( + class_or_instance.comp_registry, + states=("installed",), + exclude_addons=[current_addon], + ) + + # Fake that we are ready to work with the registry + # normally, it is set to True and the end of the build + # of the components. Here, we'll add components later in + # the components registry, but we don't mind for the tests. + class_or_instance.comp_registry.ready = True + if hasattr(class_or_instance, "env"): + # let it propagate via ctx + class_or_instance.env.context = dict( + class_or_instance.env.context, + components_registry=class_or_instance.comp_registry, + ) + + @staticmethod + def _teardown_registry(class_or_instance): + # restore the original metaclass' classes + MetaComponent._modules_components = class_or_instance._original_components + + def _load_module_components(self, module): + self.comp_registry.load_components(module) + + def _build_components(self, *classes): + for cls in classes: + cls._build_component(self.comp_registry) + + +class TransactionComponentRegistryCase(common.TransactionCase, ComponentRegistryCase): + """Adds Odoo Transaction in the base Component TestCase. + + This class doesn't set up the registry for you. + You're supposed to explicitly call `_setup_registry` and `_teardown_registry` + when you need it, either on setUpClass and tearDownClass or setUp and tearDown. + + class MyTestCase(TransactionComponentRegistryCase): + def setUp(self): + super().setUp() + self._setup_registry(self) + + def tearDown(self): + self._teardown_registry(self) + super().tearDown() + + class MyTestCase(TransactionComponentRegistryCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_registry(cls) + + @classmethod + def tearDownClass(cls): + cls._teardown_registry(cls) + super().tearDownClass() + """ + + # pylint: disable=W8106 + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.collection = cls.env["collection.base"] diff --git a/component/tests/test_build_component.py b/component/tests/test_build_component.py new file mode 100644 index 000000000..a77f15cfe --- /dev/null +++ b/component/tests/test_build_component.py @@ -0,0 +1,285 @@ +# Copyright 2017 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +# Tell pylint to not bother us for all our fake component classes +# pylint: disable=consider-merging-classes-inherited + +from unittest import mock + +from odoo.addons.component.core import AbstractComponent, Component + +from .common import TransactionComponentRegistryCase + + +class TestBuildComponent(TransactionComponentRegistryCase): + """Test build of components + + All the tests in this suite are based on the same principle with + variations: + + * Create new Components (classes inheriting from + :class:`component.core.Component` or + :class:`component.core.AbstractComponent` + * Call :meth:`component.core.Component._build_component` on them + in order to build the 'final class' composed from all the ``_inherit`` + and push it in the components registry (``self.comp_registry`` here) + * Assert that classes are built, registered, have correct ``__bases__``... + + """ + + def setUp(self): + super().setUp() + self._setup_registry(self) + + def tearDown(self): + self._teardown_registry(self) + super().tearDown() + + def test_no_name(self): + """Ensure that a component has a _name""" + + class Component1(Component): + pass + + msg = ".*must have a _name.*" + with self.assertRaisesRegex(TypeError, msg): + Component1._build_component(self.comp_registry) + + def test_register(self): + """Able to register components in components registry""" + + class Component1(Component): + _name = "component1" + + class Component2(Component): + _name = "component2" + + # build the 'final classes' for the components and check that we find + # them in the components registry + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + self.assertEqual(["base", "component1", "component2"], list(self.comp_registry)) + + def test_inherit_bases(self): + """Check __bases__ of Component with _inherit""" + + class Component1(Component): + _name = "component1" + + class Component2(Component): + _inherit = "component1" + + class Component3(Component): + _inherit = "component1" + + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + Component3._build_component(self.comp_registry) + self.assertEqual( + (Component3, Component2, Component1, self.comp_registry["base"]), + self.comp_registry["component1"].__bases__, + ) + + def test_prototype_inherit_bases(self): + """Check __bases__ of Component with _inherit and different _name""" + + class Component1(Component): + _name = "component1" + + class Component2(Component): + _name = "component2" + _inherit = "component1" + + class Component3(Component): + _name = "component3" + _inherit = "component1" + + class Component4(Component): + _name = "component4" + _inherit = ["component2", "component3"] + + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + Component3._build_component(self.comp_registry) + Component4._build_component(self.comp_registry) + self.assertEqual( + (Component1, self.comp_registry["base"]), + self.comp_registry["component1"].__bases__, + ) + self.assertEqual( + (Component2, self.comp_registry["component1"], self.comp_registry["base"]), + self.comp_registry["component2"].__bases__, + ) + self.assertEqual( + (Component3, self.comp_registry["component1"], self.comp_registry["base"]), + self.comp_registry["component3"].__bases__, + ) + self.assertEqual( + ( + Component4, + self.comp_registry["component2"], + self.comp_registry["component3"], + self.comp_registry["base"], + ), + self.comp_registry["component4"].__bases__, + ) + + # pylint: disable=W8110 + def test_custom_build(self): + """Check that we can hook at the end of a Component build""" + + class Component1(Component): + _name = "component1" + + @classmethod + def _complete_component_build(cls): + # This method should be called after the Component + # is built, and before it is pushed in the registry + cls._build_done = True + + Component1._build_component(self.comp_registry) + # we inspect that our custom build has been executed + self.assertTrue(self.comp_registry["component1"]._build_done) + + def test_inherit_attrs(self): + """Check attributes inheritance of Components with _inherit""" + + class Component1(Component): + _name = "component1" + + msg = "ping" + + def say(self): + return "foo" + + class Component2(Component): + _name = "component2" + _inherit = "component1" + + msg = "pong" + + def say(self): + return super().say() + " bar" + + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + # we initialize the components, normally we should pass + # an instance of WorkContext, but we don't need a real one + # for this test + component1 = self.comp_registry["component1"](mock.Mock()) + component2 = self.comp_registry["component2"](mock.Mock()) + self.assertEqual("ping", component1.msg) + self.assertEqual("pong", component2.msg) + self.assertEqual("foo", component1.say()) + self.assertEqual("foo bar", component2.say()) + + def test_duplicate_component(self): + """Check that we can't have 2 components with the same name""" + + class Component1(Component): + _name = "component1" + + class Component2(Component): + _name = "component1" + + Component1._build_component(self.comp_registry) + msg = "Component.*already exists.*" + with self.assertRaisesRegex(TypeError, msg): + Component2._build_component(self.comp_registry) + + def test_no_parent(self): + """Ensure we can't _inherit a non-existent component""" + + class Component1(Component): + _name = "component1" + _inherit = "component1" + + msg = "Component.*does not exist in registry.*" + with self.assertRaisesRegex(TypeError, msg): + Component1._build_component(self.comp_registry) + + def test_no_parent2(self): + """Ensure we can't _inherit by prototype a non-existent component""" + + class Component1(Component): + _name = "component1" + + class Component2(Component): + _name = "component2" + _inherit = ["component1", "component3"] + + Component1._build_component(self.comp_registry) + msg = "Component.*inherits from non-existing component.*" + with self.assertRaisesRegex(TypeError, msg): + Component2._build_component(self.comp_registry) + + def test_add_inheritance(self): + """Ensure we can add a new inheritance""" + + class Component1(Component): + _name = "component1" + + class Component2(Component): + _name = "component2" + + class Component2bis(Component): + _name = "component2" + _inherit = ["component2", "component1"] + + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + Component2bis._build_component(self.comp_registry) + + self.assertEqual( + ( + Component2bis, + Component2, + self.comp_registry["component1"], + self.comp_registry["base"], + ), + self.comp_registry["component2"].__bases__, + ) + + def test_check_parent_component_over_abstract(self): + """Component can inherit from AbstractComponent""" + + class Component1(AbstractComponent): + _name = "component1" + + class Component2(Component): + _name = "component2" + _inherit = "component1" + + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + self.assertTrue(self.comp_registry["component1"]._abstract) + self.assertFalse(self.comp_registry["component2"]._abstract) + + def test_check_parent_abstract_over_component(self): + """Prevent AbstractComponent to inherit from Component""" + + class Component1(Component): + _name = "component1" + + class Component2(AbstractComponent): + _name = "component2" + _inherit = "component1" + + Component1._build_component(self.comp_registry) + msg = ".*cannot inherit from the non-abstract.*" + with self.assertRaisesRegex(TypeError, msg): + Component2._build_component(self.comp_registry) + + def test_check_transform_abstract_to_component(self): + """Prevent AbstractComponent to be transformed to Component""" + + class Component1(AbstractComponent): + _name = "component1" + + class Component1bis(Component): + _inherit = "component1" + + Component1._build_component(self.comp_registry) + msg = ".*transforms the abstract component.*into a non-abstract.*" + with self.assertRaisesRegex(TypeError, msg): + Component1bis._build_component(self.comp_registry) diff --git a/component/tests/test_component.py b/component/tests/test_component.py new file mode 100644 index 000000000..69f168957 --- /dev/null +++ b/component/tests/test_component.py @@ -0,0 +1,393 @@ +# Copyright 2017 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from contextlib import contextmanager + +from odoo.addons.component.core import Component +from odoo.addons.component.exception import NoComponentError, SeveralComponentError + +from .common import TransactionComponentRegistryCase + + +class TestComponent(TransactionComponentRegistryCase): + """Test usage of components + + These tests are a bit more broad that mere unit tests. + We test the chain odoo Model -> generate a WorkContext instance -> Work + with Component. + + Tests are inside Odoo transactions, so we can work + with Odoo's env / models. + """ + + def setUp(self): + super().setUp() + self._setup_registry(self) + self._setUpComponents() + + def tearDown(self): + self._teardown_registry(self) + super().tearDown() + + def _setUpComponents(self): + # create some Component to play with + class Component1(Component): + _name = "component1" + _collection = "collection.base" + _usage = "for.test" + _apply_on = ["res.partner"] + + class Component2(Component): + _name = "component2" + _collection = "collection.base" + _usage = "for.test" + _apply_on = ["res.users"] + + # build the components and register them in our + # test component registry + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + + # our collection, in a less abstract use case, it + # could be a record of 'magento.backend' for instance + self.collection_record = self.collection.new() + + @contextmanager + def get_base(): + # Our WorkContext, it will be passed along in every + # components so we can share data transversally. + # We are working with res.partner in the following tests, + # unless we change it in the test. + with self.collection_record.work_on( + "res.partner", + # we use a custom registry only + # for the sake of the tests + components_registry=self.comp_registry, + ) as work: + # We get the 'base' component, handy to test the base + # methods component, many_components, ... + yield work.component_by_name("base") + + self.get_base = get_base + + def test_component_attrs(self): + """Basic access to a Component's attribute""" + with self.get_base() as base: + # as we are working on res.partner, we should get 'component1' + comp = base.work.component(usage="for.test") + # but this is not what we test here, we test the attributes: + self.assertEqual(self.collection_record, comp.collection) + self.assertEqual(base.work, comp.work) + self.assertEqual(self.env, comp.env) + self.assertEqual(self.env["res.partner"], comp.model) + + def test_component_get_by_name_same_model(self): + """Use component_by_name with current working model""" + with self.get_base() as base: + # we ask a component directly by it's name, considering + # we work with res.partner, we should get 'component1' + # this is ok because it's _apply_on contains res.partner + comp = base.component_by_name("component1") + self.assertEqual("component1", comp._name) + self.assertEqual(self.env["res.partner"], comp.model) + + def test_component_get_by_name_other_model(self): + """Use component_by_name with another model""" + with self.get_base() as base: + # we ask a component directly by it's name, but we + # want to work with 'res.users', this is ok since + # component2's _apply_on contains res.users + comp = base.component_by_name("component2", model_name="res.users") + self.assertEqual("component2", comp._name) + self.assertEqual(self.env["res.users"], comp.model) + # what happens under the hood, is that a new WorkContext + # has been created for this model, with all the other values + # identical to the previous WorkContext (the one for res.partner) + # We can check that with: + self.assertNotEqual(base.work, comp.work) + self.assertEqual("res.partner", base.work.model_name) + self.assertEqual("res.users", comp.work.model_name) + + def test_component_get_by_name_wrong_model(self): + """Use component_by_name with a model not in _apply_on""" + msg = ( + "Component with name 'component2' can't be used " + "for model 'res.partner'.*" + ) + with self.get_base() as base: + with self.assertRaisesRegex(NoComponentError, msg): + # we ask for the model 'component2' but we are working + # with res.partner, and it only accepts res.users + base.component_by_name("component2") + + def test_component_get_by_name_not_exist(self): + """Use component_by_name on a component that do not exist""" + msg = "No component with name 'foo' found." + with self.get_base() as base: + with self.assertRaisesRegex(NoComponentError, msg): + base.component_by_name("foo") + + def test_component_by_usage_same_model(self): + """Use component(usage=...) on the same model""" + # we ask for a component having _usage == 'for.test', and + # model being res.partner (the model in the current WorkContext) + with self.get_base() as base: + comp = base.component(usage="for.test") + self.assertEqual("component1", comp._name) + self.assertEqual(self.env["res.partner"], comp.model) + + def test_component_by_usage_other_model(self): + """Use component(usage=...) on a different model (name)""" + # we ask for a component having _usage == 'for.test', and + # a different model (res.users) + with self.get_base() as base: + comp = base.component(usage="for.test", model_name="res.users") + self.assertEqual("component2", comp._name) + self.assertEqual(self.env["res.users"], comp.model) + # what happens under the hood, is that a new WorkContext + # has been created for this model, with all the other values + # identical to the previous WorkContext (the one for res.partner) + # We can check that with: + self.assertNotEqual(base.work, comp.work) + self.assertEqual("res.partner", base.work.model_name) + self.assertEqual("res.users", comp.work.model_name) + + def test_component_by_usage_other_model_env(self): + """Use component(usage=...) on a different model (instance)""" + with self.get_base() as base: + comp = base.component(usage="for.test", model_name=self.env["res.users"]) + self.assertEqual("component2", comp._name) + self.assertEqual(self.env["res.users"], comp.model) + + def test_component_error_several(self): + """Use component(usage=...) when more than one generic component match""" + + # we create 1 new Component with _usage 'for.test', in the same + # collection and no _apply_on, and we remove the _apply_on of component + # 1 so they are generic components for a collection + class Component3(Component): + _name = "component3" + _collection = "collection.base" + _usage = "for.test" + + class Component1(Component): + _inherit = "component1" + _collection = "collection.base" + _usage = "for.test" + _apply_on = None + + Component3._build_component(self.comp_registry) + Component1._build_component(self.comp_registry) + + with self.get_base() as base: + with self.assertRaises(SeveralComponentError): + # When a component has no _apply_on, it means it can be applied + # on *any* model. Here, the candidates components would be: + # component3 (because it has no _apply_on so apply in any case) + # component4 (for the same reason) + base.component(usage="for.test") + + def test_component_error_several_same_model(self): + """Use component(usage=...) when more than one component match a model""" + + # we create a new Component with _usage 'for.test', in the same + # collection and no _apply_on + class Component3(Component): + _name = "component3" + _collection = "collection.base" + _usage = "for.test" + _apply_on = ["res.partner"] + + Component3._build_component(self.comp_registry) + + with self.get_base() as base: + with self.assertRaises(SeveralComponentError): + # Here, the candidates components would be: + # component1 (because we are working with res.partner), + # component3 (for the same reason) + base.component(usage="for.test") + + def test_component_specific_model(self): + """Use component(usage=...) when more than one component match but + only one for the specific model""" + + # we create a new Component with _usage 'for.test', in the same + # collection and no _apply_on. This is a generic component for the + # collection + class Component3(Component): + _name = "component3" + _collection = "collection.base" + _usage = "for.test" + + Component3._build_component(self.comp_registry) + + with self.get_base() as base: + # When a component has no _apply_on, it means it can be applied on + # *any* model. Here, the candidates components would be: + # component1 # (because we are working with res.partner), + # component3 (because it # has no _apply_on so apply in any case). + # When a component is specifically linked to a model with + # _apply_on, it takes precedence over a generic component. It + # allows to create a generic implementation (component3 here) and + # override it only for a given model. So in this case, the final + # component is component1. + comp = base.component(usage="for.test") + self.assertEqual("component1", comp._name) + + def test_component_specific_collection(self): + """Use component(usage=...) when more than one component match but + only one for the specific collection""" + + # we create a new Component with _usage 'for.test', without collection + # and no _apply_on + class Component3(Component): + _name = "component3" + _usage = "for.test" + + Component3._build_component(self.comp_registry) + + with self.get_base() as base: + # When a component has no _apply_on, it means it can be applied + # on *any* model. Here, the candidates components would be: + # component1 (because we are working with res.partner), + # component3 (because it has no _apply_on so apply in any case). + # When a component has no _collection, it means it can be applied + # on all model if no component is found for the current collection: + # component3 must be ignored since a component (component1) exists + # and is specificaly linked to the expected collection. + comp = base.component(usage="for.test") + self.assertEqual("component1", comp._name) + + def test_component_specific_collection_specific_model(self): + """Use component(usage=...) when more than one component match but + only one for the specific model and collection""" + + # we create a new Component with _usage 'for.test', without collection + # and no _apply_on. This is a component generic for all collections and + # models + class Component3(Component): + _name = "component3" + _usage = "for.test" + + Component3._build_component(self.comp_registry) + + with self.get_base() as base: + # When a component has no _apply_on, it means it can be applied on + # *any* model, no _collection, it can be applied on *any* + # collection. + # Here, the candidates components would be: + # component1 (because we are working with res.partner), + # component3 (because it has no _apply_on and no _collection so + # apply in any case). + # When a component is specifically linked to a model with + # _apply_on, it takes precedence over a generic component, the same + # happens for collection. It allows to create a generic + # implementation (component3 here) and override it only for a given + # collection and model. So in this case, the final component is + # component1. + comp = base.component(usage="for.test") + self.assertEqual("component1", comp._name) + + def test_many_components(self): + """Use many_components(usage=...) on the same model""" + + class Component3(Component): + _name = "component3" + _collection = "collection.base" + _usage = "for.test" + + Component3._build_component(self.comp_registry) + + with self.get_base() as base: + comps = base.many_components(usage="for.test") + + # When a component has no _apply_on, it means it can be applied + # on *any* model. So here, both component1 and component3 match + self.assertEqual(["component1", "component3"], [c._name for c in comps]) + + def test_many_components_other_model(self): + """Use many_components(usage=...) on a different model (name)""" + + class Component3(Component): + _name = "component3" + _collection = "collection.base" + _apply_on = "res.users" + _usage = "for.test" + + Component3._build_component(self.comp_registry) + + with self.get_base() as base: + comps = base.many_components(usage="for.test", model_name="res.users") + + self.assertEqual(["component2", "component3"], [c._name for c in comps]) + + def test_many_components_other_model_env(self): + """Use many_components(usage=...) on a different model (instance)""" + + class Component3(Component): + _name = "component3" + _collection = "collection.base" + _apply_on = "res.users" + _usage = "for.test" + + Component3._build_component(self.comp_registry) + + with self.get_base() as base: + comps = base.many_components( + usage="for.test", model_name=self.env["res.users"] + ) + + self.assertEqual(["component2", "component3"], [c._name for c in comps]) + + def test_no_component(self): + """No component found for asked usage""" + with self.get_base() as base: + with self.assertRaises(NoComponentError): + base.component(usage="foo") + + def test_no_many_component(self): + """No component found for asked usage for many_components()""" + with self.get_base() as base: + self.assertEqual([], base.many_components(usage="foo")) + + def test_work_on_component(self): + """Check WorkContext.component() (shortcut to Component.component)""" + with self.get_base() as base: + comp = base.work.component(usage="for.test") + self.assertEqual("component1", comp._name) + + def test_work_on_many_components(self): + """Check WorkContext.many_components() + + (shortcut to Component.many_components) + """ + with self.get_base() as base: + comps = base.work.many_components(usage="for.test") + self.assertEqual("component1", comps[0]._name) + + def test_component_match(self): + """Lookup with match method""" + + class Foo(Component): + _name = "foo" + _collection = "collection.base" + _usage = "speaker" + _apply_on = ["res.partner"] + + @classmethod + def _component_match(cls, work, **kw): + return False + + class Bar(Component): + _name = "bar" + _collection = "collection.base" + _usage = "speaker" + _apply_on = ["res.partner"] + + self._build_components(Foo, Bar) + + with self.get_base() as base: + # both components would we returned without the + # _component_match method + comp = base.component(usage="speaker", model_name=self.env["res.partner"]) + self.assertEqual("bar", comp._name) diff --git a/component/tests/test_lookup.py b/component/tests/test_lookup.py new file mode 100644 index 000000000..082ee68e9 --- /dev/null +++ b/component/tests/test_lookup.py @@ -0,0 +1,193 @@ +# Copyright 2017 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from odoo.addons.component.core import AbstractComponent, Component + +from .common import TransactionComponentRegistryCase + + +class TestLookup(TransactionComponentRegistryCase): + """Test the ComponentRegistry + + Tests in this testsuite mainly do: + + * Create new Components (classes inheriting from + :class:`component.core.Component` or + :class:`component.core.AbstractComponent` + * Call :meth:`component.core.Component._build_component` on them + in order to build the 'final class' composed from all the ``_inherit`` + and push it in the components registry (``self.comp_registry`` here) + * Use the lookup method of the components registry and check + that we get the correct result + + """ + + def setUp(self): + super().setUp() + self._setup_registry(self) + + def tearDown(self): + self._teardown_registry(self) + super().tearDown() + + def test_lookup_collection(self): + """Lookup components of a collection""" + + # we register 2 components in foobar and one in other + class Foo(Component): + _name = "foo" + _collection = "foobar" + + class Bar(Component): + _name = "bar" + _collection = "foobar" + + class Homer(Component): + _name = "homer" + _collection = "other" + + self._build_components(Foo, Bar, Homer) + + # we should no see the component in 'other' + components = self.comp_registry.lookup("foobar") + self.assertEqual(["foo", "bar"], [c._name for c in components]) + + def test_lookup_usage(self): + """Lookup components by usage""" + + class Foo(Component): + _name = "foo" + _collection = "foobar" + _usage = "speaker" + + class Bar(Component): + _name = "bar" + _collection = "foobar" + _usage = "speaker" + + class Baz(Component): + _name = "baz" + _collection = "foobar" + _usage = "listener" + + self._build_components(Foo, Bar, Baz) + + components = self.comp_registry.lookup("foobar", usage="listener") + self.assertEqual("baz", components[0]._name) + + components = self.comp_registry.lookup("foobar", usage="speaker") + self.assertEqual(["foo", "bar"], [c._name for c in components]) + + def test_lookup_no_component(self): + """No component""" + # we just expect an empty list when no component match, the error + # handling is handled at an higher level + self.assertEqual([], self.comp_registry.lookup("something", usage="something")) + + def test_get_by_name(self): + """Get component by name""" + + class Foo(AbstractComponent): + _name = "foo" + _collection = "foobar" + + self._build_components(Foo) + # this is just a dict access + self.assertEqual("foo", self.comp_registry["foo"]._name) + + def test_lookup_abstract(self): + """Do not include abstract components in lookup""" + + class Foo(AbstractComponent): + _name = "foo" + _collection = "foobar" + _usage = "speaker" + + class Bar(Component): + _name = "bar" + _inherit = "foo" + + self._build_components(Foo, Bar) + + comp_registry = self.comp_registry + + # we should never have 'foo' in the returned components + # as it is abstract + components = comp_registry.lookup("foobar", usage="speaker") + self.assertEqual("bar", components[0]._name) + + components = comp_registry.lookup("foobar", usage="speaker") + self.assertEqual(["bar"], [c._name for c in components]) + + def test_lookup_model_name(self): + """Lookup with model names""" + + class Foo(Component): + _name = "foo" + _collection = "foobar" + _usage = "speaker" + # support list + _apply_on = ["res.partner"] + + class Bar(Component): + _name = "bar" + _collection = "foobar" + _usage = "speaker" + # support string + _apply_on = "res.users" + + class Any(Component): + # can be used with any model as far as we look it up + # with its usage + _name = "any" + _collection = "foobar" + _usage = "listener" + + self._build_components(Foo, Bar, Any) + + components = self.comp_registry.lookup( + "foobar", usage="speaker", model_name="res.partner" + ) + self.assertEqual("foo", components[0]._name) + + components = self.comp_registry.lookup( + "foobar", usage="speaker", model_name="res.users" + ) + self.assertEqual("bar", components[0]._name) + + components = self.comp_registry.lookup( + "foobar", usage="listener", model_name="res.users" + ) + self.assertEqual("any", components[0]._name) + + def test_lookup_cache(self): + """Lookup uses a cache""" + + class Foo(Component): + _name = "foo" + _collection = "foobar" + + self._build_components(Foo) + + components = self.comp_registry.lookup("foobar") + self.assertEqual(["foo"], [c._name for c in components]) + + # we add a new component + class Bar(Component): + _name = "bar" + _collection = "foobar" + + self._build_components(Bar) + + # As the lookups are cached, we should still see only foo, + # even if we added a new component. + # We do this for testing, but in a real use case, we can't + # add new Component classes on the fly, and when we install + # new addons, the registry is rebuilt and cache cleared. + components = self.comp_registry.lookup("foobar") + self.assertEqual(["foo"], [c._name for c in components]) + + self.comp_registry._cache.clear() + # now we should find them both as the cache has been cleared + components = self.comp_registry.lookup("foobar") + self.assertEqual(["foo", "bar"], [c._name for c in components]) diff --git a/component/tests/test_utils.py b/component/tests/test_utils.py new file mode 100644 index 000000000..f630a83e6 --- /dev/null +++ b/component/tests/test_utils.py @@ -0,0 +1,20 @@ +# Copyright 2023 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from unittest import mock + +from odoo.addons.component.utils import is_component_registry_ready + +from .common import TransactionComponentRegistryCase + + +class TestUtils(TransactionComponentRegistryCase): + def test_registry_ready(self): + path = "odoo.addons.component.utils.get_component_registry" + with mock.patch(path) as mocked: + mocked.return_value = None + self.assertFalse(is_component_registry_ready(self.env.cr.dbname)) + self._setup_registry(self) + mocked.return_value = self.comp_registry + self.assertTrue(is_component_registry_ready(self.env.cr.dbname)) + self._teardown_registry(self) diff --git a/component/tests/test_work_on.py b/component/tests/test_work_on.py new file mode 100644 index 000000000..2004ba7e5 --- /dev/null +++ b/component/tests/test_work_on.py @@ -0,0 +1,73 @@ +# Copyright 2017 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from odoo.addons.component.core import ComponentRegistry, WorkContext + +from .common import TransactionComponentRegistryCase + + +class TestWorkOn(TransactionComponentRegistryCase): + """Test on WorkContext + + This model is mostly a container, so we check the access + to the attributes and properties. + + """ + + def setUp(self): + super().setUp() + self._setup_registry(self) + + def tearDown(self): + self._teardown_registry(self) + super().tearDown() + + def test_collection_work_on(self): + """Create a new instance and test attributes access""" + collection_record = self.collection.new() + with collection_record.work_on("res.partner") as work: + self.assertEqual(collection_record, work.collection) + self.assertEqual("collection.base", work.collection._name) + self.assertEqual("res.partner", work.model_name) + self.assertEqual(self.env["res.partner"], work.model) + self.assertEqual(self.env, work.env) + + def test_collection_work_on_registry_via_context(self): + """Test propagation of registry via context""" + registry = ComponentRegistry() + collection_record = self.collection.with_context( + components_registry=registry + ).new() + with collection_record.work_on("res.partner") as work: + self.assertEqual(collection_record, work.collection) + self.assertEqual("collection.base", work.collection._name) + self.assertEqual("res.partner", work.model_name) + self.assertEqual(self.env["res.partner"], work.model) + self.assertEqual(work.env, collection_record.env) + self.assertEqual(work.components_registry, registry) + + def test_propagate_work_on(self): + """Check custom attributes and their propagation""" + registry = ComponentRegistry() + work = WorkContext( + model_name="res.partner", + collection=self.collection, + # we can customize the lookup registry, but used mostly for tests + components_registry=registry, + # we can pass our own keyword args that will set as attributes + test_keyword="value", + ) + self.assertIs(registry, work.components_registry) + # check that our custom keyword is set as attribute + self.assertEqual("value", work.test_keyword) + + # when we want to work on another model, work_on() create + # another instance and propagate the attributes to it + work2 = work.work_on("res.users") + self.assertNotEqual(work, work2) + self.assertEqual(self.env, work2.env) + self.assertEqual(self.collection, work2.collection) + self.assertEqual("res.users", work2.model_name) + self.assertIs(registry, work2.components_registry) + # test_keyword has been propagated to the new WorkContext instance + self.assertEqual("value", work2.test_keyword) diff --git a/component/utils.py b/component/utils.py new file mode 100644 index 000000000..66e955125 --- /dev/null +++ b/component/utils.py @@ -0,0 +1,14 @@ +# Copyright 2023 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from .core import _component_databases + + +def get_component_registry(dbname): + return _component_databases.get(dbname) + + +def is_component_registry_ready(dbname): + """Return True if the registry is ready to be used.""" + comp_registry = get_component_registry(dbname) + return comp_registry.ready if comp_registry else False diff --git a/component_event/README.rst b/component_event/README.rst new file mode 100644 index 000000000..878954eac --- /dev/null +++ b/component_event/README.rst @@ -0,0 +1,139 @@ +================= +Components Events +================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:97bef8a6c3971c475f2ebe58ebed781490658d2174669d49ee22cea9869fc0a0 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/18.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-18-0/connector-18-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=18.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/ + +**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 +========= + +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 `_. +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- Guewen Baconnier + +Other credits +------------- + +The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp. + +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 `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/component_event/__init__.py b/component_event/__init__.py new file mode 100644 index 000000000..fb993a0ab --- /dev/null +++ b/component_event/__init__.py @@ -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 diff --git a/component_event/__manifest__.py b/component_event/__manifest__.py new file mode 100644 index 000000000..bc972f7b2 --- /dev/null +++ b/component_event/__manifest__.py @@ -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": "18.0.1.0.0", + "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, +} diff --git a/component_event/components/__init__.py b/component_event/components/__init__.py new file mode 100644 index 000000000..44ad1cba0 --- /dev/null +++ b/component_event/components/__init__.py @@ -0,0 +1 @@ +from . import event diff --git a/component_event/components/event.py b/component_event/components/event.py new file mode 100644 index 000000000..67bfd17fc --- /dev/null +++ b/component_event/components/event.py @@ -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()._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()._complete_component_build() + cls._build_event_listener_component() + return diff --git a/component_event/core.py b/component_event/core.py new file mode 100644 index 000000000..cb41b2318 --- /dev/null +++ b/component_event/core.py @@ -0,0 +1,158 @@ +# 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().__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().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 f"""EventWorkContext( + {repr(self._env or self._collection)},{self.model_name})""" diff --git a/component_event/i18n/component_event.pot b/component_event/i18n/component_event.pot new file mode 100644 index 000000000..b9c4c847a --- /dev/null +++ b/component_event/i18n/component_event.pot @@ -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 18.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 "" diff --git a/component_event/i18n/es.po b/component_event/i18n/es.po new file mode 100644 index 000000000..dc687565d --- /dev/null +++ b/component_event/i18n/es.po @@ -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 \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" diff --git a/component_event/i18n/fr.po b/component_event/i18n/fr.po new file mode 100644 index 000000000..f5c6daaae --- /dev/null +++ b/component_event/i18n/fr.po @@ -0,0 +1,27 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component_event +# +# Translators: +# Nicolas JEUDY , 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 , 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" diff --git a/component_event/i18n/it.po b/component_event/i18n/it.po new file mode 100644 index 000000000..5752ec597 --- /dev/null +++ b/component_event/i18n/it.po @@ -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 17.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-02-05 00:26+0000\n" +"Last-Translator: mymage \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" diff --git a/component_event/i18n/zh_CN.po b/component_event/i18n/zh_CN.po new file mode 100644 index 000000000..6d574eac3 --- /dev/null +++ b/component_event/i18n/zh_CN.po @@ -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 "基础" diff --git a/component_event/models/__init__.py b/component_event/models/__init__.py new file mode 100644 index 000000000..0e4444933 --- /dev/null +++ b/component_event/models/__init__.py @@ -0,0 +1 @@ +from . import base diff --git a/component_event/models/base.py b/component_event/models/base.py new file mode 100644 index 000000000..97f3b8c12 --- /dev/null +++ b/component_event/models/base.py @@ -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().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().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().unlink() + return result diff --git a/component_event/pyproject.toml b/component_event/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/component_event/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/component_event/readme/CONTRIBUTORS.md b/component_event/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..7f97cd053 --- /dev/null +++ b/component_event/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Guewen Baconnier \<\> diff --git a/component_event/readme/CREDITS.md b/component_event/readme/CREDITS.md new file mode 100644 index 000000000..83b3ec91f --- /dev/null +++ b/component_event/readme/CREDITS.md @@ -0,0 +1 @@ +The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp. diff --git a/component_event/readme/DESCRIPTION.md b/component_event/readme/DESCRIPTION.md new file mode 100644 index 000000000..d3970c0b4 --- /dev/null +++ b/component_event/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +This module implements an event system ([Observer +pattern](https://en.wikipedia.org/wiki/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: diff --git a/component_event/readme/HISTORY.md b/component_event/readme/HISTORY.md new file mode 100644 index 000000000..b0d14a600 --- /dev/null +++ b/component_event/readme/HISTORY.md @@ -0,0 +1,5 @@ +## Next + +## 12.0.1.0.0 (2018-11-26) + +- \[MIGRATION\] from 12.0 branched at rev. 324e006 diff --git a/component_event/readme/USAGE.md b/component_event/readme/USAGE.md new file mode 100644 index 000000000..b8f2035ac --- /dev/null +++ b/component_event/readme/USAGE.md @@ -0,0 +1,33 @@ +As a developer, you have access to a events system. You can find the +documentation in the code or on + +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)` diff --git a/component_event/static/description/icon.png b/component_event/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/component_event/static/description/icon.png differ diff --git a/component_event/static/description/index.html b/component_event/static/description/index.html new file mode 100644 index 000000000..2a8b56489 --- /dev/null +++ b/component_event/static/description/index.html @@ -0,0 +1,487 @@ + + + + + +Components Events + + + +
+

Components Events

+ + +

Beta License: LGPL-3 OCA/connector Translate me on Weblate Try me on Runboat

+

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/

+

Table of contents

+ +
+

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

+ +
+

12.0.1.0.0 (2018-11-26)

+
    +
  • [MIGRATION] from 12.0 branched at rev. 324e006
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp.

+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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 project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/component_event/tests/__init__.py b/component_event/tests/__init__.py new file mode 100644 index 000000000..8d73378b6 --- /dev/null +++ b/component_event/tests/__init__.py @@ -0,0 +1 @@ +from . import test_event diff --git a/component_event/tests/test_event.py b/component_event/tests/test_event.py new file mode 100644 index 000000000..da6056fa1 --- /dev/null +++ b/component_event/tests/test_event.py @@ -0,0 +1,467 @@ +# Copyright 2017 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from unittest import mock + +from odoo.tests.case import TestCase +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(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") + env.context = mock.MagicMock() + env.context.get = mock.MagicMock(return_value=False) + 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") + env.context = mock.MagicMock() + env.context.get = mock.MagicMock(return_value=False) + 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") + env.context = mock.MagicMock() + env.context.get = mock.MagicMock(return_value=False) + 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().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().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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..d3dfeea70 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +cachetools diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml new file mode 100644 index 000000000..5edc69705 --- /dev/null +++ b/setup/_metapackage/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "odoo-addons-oca-connector" +version = "18.0.20241009.1" +dependencies = [ + "odoo-addon-component==18.0.*", + "odoo-addon-component_event==18.0.*", + "odoo-addon-test_component==18.0.*", +] +classifiers=[ + "Programming Language :: Python", + "Framework :: Odoo", + "Framework :: Odoo :: 18.0", +] diff --git a/test_component/README.rst b/test_component/README.rst new file mode 100644 index 000000000..6748d4571 --- /dev/null +++ b/test_component/README.rst @@ -0,0 +1,94 @@ +================ +Components Tests +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2215f4b6a2f2130b4df71d37ab7256adbc30017c52ef82969d85d67a867bbbbd + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |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/18.0/test_component + :alt: OCA/connector +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/connector-18-0/connector-18-0-test_component + :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=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This addon is not meant to be installed, except for running the tests. +It extends the Odoo Models in order to run automated tests on the +Connector framework + +The basic tests are integrated within the ``component`` addon. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- Guewen Baconnier (Camptocamp) + +Other credits +------------- + +The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp. + +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. + +.. |maintainer-guewen| image:: https://github.com/guewen.png?size=40px + :target: https://github.com/guewen + :alt: guewen + +Current `maintainer `__: + +|maintainer-guewen| + +This module is part of the `OCA/connector `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/test_component/__init__.py b/test_component/__init__.py new file mode 100644 index 000000000..0f00a6730 --- /dev/null +++ b/test_component/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import components diff --git a/test_component/__manifest__.py b/test_component/__manifest__.py new file mode 100644 index 000000000..19c7e989e --- /dev/null +++ b/test_component/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2019 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +{ + "name": "Components Tests", + "summary": "Automated tests for Components, do not install.", + "version": "18.0.1.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "license": "LGPL-3", + "category": "Hidden", + "depends": ["component"], + "website": "https://github.com/OCA/connector", + "data": ["security/ir.model.access.csv"], + "installable": True, + "development_status": "Production/Stable", + "maintainers": ["guewen"], +} diff --git a/test_component/components/__init__.py b/test_component/components/__init__.py new file mode 100644 index 000000000..b7d85d947 --- /dev/null +++ b/test_component/components/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2017 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from . import components diff --git a/test_component/components/components.py b/test_component/components/components.py new file mode 100644 index 000000000..7cfa0a4c4 --- /dev/null +++ b/test_component/components/components.py @@ -0,0 +1,35 @@ +# Copyright 2017 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from odoo.addons.component.core import AbstractComponent, Component + + +class BaseComponent(AbstractComponent): + _inherit = "base" + + def test_inherit_base(self): + return "test_inherit_base" + + +class Mapper(AbstractComponent): + _name = "mapper" + + def test_inherit_component(self): + return "test_inherit_component" + + +class ImportTestMapper(Component): + _name = "test.mapper" + _inherit = "mapper" + _usage = "import.mapper" + _collection = "test.component.collection" + + def name(self): + return "test.mapper" + + +class UserTestComponent(Component): + _name = "test.user.component" + _apply_on = ["res.users"] + _usage = "test1" + _collection = "test.component.collection" diff --git a/test_component/i18n/am.po b/test_component/i18n/am.po new file mode 100644 index 000000000..f049e96eb --- /dev/null +++ b/test_component/i18n/am.po @@ -0,0 +1,59 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Amharic (https://www.transifex.com/oca/teams/23907/am/)\n" +"Language: am\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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Creado en" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" diff --git a/test_component/i18n/ca.po b/test_component/i18n/ca.po new file mode 100644 index 000000000..9f99f421b --- /dev/null +++ b/test_component/i18n/ca.po @@ -0,0 +1,59 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Catalan (https://www.transifex.com/oca/teams/23907/ca/)\n" +"Language: ca\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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Creat per" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Creat a" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Darrear modificació per" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Darrera modificació el" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" diff --git a/test_component/i18n/de.po b/test_component/i18n/de.po new file mode 100644 index 000000000..a8fedf22b --- /dev/null +++ b/test_component/i18n/de.po @@ -0,0 +1,62 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: German (https://www.transifex.com/oca/teams/23907/de/)\n" +"Language: de\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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Angelegt durch" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Angelegt am" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "Anzeigebezeichnung" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Zuletzt aktualisiert durch" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Zuletzt aktualisiert am" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" + +#~ msgid "Last Modified on" +#~ msgstr "Zuletzt aktualisiert am" diff --git a/test_component/i18n/el_GR.po b/test_component/i18n/el_GR.po new file mode 100644 index 000000000..b12e2a99a --- /dev/null +++ b/test_component/i18n/el_GR.po @@ -0,0 +1,60 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Greek (Greece) (https://www.transifex.com/oca/teams/23907/" +"el_GR/)\n" +"Language: el_GR\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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Δημιουργήθηκε από " + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Δημιουργήθηκε στις" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "Κωδικός" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Τελευταία ενημέρωση από" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Τελευταία ενημέρωση στις" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" diff --git a/test_component/i18n/es.po b/test_component/i18n/es.po new file mode 100644 index 000000000..4fd403ac8 --- /dev/null +++ b/test_component/i18n/es.po @@ -0,0 +1,63 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2023-08-02 13:09+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Creado en" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "Nombre" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "Colección de componentes de prueba" + +#~ msgid "Last Modified on" +#~ msgstr "Última modificación el" diff --git a/test_component/i18n/es_ES.po b/test_component/i18n/es_ES.po new file mode 100644 index 000000000..3318c5244 --- /dev/null +++ b/test_component/i18n/es_ES.po @@ -0,0 +1,60 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Spanish (Spain) (https://www.transifex.com/oca/teams/23907/" +"es_ES/)\n" +"Language: es_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" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Creado en" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" diff --git a/test_component/i18n/fi.po b/test_component/i18n/fi.po new file mode 100644 index 000000000..9a6cf1651 --- /dev/null +++ b/test_component/i18n/fi.po @@ -0,0 +1,62 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Finnish (https://www.transifex.com/oca/teams/23907/fi/)\n" +"Language: fi\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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Luonut" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Luotu" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "Nimi" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Viimeksi päivittänyt" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Viimeksi päivitetty" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" + +#~ msgid "Last Modified on" +#~ msgstr "Viimeksi muokattu" diff --git a/test_component/i18n/fr.po b/test_component/i18n/fr.po new file mode 100644 index 000000000..151f782fe --- /dev/null +++ b/test_component/i18n/fr.po @@ -0,0 +1,66 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +# Nicolas JEUDY , 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 , 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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "Nom" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" + +#~ msgid "Last Modified on" +#~ msgstr "Dernière modification le" + +#~ msgid "test.component.collection" +#~ msgstr "test.component.collection" diff --git a/test_component/i18n/gl.po b/test_component/i18n/gl.po new file mode 100644 index 000000000..dfcd37893 --- /dev/null +++ b/test_component/i18n/gl.po @@ -0,0 +1,59 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Galician (https://www.transifex.com/oca/teams/23907/gl/)\n" +"Language: gl\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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Creado en" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "ültima actualización por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" diff --git a/test_component/i18n/it.po b/test_component/i18n/it.po new file mode 100644 index 000000000..350b10401 --- /dev/null +++ b/test_component/i18n/it.po @@ -0,0 +1,63 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2023-09-22 16:38+0000\n" +"Last-Translator: mymage \n" +"Language-Team: Italian (https://www.transifex.com/oca/teams/23907/it/)\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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "Nome" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "Test collezone componente" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" diff --git a/test_component/i18n/pt.po b/test_component/i18n/pt.po new file mode 100644 index 000000000..7cb075c6e --- /dev/null +++ b/test_component/i18n/pt.po @@ -0,0 +1,59 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Portuguese (https://www.transifex.com/oca/teams/23907/pt/)\n" +"Language: pt\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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Criado por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Criado em" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Atualizado pela última vez por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Atualizado pela última vez em" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" diff --git a/test_component/i18n/pt_BR.po b/test_component/i18n/pt_BR.po new file mode 100644 index 000000000..8c760465f --- /dev/null +++ b/test_component/i18n/pt_BR.po @@ -0,0 +1,67 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2019-08-26 15:01+0000\n" +"Last-Translator: Rodrigo Macedo \n" +"Language-Team: Portuguese (Brazil) (https://www.transifex.com/oca/" +"teams/23907/pt_BR/)\n" +"Language: pt_BR\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 3.8\n" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Criado por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Criado em " + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "Exibir Nome" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Última atualização por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Última atualização em " + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "Nome" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" + +#~ msgid "Last Modified on" +#~ msgstr "Última modificação no" + +#~ msgid "test.component.collection" +#~ msgstr "test.component.collection" diff --git a/test_component/i18n/pt_PT.po b/test_component/i18n/pt_PT.po new file mode 100644 index 000000000..e77faa2e5 --- /dev/null +++ b/test_component/i18n/pt_PT.po @@ -0,0 +1,60 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Portuguese (Portugal) (https://www.transifex.com/oca/" +"teams/23907/pt_PT/)\n" +"Language: pt_PT\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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Criado por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Criado em" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Atualizado pela última vez por" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Atualizado pela última vez em" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" diff --git a/test_component/i18n/sl.po b/test_component/i18n/sl.po new file mode 100644 index 000000000..6ad8f5424 --- /dev/null +++ b/test_component/i18n/sl.po @@ -0,0 +1,63 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Slovenian (https://www.transifex.com/oca/teams/23907/sl/)\n" +"Language: sl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || " +"n%100==4 ? 2 : 3);\n" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Ustvaril" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Ustvarjeno" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "Prikazni naziv" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Zadnji posodobil" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Zadnjič posodobljeno" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" + +#~ msgid "Last Modified on" +#~ msgstr "Zadnjič spremenjeno" diff --git a/test_component/i18n/test_component.pot b/test_component/i18n/test_component.pot new file mode 100644 index 000000000..43e19792e --- /dev/null +++ b/test_component/i18n/test_component.pot @@ -0,0 +1,54 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" diff --git a/test_component/i18n/tr.po b/test_component/i18n/tr.po new file mode 100644 index 000000000..84ace4454 --- /dev/null +++ b/test_component/i18n/tr.po @@ -0,0 +1,59 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Turkish (https://www.transifex.com/oca/teams/23907/tr/)\n" +"Language: tr\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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "Oluşturan" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "Oluşturuldu" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "Son güncelleyen" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "Son güncelleme" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" diff --git a/test_component/i18n/zh_CN.po b/test_component/i18n/zh_CN.po new file mode 100644 index 000000000..1b33500d7 --- /dev/null +++ b/test_component/i18n/zh_CN.po @@ -0,0 +1,63 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * test_component +# +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: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid +msgid "Created by" +msgstr "创建者" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date +msgid "Created on" +msgstr "创建时间" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name +msgid "Display Name" +msgstr "显示名称" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id +msgid "ID" +msgstr "ID" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid +msgid "Last Updated by" +msgstr "最后更新者" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date +msgid "Last Updated on" +msgstr "最后更新时间" + +#. module: test_component +#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name +msgid "Name" +msgstr "名称" + +#. module: test_component +#: model:ir.model,name:test_component.model_test_component_collection +msgid "Test Component Collection" +msgstr "" + +#~ msgid "Last Modified on" +#~ msgstr "最后修改时间" + +#~ msgid "test.component.collection" +#~ msgstr "test.component.collection" diff --git a/test_component/models/__init__.py b/test_component/models/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/test_component/models/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/test_component/models/models.py b/test_component/models/models.py new file mode 100644 index 000000000..e0b94335f --- /dev/null +++ b/test_component/models/models.py @@ -0,0 +1,12 @@ +# Copyright 2016 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from odoo import fields, models + + +class TestComponentCollection(models.Model): + _name = "test.component.collection" + _description = "Test Component Collection" + _inherit = ["collection.base"] + + name = fields.Char() diff --git a/test_component/pyproject.toml b/test_component/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/test_component/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/test_component/readme/CONTRIBUTORS.md b/test_component/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..16f2383d6 --- /dev/null +++ b/test_component/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Guewen Baconnier (Camptocamp) diff --git a/test_component/readme/CREDITS.md b/test_component/readme/CREDITS.md new file mode 100644 index 000000000..8edec949d --- /dev/null +++ b/test_component/readme/CREDITS.md @@ -0,0 +1,2 @@ +The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp. diff --git a/test_component/readme/DESCRIPTION.md b/test_component/readme/DESCRIPTION.md new file mode 100644 index 000000000..e3b5adab0 --- /dev/null +++ b/test_component/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +This addon is not meant to be installed, except for running the tests. +It extends the Odoo Models in order to run automated tests on the +Connector framework + +The basic tests are integrated within the `component` addon. diff --git a/test_component/security/ir.model.access.csv b/test_component/security/ir.model.access.csv new file mode 100644 index 000000000..d8a18cf3b --- /dev/null +++ b/test_component/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_test_component_collection,access_test_component_collection,model_test_component_collection,base.group_user,1,1,1,1 diff --git a/test_component/static/description/icon.png b/test_component/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/test_component/static/description/icon.png differ diff --git a/test_component/static/description/index.html b/test_component/static/description/index.html new file mode 100644 index 000000000..962388778 --- /dev/null +++ b/test_component/static/description/index.html @@ -0,0 +1,434 @@ + + + + + +Components Tests + + + +
+

Components Tests

+ + +

Production/Stable License: LGPL-3 OCA/connector Translate me on Weblate Try me on Runboat

+

This addon is not meant to be installed, except for running the tests. +It extends the Odoo Models in order to run automated tests on the +Connector framework

+

The basic tests are integrated within the component addon.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+
    +
  • Guewen Baconnier (Camptocamp)
  • +
+
+
+

Other credits

+

The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp.

+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

Current maintainer:

+

guewen

+

This module is part of the OCA/connector project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/test_component/tests/__init__.py b/test_component/tests/__init__.py new file mode 100644 index 000000000..8c49d833d --- /dev/null +++ b/test_component/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_components +from . import test_component_collection diff --git a/test_component/tests/test_component_collection.py b/test_component/tests/test_component_collection.py new file mode 100644 index 000000000..5cb985a61 --- /dev/null +++ b/test_component/tests/test_component_collection.py @@ -0,0 +1,24 @@ +# Copyright 2013-2019 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from odoo.addons.component.tests.common import TransactionComponentCase +from odoo.addons.test_component.components.components import UserTestComponent + + +class TestComponentCollection(TransactionComponentCase): + def setUp(self): + super().setUp() + self.collection = self.env["test.component.collection"].create({"name": "Test"}) + + def tearDown(self): + super().tearDown() + + def test_component_by_name(self): + with self.collection.work_on("res.users") as work: + component = work.component_by_name(name="test.user.component") + self.assertEqual(UserTestComponent._name, component._name) + + def test_components_usage(self): + with self.collection.work_on("res.users") as work: + component = work.component(usage="test1") + self.assertEqual(UserTestComponent._name, component._name) diff --git a/test_component/tests/test_components.py b/test_component/tests/test_components.py new file mode 100644 index 000000000..20b21c0ca --- /dev/null +++ b/test_component/tests/test_components.py @@ -0,0 +1,30 @@ +# Copyright 2019 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from odoo.addons.component.tests.common import TransactionComponentCase + + +class TestComponentInheritance(TransactionComponentCase): + def setUp(self): + super().setUp() + self.collection = self.env["test.component.collection"].create({"name": "Test"}) + + def test_inherit_base(self): + with self.collection.work_on("res.users") as work: + component = work.component_by_name("base") + self.assertEqual("test_inherit_base", component.test_inherit_base()) + + def test_inherit_component(self): + with self.collection.work_on("res.users") as work: + component = work.component_by_name("mapper") + self.assertEqual( + "test_inherit_component", component.test_inherit_component() + ) + + def test_inherit_prototype_component(self): + with self.collection.work_on("res.users") as work: + component = work.component_by_name("test.mapper") + self.assertEqual( + "test_inherit_component", component.test_inherit_component() + ) + self.assertEqual("test.mapper", component.name())