# HG changeset patch
# User Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr>
# Date 1656945183 -7200
#      Mon Jul 04 16:33:03 2022 +0200
# Branch 13.0
# Node ID 5db7fccd06c7956ae0bf11e9b578e13b47476bef
# Parent  27848c7ff17c3046694daa752ba2ffdccdfa4be9
🔨 improvements:
Change get__type__ to return a set of values.
It is defined at the Converter level despite only returning a value for a small number of Converters but it simplifies the code using it to do so that way.

Validation and validator can be set on converters after initialization. Only some converter makes use of those values.
This makes the message_to_odoo_validate method unnecessary, that’s why it has been removed.

diff --git a/NEWS.rst b/NEWS.rst
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -2,6 +2,15 @@
 History
 *******
 
+3.0.0
+=====
+
+Change get__type__ to return a set of values.
+It is defined at the Converter level despite only returning a value for a small number of Converters but it simplifies the code using it to do so that way.
+
+Validation and validator can be set on converters after initialization. Only some converter makes use of those values.
+This makes the message_to_odoo_validate method unnecessary, that’s why it has been removed.
+
 2.0.0
 =====
 
diff --git a/__init__.py b/__init__.py
--- a/__init__.py
+++ b/__init__.py
@@ -13,5 +13,11 @@
     relation,
 )
 from .switch import Switch  # noqa: F401
-from .validate import NotInitialized, Validator  # noqa: F401
+from .validate import (  # noqa: F401
+    VALIDATION_SKIP,
+    VALIDATION_SOFT,
+    VALIDATION_STRICT,
+    NotInitialized,
+    Validator,
+)
 from .xref import Xref  # noqa: F401
