Initial commit: OCA Technical packages (595 packages)

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

View file

@ -0,0 +1,178 @@
=========
Datamodel
=========
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:220702c6d930c27e2dbb4016e1f6bef6e1b9c7b96cff4b3c5ad27fdc5733ad26
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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%2Frest--framework-lightgray.png?logo=github
:target: https://github.com/OCA/rest-framework/tree/16.0/datamodel
:alt: OCA/rest-framework
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-datamodel
: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/rest-framework&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This addon allows you to define simple data models supporting serialization/deserialization
to/from json
Datamodels are `Marshmallow models <https://github.com/sv-tools/marshmallow-objects>`_ classes that can be inherited as Odoo
Models.
**Table of contents**
.. contents::
:local:
Usage
=====
To define your own datamodel you just need to create a class that inherits from
``odoo.addons.datamodel.core.Datamodel``
.. code-block:: python
from marshmallow import fields
from odoo.addons.base_rest import restapi
from odoo.addons.component.core import Component
from odoo.addons.datamodel.core import Datamodel
class PartnerShortInfo(Datamodel):
_name = "partner.short.info"
id = fields.Integer(required=True, allow_none=False)
name = fields.String(required=True, allow_none=False)
class PartnerInfo(Datamodel):
_name = "partner.info"
_inherit = "partner.short.info"
street = fields.String(required=True, allow_none=False)
street2 = fields.String(required=False, allow_none=True)
zip_code = fields.String(required=True, allow_none=False)
city = fields.String(required=True, allow_none=False)
phone = fields.String(required=False, allow_none=True)
is_componay = fields.Boolean(required=False, allow_none=False)
As for odoo models, you can extend the `base` datamodel by inheriting of `base`.
.. code-block:: python
class Base(Datamodel):
_inherit = "base"
def _my_method(self):
pass
Datamodels are available through the `datamodels` registry provided by the Odoo's environment.
.. code-block:: python
class ResPartner(Model):
_inherit = "res.partner"
def _to_partner_info(self):
PartnerInfo = self.env.datamodels["partner.info"]
partner_info = PartnerInfo(partial=True)
partner_info.id = partner.id
partner_info.name = partner.name
partner_info.street = partner.street
partner_info.street2 = partner.street2
partner_info.zip_code = partner.zip
partner_info.city = partner.city
partner_info.phone = partner.phone
partner_info.is_company = partner.is_company
return partner_info
The Odoo's environment is also available into the datamodel instance.
.. code-block:: python
class MyDataModel(Datamodel):
_name = "my.data.model"
def _my_method(self):
partners = self.env["res.partner"].search([])
.. warning::
The `env` property into a Datamodel instance is mutable. IOW, you can't rely
on information (context, user) provided by the environment. The `env` property
is a helper property that give you access to the odoo's registry and must
be use with caution.
Known issues / Roadmap
======================
The `roadmap <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Adatamodel>`_
and `known issues <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Adatamodel>`_ can
be found on GitHub.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/rest-framework/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/rest-framework/issues/new?body=module:%20datamodel%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* ACSONE SA/NV
Contributors
~~~~~~~~~~~~
* Laurent Mignon <laurent.mignon@acsone.eu>
* `Tecnativa <https://www.tecnativa.com>`_:
* Carlos Roca
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-lmignon| image:: https://github.com/lmignon.png?size=40px
:target: https://github.com/lmignon
:alt: lmignon
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-lmignon|
This module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/16.0/datamodel>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,2 @@
from . import builder
from . import datamodels

View file

@ -0,0 +1,19 @@
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
{
"name": "Datamodel",
"summary": """
This addon allows you to define simple data models supporting
serialization/deserialization""",
"version": "16.0.1.0.2",
"license": "LGPL-3",
"development_status": "Beta",
"author": "ACSONE SA/NV, " "Odoo Community Association (OCA)",
"maintainers": ["lmignon"],
"website": "https://github.com/OCA/rest-framework",
"external_dependencies": {
"python": ["marshmallow<4.0.0", "marshmallow-objects>=2.0.0"]
},
"installable": True,
}

View file

