Skip to content
Snippets Groups Projects
model.py 8.68 KiB
Newer Older
##############################################################################
#
#    Converter Odoo module
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
#    Copyright © 2020, 2024 XCG Consulting <https://xcg-consulting.fr>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation, either version 3 of the
#    License, or (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
import logging
import traceback
Christophe de Vienne's avatar
Christophe de Vienne committed
from collections import OrderedDict
from collections.abc import Iterable, Mapping
from typing import Any, Final
Christophe de Vienne's avatar
Christophe de Vienne committed

Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
from odoo import _, api, models  # type: ignore[import-untyped]
from odoo.exceptions import UserError  # type: ignore[import-untyped]
Christophe de Vienne's avatar
Christophe de Vienne committed

from .base import (
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
    Context,
    ContextBuilder,
    Converter,
    Newinstance,
    NewinstanceType,
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
    PostHookConverter,
szeka.wong's avatar
szeka.wong committed
    SkipType,
    build_context,
)
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
from .validate import NotInitialized, Validation, Validator
Christophe de Vienne's avatar
Christophe de Vienne committed

try:
    from fastjsonschema import JsonSchemaException  # type: ignore[import-untyped]
except ImportError:
    # Ignore no-redef, added for compatibility
    class JsonSchemaException(Exception):  # type: ignore[no-redef]
        """Custom error in case of missing optional requirement"""


Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
_logger = logging.getLogger(__name__)

Christophe de Vienne's avatar
Christophe de Vienne committed

class IncorrectTypeException(Exception):
    """__type__ is in the message is not the same as the expected value"""


class MissingRequiredValidatorException(Exception):
    def __init__(self):
        super().__init__("Strict validation without validator")


Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
class Model(PostHookConverter):
Christophe de Vienne's avatar
Christophe de Vienne committed
    """A converter that takes a dict of key, used when a message has values"""

    def __init__(
        self,
        converters: Mapping[str, Converter],
Axel Prel's avatar
Axel Prel committed
        json_schema: str | None = None,
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        # The validator is usually not given at this point but is common
        # throughout a project. That’s why it is a property
Axel Prel's avatar
Axel Prel committed
        validator: Validator | None = None,
        merge_with: Iterable[Converter] | None = None,
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        validation: Validation = Validation.SKIP,
Axel Prel's avatar
Axel Prel committed
        context: ContextBuilder | None = None,
        datatype: str | None = None,
        __type__: str | None = None,
Christophe de Vienne's avatar
Christophe de Vienne committed
    ):
        """
        :param datatype: datatype to use. Usually used with None __type__.
        """
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        self._type: str | None = __type__
        self._converters: Mapping[str, Converter] = converters
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        self._post_hooks_converters: dict[str, PostHookConverter] = {}
Axel Prel's avatar
Axel Prel committed
        self._jsonschema: str | None = json_schema
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        self._get_instance: str | None = None
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        """First converter whose `is_instance_getter` is true if any"""
Axel Prel's avatar
Axel Prel committed
        self.merge_with: Iterable[Converter] | None = merge_with
        self.context: ContextBuilder | None = context
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        self.validator = validator
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        self.validation = validation
        self._datatype: Final[str | None] = datatype
Christophe de Vienne's avatar
Christophe de Vienne committed
        for key, converter in converters.items():
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
            if self._get_instance is None and converter.is_instance_getter:
Christophe de Vienne's avatar
Christophe de Vienne committed
                self._get_instance = key
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
            if isinstance(converter, PostHookConverter):
                self._post_hooks_converters[key] = converter
Christophe de Vienne's avatar
Christophe de Vienne committed

Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
Christophe de Vienne's avatar
Christophe de Vienne committed
        ctx = build_context(instance, ctx, self.context)

        message_data = {}
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        if self._type is not None:
Christophe de Vienne's avatar
Christophe de Vienne committed
            message_data["__type__"] = self._type

        errors = []
Christophe de Vienne's avatar
Christophe de Vienne committed
        for key in self._converters:
            try:
                value = self._converters[key].odoo_to_message(instance, ctx)
            except Exception as e:
                errors.append(
                    {"key": key, "traceback": "".join(traceback.format_exception(e))}
                )
                continue
            if not isinstance(value, SkipType):
Christophe de Vienne's avatar
Christophe de Vienne committed
                message_data[key] = value
        if len(errors) != 0:
            formatted_errors = "\n\n".join(
                [f"{error['traceback']}Key: {error['key']}" for error in errors]
            )
            raise UserError(
                _(
                    "Got unexpected errors while parsing substitutions:\n%s",
                    formatted_errors,
                )
            )