diff --git a/__manifest__.py b/__manifest__.py
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -17,12 +17,11 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-
 {
     "name": "Converter",
     "license": "AGPL-3",
     "summary": "Convert odoo records to/from plain data structures.",
-    "version": "13.0.2.0.0",
+    "version": "13.0.3.0.0",
     "category": "Hidden",
     "author": "XCG Consulting",
     "website": "https://odoo.consulting/",
diff --git a/base.py b/base.py
--- a/base.py
+++ b/base.py
@@ -36,10 +36,13 @@
 
 import inspect
 import logging
-from typing import Any, Callable, Dict, Mapping, Optional
+from typing import Any, Callable, Dict, Mapping, Optional, Set
 
 from odoo import api, models
 
+from .exception import InternalError
+from .validate import Validator
+
 logger = logging.getLogger(__name__)
 
 
@@ -62,6 +65,9 @@
 PHASE_POSTCREATE = "postcreate"
 PHASE_UPDATE = "UPDATE"
 
+OPERATION_CREATION = "create"
+OPERATION_UPDATE = "update"
+
 
 def build_context(
     instance: Optional[models.Model],
@@ -120,6 +126,39 @@
     def is_instance_getter(cls) -> bool:
         return False
 
+    def get__type__(self) -> Set[str]:
+        """Indicate if this converter is associated to several __type__.
+        If so, it will be called with incoming messages associated to them.
+        (using message_to_odoo)"""
+        return set()
+
+    @property
+    def validator(self) -> Optional[Validator]:
+        """A validator to use for validation of created messages"""
+        return self._validator
+
+    @validator.setter
+    def validator(self, value: Optional[Validator]) -> None:
+        if value is None:
+            self._validator = None
+        else:
+            if value.initialized:
+                self._validator = value
+            else:
+                raise InternalError(
+                    "you must initialize() the validator before passing it"
+                )
+
+    @property
+    def validation(self) -> str:
+        return self._validation
+
+    @validation.setter
+    def validation(self, value: str) -> None:
+        """Define if validation should be done"""
+        assert value is not None
+        self._validation = value
+
 
 class Readonly(Converter):
     def __init__(self, conv):
@@ -167,21 +206,40 @@
 def message_to_odoo(
     odoo_env: api.Environment,
     payload: Mapping,
-    model: models.Model,
+    model_name: str,
     converter: Converter,
+    operation: Optional[str] = None,
 ) -> models.Model:
-    instance = converter.get_instance(odoo_env, payload)
-    if not instance or instance is Newinstance:
+    """
+
+    :param odoo_env: an Odoo environment
+    :param payload: received data
+    :param model_name: name of an Odoo model
+    :param converter:
+    :param operation: if operation is not given, creation will be done if no
+       instance can be found by using
+       :py:meth:odoo.addons.Converter.get_instance
+    :return:
+    """
+    if operation == OPERATION_CREATION:
+        instance = Newinstance
+    else:
+        instance = converter.get_instance(odoo_env, payload)
+    if operation == OPERATION_CREATION or (
+        operation is None and not instance or instance is Newinstance
+    ):
         changes = converter.message_to_odoo(
             odoo_env, PHASE_PRECREATE, payload, instance
         )
-        instance = model.create(changes)
+        instance = odoo_env[model_name].create(changes)
         changes = converter.message_to_odoo(
             odoo_env, PHASE_POSTCREATE, payload, instance
         )
         if changes:
             instance.write(changes)
-    else:
+    if operation == OPERATION_UPDATE or not (
+        operation is None and not instance or instance is Newinstance
+    ):
         changes = converter.message_to_odoo(
             odoo_env, PHASE_UPDATE, payload, instance
         )
diff --git a/field.py b/field.py
--- a/field.py
+++ b/field.py
@@ -162,9 +162,9 @@
 
     def message_to_odoo(
         self,
-        odoo_env,
-        phase,
-        message_value,
+        odoo_env: api.Environment,
+        phase: str,
+        message_value: Any,
         instance: models.Model,
         value_present: bool = True,
     ) -> Dict:
diff --git a/model.py b/model.py
--- a/model.py
+++ b/model.py
@@ -1,7 +1,8 @@
+import logging
 from collections import OrderedDict
-from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union
+from typing import Any, Dict, Iterable, List, Mapping, Optional, Set, Union
 
-from odoo import _, api, exceptions, models
+from odoo import api, models
 
 from .base import (
     ContextBuilder,
@@ -11,11 +12,12 @@
     Skip,
     build_context,
 )
-from .exception import InternalError
-from .validate import Validator
+from .validate import VALIDATION_SKIP, VALIDATION_STRICT, Validator
 
 import jsonschema
 
+_logger = logging.getLogger(__name__)
+
 
 class Model(Converter):
     """A converter that takes a dict of key, used when a message has values"""
@@ -24,28 +26,25 @@
         self,
         __type__: str,
         converters: Mapping[str, Converter],
-        json_schema: Any = None,
+        json_schema: Optional[str] = None,
+        # The validator is usually not given at this point but is common
+        # throughout a project. That’s why it is a property
         validator: Optional[Validator] = None,
         merge_with: Optional[Iterable[Converter]] = None,
+        validation: str = VALIDATION_SKIP,
         context: Optional[ContextBuilder] = None,
     ):
         super().__init__()
         self._type: str = __type__
         self._converters: Mapping[str, Converter] = converters
         self._post_hooks_converters_key: List[str] = []
-        self._jsonschema: Any = json_schema
-        self._get_instance = None
-        self.merge_with = merge_with
+        self._jsonschema: Optional[str] = json_schema
+        self._get_instance: Converter = None
+        """First converter whose `is_instance_getter` is true if any"""
+        self.merge_with: Optional[Iterable[Converter]] = merge_with
         self.context: Optional[ContextBuilder] = context
-        if validator:
-            if validator.initialized:
-                self.validator = validator
-            else:
-                raise InternalError(
-                    "you must initialize() the validator before passing it"
-                )
-        else:
-            self.validator = None
+        self.validator: Optional[Validator] = validator
+        self.validation = validation
 
         for key, converter in converters.items():
             if self._get_instance is None and converter.is_instance_getter():
@@ -53,33 +52,6 @@
             if hasattr(converter, "post_hook"):
                 self._post_hooks_converters_key.append(key)
 
-    def odoo_to_message_validate(
-        self,
-        instance: models.Model,
-        synchronizer: Optional[models.Model] = None,
-        validation: bool = True,
-    ) -> Tuple[Any, Optional[str]]:
-        data = self.odoo_to_message(instance)
-
-        if validation and self._jsonschema is not None:
-            # XXX should not this be necessary when there is no validator?
-            if not synchronizer:
-                raise exceptions.UserError(
-                    _("Missing synchronizer for validation")
-                )
-
-            # XXX should this really update the value for all future calls?
-            if self.validator is None:
-                self.validator = synchronizer.get_validator()
-
-            if isinstance(self._jsonschema, str):
-                try:
-                    self.validator.validate(self._jsonschema, data)
-                except jsonschema.exceptions.ValidationError as exc:
-                    return data, str(exc)
-
-        return data, None
-
     def odoo_to_message(
         self, instance: models.Model, ctx: Optional[Dict] = None
     ) -> Any:
@@ -101,13 +73,21 @@
                     continue
                 message_data.update(value)
 
+        if self.validation != VALIDATION_SKIP and self._jsonschema is not None:
+            try:
+                self.validator.validate(self._jsonschema, message_data)
+            except jsonschema.exceptions.ValidationError as exception:
+                _logger.warning("Validation failed", exception)
+                if self.validation == VALIDATION_STRICT:
+                    raise exception
+
         return message_data
 
     def message_to_odoo(
         self,
-        odoo_env,
+        odoo_env: api.Environment,
         phase: str,
-        message_value,
+        message_value: Any,
         instance: models.Model,
         value_present: bool = True,
     ) -> Dict:
@@ -166,5 +146,5 @@
                 if hasattr(converter, "post_hook"):
                     converter.post_hook(instance, message_data)
 
-    def get__type__(self) -> str:
-        return self._type
+    def get__type__(self) -> Set[str]:
+        return {self._type}
diff --git a/switch.py b/switch.py
--- a/switch.py
+++ b/switch.py
@@ -1,11 +1,9 @@
-from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple
+from typing import Any, Callable, Dict, List, Mapping, Optional, Set, Tuple
 
-from odoo import api, exceptions, models
+from odoo import api, models
 
 from .base import ContextBuilder, Converter, Skip
-from .validate import NotInitialized, Validator
-
-import jsonschema
+from .validate import VALIDATION_SKIP, Validator
 
 
 class Switch(Converter):
@@ -40,6 +38,7 @@
             ]
         ],
         validator: Optional[Validator] = None,