@ -0,0 +1,96 @@
# Copyright 2017 Camptocamp SA
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
"""
Datamodels Builder
==================
Build the datamodels at the build of a registry.
"""
from odoo import models, modules
from .core import DEFAULT_CACHE_SIZE, DatamodelRegistry, _datamodel_databases
class DatamodelBuilder(models.AbstractModel):
"""Build the datamodel classes
And register them in a global registry.
Every time an Odoo registry is built, the know datamodels are cleared and
rebuilt as well. The Datamodel classes are built using the same mechanism
than Odoo's Models: a final class is created, taking every Datamodels with
a ``_name`` and applying Datamodels with an ``_inherits`` upon them.
The final Datamodel classes are registered in global registry.
This class is an Odoo model, allowing us to hook the build of the
datamodels 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 datamodels Classes and in the correct
order.
"""
_name = "datamodel.builder"
_description = "Datamodel Builder"
_datamodels_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 rebuild the datamodels. We use a new
# registry so we have an empty cache and we'll add datamodels in it.
datamodels_registry = self._init_global_registry()
self.build_registry(datamodels_registry)
datamodels_registry.ready = True
def _init_global_registry(self):
datamodels_registry = DatamodelRegistry(
cachesize=self._datamodels_registry_cache_size
)
_datamodel_databases[self.env.cr.dbname] = datamodels_registry
return datamodels_registry
def build_registry(self, datamodels_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 datamodels following the order
# of the addons' dependencies
graph = 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_datamodels(module.name, datamodels_registry=datamodels_registry)
def load_datamodels(self, module, datamodels_registry=None):
"""Build every datamodel known by MetaDatamodel for an odoo module
The final datamodel (composed by all the Datamodel classes in this
module) will be pushed into the registry.
:param module: the name of the addon for which we want to load
the datamodels
:type module: str | unicode
:param registry: the registry in which we want to put the Datamodel
:type registry: :py:class:`~.core.DatamodelRegistry`
"""
datamodels_registry = (
datamodels_registry or _datamodel_databases[self.env.cr.dbname]
)
datamodels_registry.load_datamodels(module)

View file

@ -0,0 +1,428 @@
# Copyright 2017 Camptocamp SA
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
import logging
from collections import OrderedDict, defaultdict
from contextlib import ExitStack
from marshmallow import INCLUDE
from odoo.api import Environment
from odoo.tools import LastOrderedSet, OrderedSet
_logger = logging.getLogger(__name__)
try:
import marshmallow
from marshmallow_objects.models import Model as MarshmallowModel, ModelMeta
except ImportError:
_logger.debug("Cannot import 'marshmallow_objects'.")
# The Cache size represents the number of items, so the number
# of datamodels (include abstract datamodels) 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 = module_parts[2]
else:
addon_name = module_parts[0]
return addon_name
def _get_nested_schemas(schema):
res = [schema]
for field in schema.fields.values():
if getattr(field, "schema", None):
res += _get_nested_schemas(field.schema)
return res
class DatamodelDatabases(dict):
"""Holds a registry of datamodels for each database"""
class DatamodelRegistry(object):
"""Store all the datamodel and allow to retrieve them by name
The key is the ``_name`` of the datamodels.
This is an OrderedDict, because we want to keep the registration order of
the datamodels, addons loaded first have their datamodels found first.
The :attr:`ready` attribute must be set to ``True`` when all the datamodels
are loaded.
"""
def __init__(self, cachesize=DEFAULT_CACHE_SIZE):
self._datamodels = OrderedDict()
self._loaded_modules = set()
self.ready = False
def __getitem__(self, key):
return self._datamodels[key]
def __setitem__(self, key, value):
self._datamodels[key] = value
def __contains__(self, key):
return key in self._datamodels
def get(self, key, default=None):
return self._datamodels.get(key, default)
def __iter__(self):
return iter(self._datamodels)
def load_datamodels(self, module):
if module in self._loaded_modules:
return
for datamodel_class in MetaDatamodel._modules_datamodels[module]:
datamodel_class._build_datamodel(self)
self._loaded_modules.add(module)
# We will store a DatamodeltRegistry per database here,
# it will be cleared and updated when the odoo's registry is rebuilt
_datamodel_databases = DatamodelDatabases()
@marshmallow.post_load
def __make_object__(self, data, **kwargs):
datamodel = self._env.datamodels[self._datamodel_name]
return datamodel(__post_load__=True, __schema__=self, **data)
class MetaDatamodel(ModelMeta):
"""Metaclass for Datamodel
Every new :class:`Datamodel` will be added to ``_modules_datamodels``,
that will be used by the datamodel builder.
"""
_modules_datamodels = defaultdict(list)
def __init__(self, name, bases, attrs):
if not self._register:
self._register = True
super(MetaDatamodel, self).__init__(name, bases, attrs)
return
# If datamodels are declared in tests, exclude them from the
# "datamodels of the addon" list. If not, when we use the
# "load_datamodels" method, all the test datamodels 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 datamodels for the purpose of the test, then a
# second tests uses the "load_datamodels" to load all the addons of the
# module: it will load the datamodel of the previous test.
if "tests" in self.__module__.split("."):
return
if not hasattr(self, "_module"):
self._module = _get_addon_name(self.__module__)
self._modules_datamodels[self._module].append(self)
def __call__(self, *args, **kwargs):
"""Allow to set any field (including 'dump_only') at instantiation
This is not an issue thanks to cleanup during (de)serialization
"""
kwargs["unknown"] = kwargs.get("unknown", INCLUDE)
return super().__call__(*args, **kwargs)
class Datamodel(MarshmallowModel, metaclass=MetaDatamodel):
"""Main Datamodel Model
All datamodels have a Python inheritance either on
:class:`Datamodel`.
Inheritance mechanism
The inheritance mechanism is like the Odoo's one for Models. Each
datamodel has a ``_name``. This is the absolute minimum in a Datamodel
class.
::
from marshmallow import fields
from odoo.addons.datamodel.core import Datamodel
class MyDatamodel(Datamodel):
_name = 'my.datamodel'
name = fields.String()
Every datamodel implicitly inherit from the `'base'` datamodel.
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 datamodel we want to extend. With
the following example, ``my.datamodel`` is now able to speak and to
yell.
::
class MyDatamodel(Datamodel): # name of the class does not matter
_inherit = 'my.datamodel'
The second has a different ``_name``, it creates a new datamodel,
including the behavior of the inherited datamodel, but without
modifying it.
::
class AnotherDatamodel(Datamodel):
_name = 'another.datamodel'
_inherit = 'my.datamodel'
age = fields.Int()
"""
_register = False
_env = None # Odoo Environment
# used for inheritance
_name = None #: Name of the datamodel
#: Name or list of names of the datamodel(s) to inherit from
_inherit = None
def __init__(self, context=None, partial=None, env=None, **kwargs):
self._env = env or type(self)._env
super().__init__(context=context, partial=partial, **kwargs)
@property
def env(self):
return self._env
@classmethod
def get_schema(cls, **kwargs):
"""
Get a marshmallow schema instance
:param kwargs:
:return:
"""
return cls.__get_schema_class__(**kwargs)
@classmethod
def validate(cls, data, context=None, many=None, partial=None, unknown=None):
schema = cls.__get_schema_class__(
context=context, partial=partial, unknown=unknown
)
all_schemas = _get_nested_schemas(schema)
with ExitStack() as stack:
# propagate 'unknown' to each nested schema during validate
for nested_schema in all_schemas:
stack.enter_context(cls.propagate_unknwown(nested_schema, unknown))
return schema.validate(data, many=many, partial=partial)
@classmethod
def _build_datamodel(cls, registry):
"""Instantiate a given Datamodel in the datamodels registry.
This method is called at the end of the Odoo's registry build. The
caller is :meth:`datamodel.builder.DatamodelBuilder.load_datamodels`.
It generates new classes, which will be the Datamodel classes we will
be using. The new classes are generated following the inheritance
of ``_inherit``. It ensures that the ``__bases__`` of the generated
Datamodel classes follow the ``_inherit`` chain.
Once a Datamodel class is created, it adds it in the Datamodel Registry
(:class:`DatamodelRegistry`), so it will be available for
lookups.
At the end of new class creation, a hook method
:meth:`_complete_datamodel_build` is called, so you can customize
further the created datamodels.
The following code is roughly the same than the Odoo's one for
building Models.
"""
# In the simplest case, the datamodel's registry class inherits from
# cls and the other classes that define the datamodel in a flat
# hierarchy. The registry contains the instance ``datamodel`` (on the
# left). Its class, ``DatamodelClass``, carries inferred metadata that
# is shared between all the datamodel's instances for this registry
# only.
#
# class A1(Datamodel): Datamodel
# _name = 'a' / | \
# A3 A2 A1
# class A2(Datamodel): \ | /
# _inherit = 'a' DatamodelClass
#
# class A3(Datamodel):
# _inherit = 'a'
#
# When a datamodel is extended by '_inherit', its base classes are
# modified to include the current class and the other inherited
# datamodel classes.
# Note that we actually inherit from other ``DatamodelClass``, so that
# extensions to an inherited datamodel are immediately visible in the
# current datamodel class, like in the following example:
#
# class A1(Datamodel):
# _name = 'a' Datamodel
# / / \ \
# class B1(Datamodel): / A2 A1 \
# _name = 'b' / \ / \
# B2 DatamodelA B1
# class B2(Datamodel): \ | /
# _name = 'b' \ | /
# _inherit = ['b', 'a'] \ | /
# DatamodelB
# class A2(Datamodel):
# _inherit = 'a'
# determine inherited datamodels
parents = cls._inherit
if isinstance(parents, str):
parents = [parents]
elif parents is None:
parents = []
if cls._name in registry and not parents:
raise TypeError(
"Datamodel %r (in class %r) already exists. "
"Consider using _inherit instead of _name "
"or using a different _name." % (cls._name, cls)
)
# determine the datamodel's name
name = cls._name or (len(parents) == 1 and parents[0])
if not name:
raise TypeError("Datamodel %r must have a _name" % cls)
# all datamodels except 'base' implicitly inherit from 'base'
if name != "base":
parents = list(parents) + ["base"]
# create or retrieve the datamodel's class
if name in parents:
if name not in registry:
raise TypeError("Datamodel %r does not exist in registry." % name)
# determine all the classes the datamodel should inherit from
bases = LastOrderedSet([cls])
for parent in parents:
if parent not in registry:
raise TypeError(
"Datamodel %r inherits from non-existing datamodel %r."
% (name, parent)
)
parent_class = registry[parent]
if parent == name:
for base in parent_class.__bases__:
bases.add(base)
else:
bases.add(parent_class)
parent_class._inherit_children.add(name)
if name in parents:
DatamodelClass = registry[name]
# Add the new bases to the existing model since the class into
# the registry could already be used into an inherit
DatamodelClass.__bases__ = tuple(bases)
# We must update the marshmallow schema on the existing datamodel
# class to include those inherited
parent_schemas = []
for parent in bases:
if issubclass(parent, MarshmallowModel):
parent_schemas.append(parent.__schema_class__)
schema_class = type(name + "Schema", tuple(parent_schemas), {})
DatamodelClass.__schema_class__ = schema_class
else:
attrs = {
"_name": name,
"_register": False,
# names of children datamodel
"_inherit_children": OrderedSet(),
}
if name == "base":
attrs["_registry"] = registry
DatamodelClass = type(name, tuple(bases), attrs)
setattr(DatamodelClass.__schema_class__, "_registry", registry) # noqa: B010
setattr(DatamodelClass.__schema_class__, "_datamodel_name", name) # noqa: B010
setattr( # noqa: B010
DatamodelClass.__schema_class__, "__make_object__", __make_object__
)
DatamodelClass._complete_datamodel_build()
registry[name] = DatamodelClass
return DatamodelClass
@classmethod
def _complete_datamodel_build(cls):
"""Complete build of the new datamodel class
After the datamodel 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 Datamodel, but a Datamodel can inherit
the method to add its own behavior.
"""
# makes the datamodels registry available on env
class DataModelFactory(object):
"""Factory for datamodels
This factory ensures the propagation of the environment to the
instanciated datamodels and related schema.
"""
__slots__ = ("env", "registry")
def __init__(self, env, registry):
self.env = env
self.registry = registry
def __getitem__(self, key):
model = self.registry[key]
model._env = self.env
@classmethod
def __get_schema_class__(cls, **kwargs):
cls = cls.__schema_class__(**kwargs)
cls._env = self.env
return cls
model.__get_schema_class__ = __get_schema_class__
return model
@property
def datamodels(self):
if not hasattr(self, "_datamodels_factory"):
factory = DataModelFactory(self, _datamodel_databases.get(self.cr.dbname))
self._datamodels_factory = factory
return self._datamodels_factory
Environment.datamodels = datamodels

View file

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

View file

@ -0,0 +1,15 @@
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
from ..core import Datamodel
class BaseDatamodel(Datamodel):
"""This is the base datamodel for every datamodel
It is implicitely inherited by all datamodels.
All your base are belong to us
"""
_name = "base"

View file

@ -0,0 +1,50 @@
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
"""
Fields
=====
Create a single place for all fields defined for datamodels
"""
import logging
from .core import Datamodel
_logger = logging.getLogger(__name__)
try:
from marshmallow.fields import * # noqa: F403,F401
from marshmallow.fields import Nested
except ImportError:
Nested = object
_logger.debug("Cannot import 'marshmallow'.")
class NestedModel(Nested):
def __init__(self, nested, **kwargs):
self.datamodel_name = nested
super(NestedModel, self).__init__(None, **kwargs)
@property
def schema(self):
if not self.nested:
# Get the major parent to avoid error of _env does not exist
super_parent = None
parent = self
while not super_parent:
if not hasattr(parent, "parent"):
super_parent = parent
break
parent = parent.parent
self.nested = super_parent._env.datamodels[
self.datamodel_name
].__schema_class__
self.nested._env = super_parent._env
return super(NestedModel, self).schema
def _deserialize(self, value, attr, data, **kwargs):
if isinstance(value, Datamodel):
return value
return super(NestedModel, self)._deserialize(value, attr, data, **kwargs)

View file

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

View file

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

View file

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

View file

@ -0,0 +1,4 @@
* Laurent Mignon <laurent.mignon@acsone.eu>
* `Tecnativa <https://www.tecnativa.com>`_:
* Carlos Roca

View file

@ -0,0 +1,5 @@
This addon allows you to define simple data models supporting serialization/deserialization
to/from json
Datamodels are `Marshmallow models <https://github.com/sv-tools/marshmallow-objects>`_ classes that can be inherited as Odoo
Models.

View file

@ -0,0 +1,3 @@
The `roadmap <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Adatamodel>`_
and `known issues <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Adatamodel>`_ can
be found on GitHub.

View file

@ -0,0 +1,76 @@
To define your own datamodel you just need to create a class that inherits from
``odoo.addons.datamodel.core.Datamodel``
.. code-block:: python
from marshmallow import fields
from odoo.addons.base_rest import restapi
from odoo.addons.component.core import Component
from odoo.addons.datamodel.core import Datamodel
class PartnerShortInfo(Datamodel):
_name = "partner.short.info"
id = fields.Integer(required=True, allow_none=False)
name = fields.String(required=True, allow_none=False)
class PartnerInfo(Datamodel):
_name = "partner.info"
_inherit = "partner.short.info"
street = fields.String(required=True, allow_none=False)
street2 = fields.String(required=False, allow_none=True)
zip_code = fields.String(required=True, allow_none=False)
city = fields.String(required=True, allow_none=False)
phone = fields.String(required=False, allow_none=True)
is_componay = fields.Boolean(required=False, allow_none=False)
As for odoo models, you can extend the `base` datamodel by inheriting of `base`.
.. code-block:: python
class Base(Datamodel):
_inherit = "base"
def _my_method(self):
pass
Datamodels are available through the `datamodels` registry provided by the Odoo's environment.
.. code-block:: python
class ResPartner(Model):
_inherit = "res.partner"
def _to_partner_info(self):
PartnerInfo = self.env.datamodels["partner.info"]
partner_info = PartnerInfo(partial=True)
partner_info.id = partner.id
partner_info.name = partner.name
partner_info.street = partner.street
partner_info.street2 = partner.street2
partner_info.zip_code = partner.zip
partner_info.city = partner.city
partner_info.phone = partner.phone
partner_info.is_company = partner.is_company
return partner_info
The Odoo's environment is also available into the datamodel instance.
.. code-block:: python
class MyDataModel(Datamodel):
_name = "my.data.model"
def _my_method(self):
partners = self.env["res.partner"].search([])
.. warning::
The `env` property into a Datamodel instance is mutable. IOW, you can't rely
on information (context, user) provided by the environment. The `env` property
is a helper property that give you access to the odoo's registry and must
be use with caution.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,515 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Datamodel</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="datamodel">
<h1 class="title">Datamodel</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:220702c6d930c27e2dbb4016e1f6bef6e1b9c7b96cff4b3c5ad27fdc5733ad26
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/rest-framework/tree/16.0/datamodel"><img alt="OCA/rest-framework" src="https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-datamodel"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This addon allows you to define simple data models supporting serialization/deserialization
to/from json</p>
<p>Datamodels are <a class="reference external" href="https://github.com/sv-tools/marshmallow-objects">Marshmallow models</a> classes that can be inherited as Odoo
Models.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-2">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>To define your own datamodel you just need to create a class that inherits from
<tt class="docutils literal">odoo.addons.datamodel.core.Datamodel</tt></p>
<pre class="code python literal-block">
<span class="kn">from</span><span class="w"> </span><span class="nn">marshmallow</span><span class="w"> </span><span class="kn">import</span> <span class="n">fields</span><span class="w">
</span><span class="kn">from</span><span class="w"> </span><span class="nn">odoo.addons.base_rest</span><span class="w"> </span><span class="kn">import</span> <span class="n">restapi</span><span class="w">
</span><span class="kn">from</span><span class="w"> </span><span class="nn">odoo.addons.component.core</span><span class="w"> </span><span class="kn">import</span> <span class="n">Component</span><span class="w">
</span><span class="kn">from</span><span class="w"> </span><span class="nn">odoo.addons.datamodel.core</span><span class="w"> </span><span class="kn">import</span> <span class="n">Datamodel</span><span class="w">
</span><span class="k">class</span><span class="w"> </span><span class="nc">PartnerShortInfo</span><span class="p">(</span><span class="n">Datamodel</span><span class="p">):</span><span class="w">
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s2">&quot;partner.short.info&quot;</span><span class="w">
</span> <span class="nb">id</span> <span class="o">=</span> <span class="n">fields</span><span class="o">.</span><span class="n">Integer</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">allow_none</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span> <span class="n">name</span> <span class="o">=</span> <span class="n">fields</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">allow_none</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span><span class="k">class</span><span class="w"> </span><span class="nc">PartnerInfo</span><span class="p">(</span><span class="n">Datamodel</span><span class="p">):</span><span class="w">
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s2">&quot;partner.info&quot;</span><span class="w">
</span> <span class="n">_inherit</span> <span class="o">=</span> <span class="s2">&quot;partner.short.info&quot;</span><span class="w">
</span> <span class="n">street</span> <span class="o">=</span> <span class="n">fields</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">allow_none</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span> <span class="n">street2</span> <span class="o">=</span> <span class="n">fields</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">allow_none</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span><span class="w">
</span> <span class="n">zip_code</span> <span class="o">=</span> <span class="n">fields</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">allow_none</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span> <span class="n">city</span> <span class="o">=</span> <span class="n">fields</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">allow_none</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span> <span class="n">phone</span> <span class="o">=</span> <span class="n">fields</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">allow_none</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span><span class="w">
</span> <span class="n">is_componay</span> <span class="o">=</span> <span class="n">fields</span><span class="o">.</span><span class="n">Boolean</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">allow_none</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</pre>
<p>As for odoo models, you can extend the <cite>base</cite> datamodel by inheriting of <cite>base</cite>.</p>
<pre class="code python literal-block">
<span class="k">class</span><span class="w"> </span><span class="nc">Base</span><span class="p">(</span><span class="n">Datamodel</span><span class="p">):</span><span class="w">
</span> <span class="n">_inherit</span> <span class="o">=</span> <span class="s2">&quot;base&quot;</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">_my_method</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
</span> <span class="k">pass</span>
</pre>
<p>Datamodels are available through the <cite>datamodels</cite> registry provided by the Odoos environment.</p>
<pre class="code python literal-block">
<span class="k">class</span><span class="w"> </span><span class="nc">ResPartner</span><span class="p">(</span><span class="n">Model</span><span class="p">):</span><span class="w">
</span> <span class="n">_inherit</span> <span class="o">=</span> <span class="s2">&quot;res.partner&quot;</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">_to_partner_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
</span> <span class="n">PartnerInfo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">datamodels</span><span class="p">[</span><span class="s2">&quot;partner.info&quot;</span><span class="p">]</span><span class="w">
</span> <span class="n">partner_info</span> <span class="o">=</span> <span class="n">PartnerInfo</span><span class="p">(</span><span class="n">partial</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span><span class="w">
</span> <span class="n">partner_info</span><span class="o">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">partner</span><span class="o">.</span><span class="n">id</span><span class="w">
</span> <span class="n">partner_info</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">partner</span><span class="o">.</span><span class="n">name</span><span class="w">
</span> <span class="n">partner_info</span><span class="o">.</span><span class="n">street</span> <span class="o">=</span> <span class="n">partner</span><span class="o">.</span><span class="n">street</span><span class="w">
</span> <span class="n">partner_info</span><span class="o">.</span><span class="n">street2</span> <span class="o">=</span> <span class="n">partner</span><span class="o">.</span><span class="n">street2</span><span class="w">
</span> <span class="n">partner_info</span><span class="o">.</span><span class="n">zip_code</span> <span class="o">=</span> <span class="n">partner</span><span class="o">.</span><span class="n">zip</span><span class="w">
</span> <span class="n">partner_info</span><span class="o">.</span><span class="n">city</span> <span class="o">=</span> <span class="n">partner</span><span class="o">.</span><span class="n">city</span><span class="w">
</span> <span class="n">partner_info</span><span class="o">.</span><span class="n">phone</span> <span class="o">=</span> <span class="n">partner</span><span class="o">.</span><span class="n">phone</span><span class="w">
</span> <span class="n">partner_info</span><span class="o">.</span><span class="n">is_company</span> <span class="o">=</span> <span class="n">partner</span><span class="o">.</span><span class="n">is_company</span><span class="w">
</span> <span class="k">return</span> <span class="n">partner_info</span>
</pre>
<p>The Odoos environment is also available into the datamodel instance.</p>
<pre class="code python literal-block">
<span class="k">class</span><span class="w"> </span><span class="nc">MyDataModel</span><span class="p">(</span><span class="n">Datamodel</span><span class="p">):</span><span class="w">
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s2">&quot;my.data.model&quot;</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">_my_method</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
</span> <span class="n">partners</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">&quot;res.partner&quot;</span><span class="p">]</span><span class="o">.</span><span class="n">search</span><span class="p">([])</span>
</pre>
<div class="admonition warning">
<p class="first admonition-title">Warning</p>
<p class="last">The <cite>env</cite> property into a Datamodel instance is mutable. IOW, you cant rely
on information (context, user) provided by the environment. The <cite>env</cite> property
is a helper property that give you access to the odoos registry and must
be use with caution.</p>
</div>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-2">Known issues / Roadmap</a></h1>
<p>The <a class="reference external" href="https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Adatamodel">roadmap</a>
and <a class="reference external" href="https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Adatamodel">known issues</a> can
be found on GitHub.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/rest-framework/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/rest-framework/issues/new?body=module:%20datamodel%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
<ul class="simple">
<li>ACSONE SA/NV</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<ul>
<li><p class="first">Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</p>
</li>
<li><p class="first"><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:</p>
<blockquote>
<ul class="simple">
<li>Carlos Roca</li>
</ul>
</blockquote>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/lmignon"><img alt="lmignon" src="https://github.com/lmignon.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/rest-framework/tree/16.0/datamodel">OCA/rest-framework</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View file

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

View file

@ -0,0 +1,185 @@
# Copyright 2017 Camptocamp SA
# Copyright 2019 ACSONE SA/NV
# 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 ..core import (
DatamodelRegistry,
MetaDatamodel,
_datamodel_databases,
_get_addon_name,
)
@contextmanager
def new_rollbacked_env():
registry = odoo.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 DatamodelMixin(object):
@classmethod
def setUpDatamodel(cls):
with new_rollbacked_env() as env:
builder = env["datamodel.builder"]
# build the datamodels of every installed addons
datamodel_registry = builder._init_global_registry()
cls._datamodels_registry = datamodel_registry
# ensure that we load only the datamodels of the 'installed'
# modules, not 'to install', which means we load only the
# dependencies of the tested addons, not the siblings or
# chilren addons
builder.build_registry(datamodel_registry, states=("installed",))
# build the datamodels of the current tested addon
current_addon = _get_addon_name(cls.__module__)
env["datamodel.builder"].load_datamodels(current_addon)
# pylint: disable=W8106
def setUp(self):
# should be ready only during tests, never during installation
# of addons
self._datamodels_registry.ready = True
@self.addCleanup
def notready():
self._datamodels_registry.ready = False
class TransactionDatamodelCase(common.TransactionCase, DatamodelMixin):
"""A TransactionCase that loads all the datamodels
It is used like an usual Odoo's TransactionCase, but it ensures
that all the datamodels of the current addon and its dependencies
are loaded.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.setUpDatamodel()
# pylint: disable=W8106
def setUp(self):
# resolve an inheritance issue (common.TransactionCase does not call
# super)
common.TransactionCase.setUp(self)
DatamodelMixin.setUp(self)
class DatamodelRegistryCase(
common.BaseCase, common.MetaCase("DummyCase", (object,), {})
):
"""This test case can be used as a base for writings tests on datamodels
This test case is meant to test datamodels in a special datamodel registry,
where you want to have maximum control on which datamodels are loaded
or not, or when you want to create additional datamodels in your tests.
If you only want to *use* the datamodels of the tested addon in your tests,
then consider using:
* :class:`TransactionDatamodelCase`
This test case creates a special
:class:`odoo.addons.datamodel.core.DatamodelRegistry` for the purpose of
the tests. By default, it loads all the datamodels of the dependencies, but
not the datamodels of the current addon (which you have to handle
manually). In your tests, you can add more datamodels in 2 manners.
All the datamodels of an Odoo module::
self._load_module_datamodels('connector')
Only specific datamodels::
self._build_datamodels(MyDatamodel1, MyDatamodel2)
Note: for the lookups of the datamodels, the default datamodel
registry is a global registry for the database. Here, you will
need to explicitly pass ``self.datamodel_registry`` in the
"""
def setUp(self):
super().setUp()
# 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 datamodels
self._original_datamodels = copy.deepcopy(MetaDatamodel._modules_datamodels)
# it will be our temporary datamodel registry for our test session
self.datamodel_registry = DatamodelRegistry()
# it builds the 'final datamodel' for every datamodel of the
# 'datamodel' addon and push them in the datamodel registry
self.datamodel_registry.load_datamodels("datamodel")
# build the datamodels 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(self.__module__)
registry = odoo.registry(common.get_db_name())
uid = odoo.SUPERUSER_ID
cr = registry.cursor()
env = api.Environment(cr, uid, {})
env["datamodel.builder"].build_registry(
self.datamodel_registry,
states=("installed",),
exclude_addons=[current_addon],
)
self.env = env
_datamodel_databases[self.env.cr.dbname] = self.datamodel_registry
@self.addCleanup
def _close_and_roolback():
cr.rollback() # we shouldn't have to commit anything
cr.close()
# Fake that we are ready to work with the registry
# normally, it is set to True and the end of the build
# of the datamodels. Here, we'll add datamodels later in
# the datamodels registry, but we don't mind for the tests.
self.datamodel_registry.ready = True
def tearDown(self):
super().tearDown()
# restore the original metaclass' classes
MetaDatamodel._modules_datamodels = self._original_datamodels
def _load_module_datamodels(self, module):
self.datamodel_registry.load_datamodels(module)
def _build_datamodels(self, *classes):
for cls in classes:
cls._build_datamodel(self.datamodel_registry)
class TransactionDatamodelRegistryCase(common.TransactionCase, DatamodelRegistryCase):
"""Adds Odoo Transaction in the base Datamodel TestCase"""
# pylint: disable=W8106
@classmethod
def setUpClass(cls):
# resolve an inheritance issue (common.TransactionCase does not use
# super)
common.TransactionCase.setUpClass(cls)
DatamodelRegistryCase.setUp(cls)
cls.collection = cls.env["collection.base"]
@classmethod
def tearDownClass(cls):
common.TransactionCase.tearDownClass(cls)
DatamodelRegistryCase.tearDown(cls)

View file

@ -0,0 +1,486 @@
# Copyright 2017 Camptocamp SA
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
from unittest import mock
from marshmallow_objects.models import Model as MarshmallowModel
from odoo import SUPERUSER_ID, api
from .. import fields
from ..core import Datamodel
from .common import DatamodelRegistryCase, TransactionDatamodelCase
class TestBuildDatamodel(DatamodelRegistryCase):
"""Test build of datamodels
All the tests in this suite are based on the same principle with
variations:
* Create new Datamodels (classes inheriting from
:class:`datamodel.core.Datamodel`
* Call :meth:`datamodel.core.Datamodel._build_datamodel` on them
in order to build the 'final class' composed from all the ``_inherit``
and push it in the datamodels registry (``self.datamodel_registry`` here)
* Assert that classes are built, registered, have correct ``__bases__``...
"""
def test_type(self):
"""Ensure that a datamodels are instances of
marshomallow_objects.Model"""
class Datamodel1(Datamodel):
_name = "datamodel1"
class Datamodel2(Datamodel):
_name = "datamodel2"
_inherit = "datamodel1"
self.assertIsInstance(Datamodel1(), MarshmallowModel)
self.assertIsInstance(Datamodel2(), MarshmallowModel)
def test_no_name(self):
"""Ensure that a datamodel has a _name"""
class Datamodel1(Datamodel):
pass
msg = ".*must have a _name.*"
with self.assertRaisesRegex(TypeError, msg):
Datamodel1._build_datamodel(self.datamodel_registry)
def test_register(self):
"""Able to register datamodels in datamodels registry"""
class Datamodel1(Datamodel):
_name = "datamodel1"
class Datamodel2(Datamodel):
_name = "datamodel2"
# build the 'final classes' for the datamodels and check that we find
# them in the datamodels registry
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
self.assertEqual(
["base", "datamodel1", "datamodel2"], list(self.datamodel_registry)
)
# pylint: disable=R7980
def test_inherit_bases(self):
"""Check __bases__ of Datamodel with _inherit"""
class Datamodel1(Datamodel):
_name = "datamodel1"
field_str1 = fields.String(load_default="field_str1")
class Datamodel2(Datamodel):
_inherit = "datamodel1" # pylint:disable=R8180
field_str2 = fields.String(load_default="field_str2")
class Datamodel3(Datamodel):
_inherit = "datamodel1" # pylint:disable=R8180
field_str3 = fields.String(load_default="field_str3")
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
Datamodel3._build_datamodel(self.datamodel_registry)
self.assertEqual(
(Datamodel3, Datamodel2, Datamodel1, self.env.datamodels["base"]),
self.env.datamodels["datamodel1"].__bases__,
)
def test_prototype_inherit_bases(self):
"""Check __bases__ of Datamodel with _inherit and different _name"""
class Datamodel1(Datamodel):
_name = "datamodel1"
field_int = fields.Int(load_default=1)
class Datamodel2(Datamodel):
_name = "datamodel2"
_inherit = "datamodel1"
field_boolean = fields.Boolean(load_default=True)
field_int = fields.Int(load_default=2)
class Datamodel3(Datamodel):
_name = "datamodel3"
_inherit = "datamodel1"
field_float = fields.Float(load_default=0.3)
class Datamodel4(Datamodel):
_name = "datamodel4"
_inherit = ["datamodel2", "datamodel3"]
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
Datamodel3._build_datamodel(self.datamodel_registry)
Datamodel4._build_datamodel(self.datamodel_registry)
self.assertEqual(
(Datamodel1, self.env.datamodels["base"]),
self.env.datamodels["datamodel1"].__bases__,
)
self.assertEqual(
(
Datamodel2,
self.env.datamodels["datamodel1"],
self.env.datamodels["base"],
),
self.env.datamodels["datamodel2"].__bases__,
)
self.assertEqual(
(
Datamodel3,
self.env.datamodels["datamodel1"],
self.env.datamodels["base"],
),
self.env.datamodels["datamodel3"].__bases__,
)
self.assertEqual(
(
Datamodel4,
self.env.datamodels["datamodel2"],
self.env.datamodels["datamodel3"],
self.env.datamodels["base"],
),
self.env.datamodels["datamodel4"].__bases__,
)
def test_final_class_schema(self):
"""Check the MArshmallow schema of the final class"""
class Datamodel1(Datamodel):
_name = "datamodel1"
field_int = fields.Int(load_default=1)
class Datamodel2(Datamodel):
_name = "datamodel2"
_inherit = "datamodel1"
field_boolean = fields.Boolean(load_default=True)
field_int = fields.Int(load_default=2)
class Datamodel3(Datamodel):
_name = "datamodel3"
_inherit = "datamodel1"
field_float = fields.Float(load_default=0.3)
class Datamodel4(Datamodel):
_name = "datamodel4"
_inherit = ["datamodel2", "datamodel3"]
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
Datamodel3._build_datamodel(self.datamodel_registry)
Datamodel4._build_datamodel(self.datamodel_registry)
Datamodel1 = self.env.datamodels["datamodel1"]
Datamodel2 = self.env.datamodels["datamodel2"]
Datamodel3 = self.env.datamodels["datamodel3"]
Datamodel4 = self.env.datamodels["datamodel4"]
self.assertEqual(Datamodel1().dump(), {"field_int": 1})
self.assertDictEqual(
Datamodel2().dump(), {"field_boolean": True, "field_int": 2}
)
self.assertDictEqual(Datamodel3().dump(), {"field_float": 0.3, "field_int": 1})
self.assertDictEqual(
Datamodel4().dump(),
{"field_boolean": True, "field_int": 2, "field_float": 0.3},
)
def test_custom_build(self):
"""Check that we can hook at the end of a Datamodel build"""
class Datamodel1(Datamodel):
_name = "datamodel1"
@classmethod
def _complete_datamodel_build(cls):
# This method should be called after the Datamodel
# is built, and before it is pushed in the registry
cls._build_done = True
Datamodel1._build_datamodel(self.datamodel_registry)
# we inspect that our custom build has been executed
self.assertTrue(self.env.datamodels["datamodel1"]._build_done)
# pylint: disable=W8110
def test_inherit_attrs(self):
"""Check attributes inheritance of Datamodels with _inherit"""
class Datamodel1(Datamodel):
_name = "datamodel1"
msg = "ping"
def say(self):
return "foo"
class Datamodel2(Datamodel):
_name = "datamodel2"
_inherit = "datamodel1"
msg = "pong"
def say(self):
return super(Datamodel2, self).say() + " bar"
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
# we initialize the datamodels, normally we should pass
# an instance of WorkContext, but we don't need a real one
# for this test
datamodel1 = self.env.datamodels["datamodel1"](mock.Mock())
datamodel2 = self.env.datamodels["datamodel2"](mock.Mock())
self.assertEqual("ping", datamodel1.msg)
self.assertEqual("pong", datamodel2.msg)
self.assertEqual("foo", datamodel1.say())
self.assertEqual("foo bar", datamodel2.say())
def test_duplicate_datamodel(self):
"""Check that we can't have 2 datamodels with the same name"""
class Datamodel1(Datamodel):
_name = "datamodel1"
class Datamodel2(Datamodel):
_name = "datamodel1"
Datamodel1._build_datamodel(self.datamodel_registry)
msg = "Datamodel.*already exists.*"
with self.assertRaisesRegex(TypeError, msg):
Datamodel2._build_datamodel(self.datamodel_registry)
def test_no_parent(self):
"""Ensure we can't _inherit a non-existent datamodel"""
class Datamodel1(Datamodel):
_name = "datamodel1"
_inherit = "datamodel1"
msg = "Datamodel.*does not exist in registry.*"
with self.assertRaisesRegex(TypeError, msg):
Datamodel1._build_datamodel(self.datamodel_registry)
def test_no_parent2(self):
"""Ensure we can't _inherit by prototype a non-existent datamodel"""
class Datamodel1(Datamodel):
_name = "datamodel1"
class Datamodel2(Datamodel):
_name = "datamodel2"
_inherit = ["datamodel1", "datamodel3"]
Datamodel1._build_datamodel(self.datamodel_registry)
msg = "Datamodel.*inherits from non-existing datamodel.*"
with self.assertRaisesRegex(TypeError, msg):
Datamodel2._build_datamodel(self.datamodel_registry)
def test_add_inheritance(self):
"""Ensure we can add a new inheritance"""
class Datamodel1(Datamodel):
_name = "datamodel1"
class Datamodel2(Datamodel):
_name = "datamodel2"
class Datamodel2bis(Datamodel):
_name = "datamodel2"
_inherit = ["datamodel2", "datamodel1"]
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
Datamodel2bis._build_datamodel(self.datamodel_registry)
self.assertEqual(
(
Datamodel2bis,
Datamodel2,
self.env.datamodels["datamodel1"],
self.env.datamodels.registry.get("base"),
),
self.env.datamodels["datamodel2"].__bases__,
)
def test_add_inheritance_final_schema(self):
"""Ensure that the Marshmallow schema is updated if we add a
new inheritance"""
class Datamodel1(Datamodel):
_name = "datamodel1"
field_str1 = fields.String(load_default="str1")
class Datamodel2(Datamodel):
_name = "datamodel2"
field_str2 = fields.String(load_default="str2")
class Datamodel2bis(Datamodel):
_name = "datamodel2"
_inherit = ["datamodel2", "datamodel1"]
field_str3 = fields.String(load_default="str3")
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
Datamodel2bis._build_datamodel(self.datamodel_registry)
Datamodel2 = self.env.datamodels["datamodel2"]
self.assertDictEqual(
Datamodel2().dump(),
{"field_str1": "str1", "field_str2": "str2", "field_str3": "str3"},
)
def test_recursion(self):
class Datamodel1(Datamodel):
_name = "datamodel1"
field_str = fields.String()
Datamodel1._build_datamodel(self.datamodel_registry)
for _i in range(0, 1000):
self.env.datamodels["datamodel1"](field_str="1234")
def test_nested_model(self):
"""Test nested model serialization/deserialization"""
class Parent(Datamodel):
_name = "parent"
name = fields.String()
child = fields.NestedModel("child")
class Child(Datamodel):
_name = "child"
field_str = fields.String()
Parent._build_datamodel(self.datamodel_registry)
Child._build_datamodel(self.datamodel_registry)
Parent = self.env.datamodels["parent"]
Child = self.env.datamodels["child"]
instance = Parent(name="Parent", child=Child(field_str="My other string"))
res = instance.dump()
self.assertDictEqual(
res, {"child": {"field_str": "My other string"}, "name": "Parent"}
)
new_instance = instance.load(res)
self.assertEqual(new_instance.name, instance.name)
self.assertEqual(new_instance.child.field_str, instance.child.field_str)
def test_list_nested_model(self):
"""Test list model of nested model serialization/deserialization"""
class Parent(Datamodel):
_name = "parent"
name = fields.String()
list_child = fields.List(fields.NestedModel("child"))
class Child(Datamodel):
_name = "child"
field_str = fields.String()
Parent._build_datamodel(self.datamodel_registry)
Child._build_datamodel(self.datamodel_registry)
Parent = self.env.datamodels["parent"]
Child = self.env.datamodels["child"]
childs = [
Child(field_str="My 1st other string"),
Child(field_str="My 2nd other string"),
]
instance = Parent(name="Parent", list_child=childs)
res = instance.dump()
self.assertDictEqual(
res,
{
"list_child": [
{"field_str": "My 1st other string"},
{"field_str": "My 2nd other string"},
],
"name": "Parent",
},
)
new_instance = instance.load(res)
self.assertEqual(new_instance.name, instance.name)
self.assertEqual(new_instance.list_child, instance.list_child)
def test_many(self):
"""Test loads of many"""
class Item(Datamodel):
_name = "item"
idx = fields.Integer()
Item._build_datamodel(self.datamodel_registry)
Item = self.env.datamodels["item"]
items = Item.load([{"idx": 1}, {"idx": 2}], many=True)
self.assertTrue(len(items), 2)
self.assertEqual([i.idx for i in items], [1, 2])
def test_nested_many(self):
"""Tests loads and dump of model with array of nested model"""
class Parent(Datamodel):
_name = "parent"
items = fields.NestedModel("item", many=True)
class Item(Datamodel):
_name = "item"
idx = fields.Integer()
Parent._build_datamodel(self.datamodel_registry)
Item._build_datamodel(self.datamodel_registry)
Parent = self.env.datamodels["parent"]
Item = self.env.datamodels["item"]
instance = Parent.load({"items": [{"idx": 1}, {"idx": 2}]})
res = instance.dump()
self.assertEqual(res, {"items": [{"idx": 1}, {"idx": 2}]})
new_instance = Parent.load(res)
self.assertEqual(len(new_instance.items), 2)
self.assertEqual([i.idx for i in new_instance.items], [1, 2])
new_instance.items.append(Item(idx=3))
res = new_instance.dump()
self.assertEqual(res, {"items": [{"idx": 1}, {"idx": 2}, {"idx": 3}]})
def test_env(self):
"""
Tests that the current env is always available on datamodel instances
and schema
"""
class Parent(Datamodel):
_name = "parent"
items = fields.NestedModel("item", many=True)
class Item(Datamodel):
_name = "item"
idx = fields.Integer()
Parent._build_datamodel(self.datamodel_registry)
Item._build_datamodel(self.datamodel_registry)
Parent = self.env.datamodels["parent"]
p = Parent()
self.assertEqual(p.env, self.env)
schema = Parent.get_schema()
self.assertEqual(schema._env, self.env)
instance = Parent.load({"items": [{"idx": 1}, {"idx": 2}]})
self.assertEqual(instance.items[0].env, self.env)
schema = instance.items[0].get_schema()
self.assertEqual(schema._env, self.env)
another_env = api.Environment(self.env.registry.cursor(), SUPERUSER_ID, {})
new_p = another_env.datamodels["parent"]()
self.assertEqual(new_p.env, another_env)
class TestRegistryAccess(TransactionDatamodelCase):
def test_registry_access(self):
"""Check the access to the registry directly on tnv"""
base = self.env.datamodels["base"]
self.assertIsInstance(base(), MarshmallowModel)