Christophe de Vienne's avatar
Christophe de Vienne committed

        if self.merge_with:
            for conv in self.merge_with:
                value = conv.odoo_to_message(instance, ctx)
                if isinstance(value, SkipType):
Christophe de Vienne's avatar
Christophe de Vienne committed
                    continue
                message_data.update(value)

Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        if self.validation != Validation.SKIP and self._jsonschema is not None:
            if self.validator:
                try:
                    self.validator.validate(self._jsonschema, message_data)
                except (NotInitialized, JsonSchemaException):
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
                    _logger.warning("Validation failed", exc_info=True)
                    if self.validation == Validation.STRICT:
                        raise
            elif self.validation == Validation.STRICT:
                raise MissingRequiredValidatorException()
Christophe de Vienne's avatar
Christophe de Vienne committed
        return message_data

    def message_to_odoo(
        self,
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        odoo_env: api.Environment,
Christophe de Vienne's avatar
Christophe de Vienne committed
        phase: str,
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        message_value: Any,
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        instance: models.BaseModel,
Christophe de Vienne's avatar
Christophe de Vienne committed
        value_present: bool = True,
szeka.wong's avatar
szeka.wong committed
    ) -> dict | SkipType:
        values: dict[str, Any] = OrderedDict()
Christophe de Vienne's avatar
Christophe de Vienne committed

        if self.validation != Validation.SKIP and self._jsonschema is not None:
            if self.validator:
                try:
                    self.validator.validate(self._jsonschema, message_value)
                except (NotInitialized, JsonSchemaException):
                    _logger.warning("Validation failed", exc_info=True)
                    if self.validation == Validation.STRICT:
                        raise
            elif self.validation == Validation.STRICT:
                raise MissingRequiredValidatorException()

Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        if self._type is not None and message_value["__type__"] != self._type:
            raise IncorrectTypeException(
                "Expected __type__ {}, found {}".format(
                    self._type, message_value["__type__"]
                )
Christophe de Vienne's avatar
Christophe de Vienne committed
            )
        for key in self._converters:
            value = message_value.get(key, None) if message_value else None
szeka.wong's avatar
szeka.wong committed
            attribute_vals = self._converters[key].message_to_odoo(
                odoo_env,
                phase,
                value,
                instance,
                message_value and key in message_value,
Christophe de Vienne's avatar
Christophe de Vienne committed
            )
szeka.wong's avatar
szeka.wong committed
            if isinstance(attribute_vals, SkipType):
                continue
            values.update(attribute_vals)
Christophe de Vienne's avatar
Christophe de Vienne committed
        if self.merge_with:
            for conv in self.merge_with:
                value = conv.message_to_odoo(
                    odoo_env, phase, message_value, instance, value_present
                )
szeka.wong's avatar
szeka.wong committed
                if isinstance(value, SkipType):
Christophe de Vienne's avatar
Christophe de Vienne committed
                    continue
                values.update(value)

        return values

Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
    @property
Christophe de Vienne's avatar
Christophe de Vienne committed
    def is_instance_getter(self) -> bool:
        return self._get_instance is not None

    def get_instance(
        self, odoo_env: api.Environment, message_data
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
    ) -> models.BaseModel | NewinstanceType | None:
Christophe de Vienne's avatar
Christophe de Vienne committed
        """:return: an instance linked to the converter, if any"""
        if self._get_instance:
            instance = self._converters[self._get_instance].get_instance(
                odoo_env, message_data[self._get_instance]
            )
            if instance is None:
                instance = Newinstance
Christophe de Vienne's avatar
Christophe de Vienne committed
            return instance
        return None

Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
    def post_hook(self, instance: models.BaseModel, message_data):
        for key in self._post_hooks_converters:
Christophe de Vienne's avatar
Christophe de Vienne committed
            if key in message_data:
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
                self._post_hooks_converters[key].post_hook(instance, message_data[key])
Christophe de Vienne's avatar
Christophe de Vienne committed
        if self.merge_with:
            for converter in self.merge_with:
                if hasattr(converter, "post_hook"):
                    converter.post_hook(instance, message_data)
Christophe de Vienne's avatar
Christophe de Vienne committed

Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        return set() if self._type is None else {self._type}

    @property
    def possible_datatypes(self) -> set[str]:
        result = set()
        if self._datatype is not None:
            result.add(self._datatype)
        return result

    def odoo_datatype(self, instance: models.BaseModel) -> str | None:
        return self._datatype