+        validation: str = VALIDATION_SKIP,
         context: Optional[ContextBuilder] = None,
     ):
         """
@@ -51,15 +50,8 @@
         super()
         self._converters = converters
         self.context = context
-        if validator:
-            if validator.initialized:
-                self.validator = validator
-            else:
-                raise NotInitialized(
-                    "you must initialize() the validator before passing it"
-                )
-        else:
-            self.validator = None
+        self.validator = validator
+        self.validation = validation
 
     def odoo_to_message(
         self, instance: models.Model, ctx: Optional[Mapping] = None
@@ -99,34 +91,27 @@
 
     def get_instance(self, odoo_env, message_data):
         for _out_cond, in_cond, converter in self._converters:
-            if in_cond is None or in_cond(message_data):
+            if converter.is_instance_getter() and (
+                in_cond is None or in_cond(message_data)
+            ):
                 return converter.get_instance(odoo_env, message_data)
 
-    def odoo_to_message_validate(
-        self,
-        instance: models.Model,
-        synchronizer: Optional[models.Model] = None,
-        validation: bool = True,
-    ):
-        data = self.odoo_to_message(instance)
+    @Converter.validator.setter
+    def validator(self, value: Optional[Validator]) -> None:
+        # also set validator on any converters in our switch, in case they care
+        Converter.validator.fset(self, value)
+        for _out_cond, _in_cond, converter in self._converters:
+            converter.validator = value
 
-        for out_cond, _in_cond, converter in self._converters:
-            if out_cond is None or out_cond(instance):
-                if validation and converter._jsonschema is not None:
-                    if not synchronizer:
-                        raise exceptions.UserError(
-                            "missing synchronizer for validation"
-                        )
+    @Converter.validation.setter
+    def validation(self, value: str) -> None:
+        # also set validation on any converters in our switch
+        Converter.validation.fset(self, value)
+        for _out_cond, _in_cond, converter in self._converters:
+            converter.validation = value
 
-                    if self.validator is None:
-                        self.validator = synchronizer.get_validator()
-
-                    if isinstance(converter._jsonschema, str):
-                        try:
-                            self.validator.validate(
-                                converter._jsonschema, data
-                            )
-                        except jsonschema.exceptions.ValidationError as e:
-                            return data, str(e)
-
-                return data, None
+    def get__type__(self) -> Set[str]:
+        types = []
+        for _out_cond, _in_cond, converter in self._converters:
+            types.update(converter.get__type__())
+        return types
diff --git a/tests/test_base.py b/tests/test_base.py
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -43,10 +43,13 @@
                 "bic": Field("bic"),
             },
         )
-        model = self.env["res.bank"]
+        model_name = "res.bank"
         self.assertTrue(self.env.ref("base.bank_bnp").active)
         message_to_odoo(
-            self.env, {"ref": "bank_bnp", "active": False}, model, converter
+            self.env,
+            {"ref": "bank_bnp", "active": False},
+            model_name,
+            converter,
         )
         self.assertFalse(self.env.ref("base.bank_bnp").active)
 
@@ -58,6 +61,6 @@
                 "name": "New Bank",
                 "bic": "CBSBLT26",
             },
-            model,
+            model_name,
             converter,
         )
diff --git a/tests/test_converters.py b/tests/test_converters.py
--- a/tests/test_converters.py
+++ b/tests/test_converters.py
@@ -44,11 +44,8 @@
         user.country_id = self.env.ref("base.de")
 
         # Convert!
-        payload, error = self._build_test_converter().odoo_to_message_validate(
-            user
-        )
+        payload = self._build_test_converter().odoo_to_message(user)
         self.assertIsNot(payload, Skip)
-        self.assertIs(error, None)
 
         # Grab xrefs that got built along with the payload; checked further.
         user_ref = self.env["ir.model.data"].object_to_xmlid(user)
@@ -107,6 +104,7 @@
         Use converters of different kinds.
         """
         return Model(
+            # TODO add a schema to test validation
             "test-type",
             {
                 "id": Xref(),
diff --git a/tests/test_ir_model.py b/tests/test_ir_model.py
--- a/tests/test_ir_model.py
+++ b/tests/test_ir_model.py
@@ -17,12 +17,11 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ###############################################################################
-from odoo import tests
+from odoo.tests import TransactionCase, tagged
 
 
-@tests.common.at_install(False)
-@tests.common.post_install(True)
-class Test(tests.TransactionCase):
+@tagged("post_install", "-at_install")
+class Test(TransactionCase):
     def test_get_xmlid_present(self):
         """Ensure we get a new UUID-ish xref when a record already has an
         external ID.
diff --git a/validate.py b/validate.py
--- a/validate.py
+++ b/validate.py
@@ -7,6 +7,10 @@
 
 import jsonschema
 
+VALIDATION_SKIP = "skip"
+VALIDATION_SOFT = "soft"
+VALIDATION_STRICT = "strict"
+
 
 class NotInitialized(Exception):
     pass
@@ -42,7 +46,7 @@
             for fname in files:
                 fpath = os.path.join(root, fname)
                 if fpath.endswith((".json",)):
-                    with open(fpath, "r", self.encoding) as schema_fd:
+                    with open(fpath, "r", encoding=self.encoding) as schema_fd:
                         schema = json.load(schema_fd)
                         if "$id" in schema:
                             self.schemastore[schema["$id"]] = schema
diff --git a/xref.py b/xref.py
--- a/xref.py
+++ b/xref.py
@@ -6,6 +6,7 @@
 from .models.ir_model_data import _XREF_IMD_MODULE
 
 
+# TODO dans quel cas ça ne pourrait pas être un instance getter???
 class Xref(Converter):
     def __init__(
         self, module: str = _XREF_IMD_MODULE, is_instance_getter: bool = True