# HG changeset patch
# User Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr>
# Date 1739455665 -3600
#      Thu Feb 13 15:07:45 2025 +0100
# Branch 18.0
# Node ID 03975788e4d96350d046172325f303848f246c94
# Parent  16ef4c946a8788b84cc44a3fdb5e2c1d1dff04db
✨ Added Writeonly converter.
👕 Add some typing information, or make it consistent.
📚 Add more docstrings.
🚑 Fix using Skip in switch converter.

diff --git a/NEWS.rst b/NEWS.rst
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,6 +1,17 @@
 Changelog
 =========
 
+18.0.3.1.0
+----------
+
+Added Writeonly converter.
+
+Add some typing information, or make it consistent.
+
+Add more docstrings.
+
+Fix using Skip in switch converter.
+
 18.0.3.0.0
 ----------
 
diff --git a/__init__.py b/__init__.py
--- a/__init__.py
+++ b/__init__.py
@@ -34,6 +34,8 @@
     NewinstanceType,
     Readonly,
     Skip,
+    SkipType,
+    Writeonly,
     message_to_odoo,
     build_context,
 )
diff --git a/__manifest__.py b/__manifest__.py
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -21,7 +21,7 @@
     "name": "Converter",
     "license": "AGPL-3",
     "summary": "Convert odoo records to/from plain data structures.",
-    "version": "18.0.3.0.0",
+    "version": "18.0.3.1.0",
     "category": "Hidden",
     "author": "XCG Consulting",
     "website": "https://orbeet.io/",
diff --git a/base.py b/base.py
--- a/base.py
+++ b/base.py
@@ -1,7 +1,7 @@
 ##############################################################################
 #
 #    Converter Odoo module
-#    Copyright © 2020 XCG Consulting <https://xcg-consulting.fr>
+#    Copyright © 2020, 2025 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
@@ -189,7 +189,7 @@
 
 
 class Readonly(Converter):
-    def __init__(self, conv):
+    def __init__(self, conv: Converter):
         super().__init__()
         self.conv = conv
 
@@ -197,6 +197,35 @@
         return self.conv.odoo_to_message(instance, ctx)
 
 
+class Writeonly(Converter):
+    """A converter that only convert to odoo but does nothing from odoo."""
+
+    def __init__(self, conv: Converter):
+        super().__init__()
+        self._conv = conv
+
+    def message_to_odoo(
+        self,
+        odoo_env: api.Environment,
+        phase: str,
+        message_value: Any,
+        instance: models.BaseModel,
+        value_present: bool = True,
+    ) -> dict | SkipType:
+        return self._conv.message_to_odoo(
+            odoo_env, phase, message_value, instance, value_present
+        )
+
+    @property
+    def is_instance_getter(self) -> bool:
+        return self._conv.is_instance_getter
+
+    def get_instance(
+        self, odoo_env: api.Environment, message_data
+    ) -> models.BaseModel | NewinstanceType | None:
+        return self._conv.get_instance(odoo_env, message_data)
+
+
 class Computed(Converter):
     def __init__(self, from_odoo: Callable[[models.BaseModel, Context], Any]):
         self.from_odoo = from_odoo
@@ -216,11 +245,13 @@
 
 
 class Constant(Converter):
-    def __init__(self, value):
-        self.value = value
+    """When building messages, this converter return a constant value."""
+
+    def __init__(self, value: Any):
+        self._value = value
 
     def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
-        return self.value
+        return self._value
 
 
 def message_to_odoo(
diff --git a/field.py b/field.py
--- a/field.py
+++ b/field.py
@@ -25,7 +25,7 @@
 import pytz
 from odoo import api, models  # type: ignore[import-untyped]
 
-from .base import PHASE_POSTCREATE, Context, Converter, Newinstance, Skip
+from .base import PHASE_POSTCREATE, Context, Converter, Newinstance, Skip, SkipType
 
 
 class Field(Converter):
@@ -122,7 +122,7 @@
         message_value: Any,
         instance: models.BaseModel,
         value_present: bool = True,
-    ) -> dict:
+    ) -> dict | SkipType:
         if phase == PHASE_POSTCREATE:
             return {}
         if not value_present:
@@ -133,7 +133,7 @@
         # do not include value if already the same
         if instance and instance is not Newinstance:
             value = self.odoo_to_message(instance)
-            if value is Skip or value == message_value:
+            if isinstance(value, SkipType) or value == message_value:
                 return {}
         if self._odoo_formatter:
             message_value = self._odoo_formatter(message_value)
@@ -187,11 +187,11 @@
         message_value: Any,
         instance: models.BaseModel,
         value_present: bool = True,
-    ) -> dict:
+    ) -> dict | SkipType:
         message = super().message_to_odoo(
             odoo_env, phase, message_value, instance, value_present
         )
-        if self.field_name in message:
+        if not isinstance(message, SkipType) and self.field_name in message:
             self._lazy_dicts(instance)
             message[self.field_name] = self._lazy_dict_message_to_odoo.get(
                 message[self.field_name]
diff --git a/list.py b/list.py
--- a/list.py
+++ b/list.py
@@ -20,9 +20,9 @@
 
 from typing import Any
 
-from odoo import models  # type: ignore[import-untyped]
+from odoo import api, models  # type: ignore[import-untyped]
 
-from .base import Context, ContextBuilder, Converter, Skip, build_context
+from .base import Context, ContextBuilder, Converter, SkipType, build_context
 
 
 class List(Converter):
@@ -44,17 +44,24 @@
 
         for converter in self._converters:
             value = converter.odoo_to_message(instance, ctx)
-            if value is not Skip:
+            if not isinstance(value, SkipType):
                 message_data.append(value)
 
         return message_data
 
     def message_to_odoo(
         self,
-        odoo_env,
+        odoo_env: api.Environment,
         phase: str,
-        message_value,
+        message_value: Any,
         instance: models.BaseModel,
         value_present: bool = True,
-    ) -> dict:
-        return {}
+    ) -> dict | SkipType:
+        result = {}
+        for i, converter in enumerate(self._converters):
+            new_values = converter.message_to_odoo(
+                odoo_env, phase, message_value[i], instance, value_present
+            )
+            if not isinstance(new_values, SkipType):
+                result.update(new_values)
+        return result
diff --git a/mail_template.py b/mail_template.py
--- a/mail_template.py
+++ b/mail_template.py
@@ -28,7 +28,7 @@
 class MailTemplate(Converter):
     """This converter wraps ``mail.template::_render_template``.
     Multiple records are allowed but ``mail.template::_render_template`` still
-    runs once per record; to accomodate, we provide ``ctx["records"]``.
+    runs once per record; to accommodate, we provide ``ctx["records"]``.
 
     Using this converter requires the mail module to be installed.
     """
diff --git a/model.py b/model.py
--- a/model.py
+++ b/model.py
@@ -34,7 +34,6 @@
     Newinstance,
     NewinstanceType,
     PostHookConverter,
-    Skip,
     SkipType,
     build_context,
 )
@@ -92,7 +91,7 @@
                     {"key": key, "traceback": "".join(traceback.format_exception(e))}
                 )
                 continue
-            if value is not Skip:
+            if not isinstance(value, SkipType):
                 message_data[key] = value
         if len(errors) != 0:
             formatted_errors = "\n\n".join(
@@ -108,7 +107,7 @@
         if self.merge_with:
             for conv in self.merge_with:
                 value = conv.odoo_to_message(instance, ctx)
-                if value is Skip:
+                if isinstance(value, SkipType):
                     continue
                 message_data.update(value)
 
diff --git a/relation.py b/relation.py
--- a/relation.py
+++ b/relation.py
@@ -29,6 +29,7 @@
     Converter,
     NewinstanceType,
     Skip,
+    SkipType,
     build_context,
 )
 from .field import Field
@@ -69,7 +70,7 @@
         message_value: Any,
         instance: models.BaseModel,
         value_present: bool = True,
-    ) -> dict:
+    ) -> dict | SkipType:
         if not value_present:
             return {}
 
@@ -107,8 +108,8 @@
     def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         ctx = build_context(instance, ctx, self.context)
         value = super().odoo_to_message(instance, ctx)
-        if value is Skip:
-            return Skip
+        if isinstance(value, SkipType):
+            return value
         if self.filtered:
             value = value.filtered(self.filtered)
         if self.sortkey:
@@ -119,7 +120,7 @@
         return [
             m
             for m in (self.converter.odoo_to_message(r, ctx) for r in value)
-            if m is not Skip
+            if not isinstance(m, SkipType)
         ]
 
     def message_to_odoo(
@@ -129,7 +130,7 @@
         message_value: Any,
         instance: models.BaseModel,
         value_present: bool = True,
-    ) -> dict:
+    ) -> dict | SkipType:
         # if not present or value is None, do not update the values.
         if not value_present or message_value is None:
             return {}
@@ -173,8 +174,8 @@
     def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         ctx = build_context(instance, ctx, self.context)
         value = super().odoo_to_message(instance, ctx)
-        if value is Skip:
-            return Skip
+        if isinstance(value, SkipType):
+            return value
         if self.filtered:
             value = value.filtered(self.filtered)
         return {
@@ -186,7 +187,7 @@
                 )
                 for r in value
             )
-            if k is not Skip and v is not Skip
+            if not isinstance(k, SkipType) and not isinstance(v, SkipType)
         }
 
     def message_to_odoo(
@@ -196,7 +197,7 @@
         message_value: Any,
         instance: models.BaseModel,
         value_present: bool = True,
-    ) -> dict:
+    ) -> dict | SkipType:
         # if not present or value is None, do not update the values.
         if not value_present or message_value is None:
             return {}
diff --git a/switch.py b/switch.py
--- a/switch.py
+++ b/switch.py
@@ -39,8 +39,8 @@
         AURION_REFERENTIAL: Switch(
             [
               (
-                  lambda e: e.is_xxx,
-                  lambda p: "wave_code" in p,
+                  lambda record: record.is_xxx,
+                  lambda message_value: "wave_code" in message_value,
                   Model("__wave__", {}),
               ),
               (None, None, Model("__event__", {})),
@@ -53,8 +53,8 @@
         converters: list[
             tuple[
                 Callable[[models.BaseModel], bool] | None,
-                Callable[[Any], bool],
-                Converter,
+                Callable[[Any], bool] | None,
+                Converter | SkipType,
             ]
         ],
         validator: Validator | None = None,
@@ -92,7 +92,9 @@
         value_present: bool = True,
     ) -> dict | SkipType:
         for _out_cond, in_cond, converter in self._converters:
-            if in_cond is None or in_cond(message_value):
+            if not isinstance(converter, SkipType) and (
+                in_cond is None or in_cond(message_value)
+            ):
                 return converter.message_to_odoo(
                     odoo_env,
                     phase,
@@ -106,7 +108,7 @@
     @property
     def is_instance_getter(self) -> bool:
         for _out_cond, _in_cond, converter in self._converters:
-            if converter.is_instance_getter:
+            if not isinstance(converter, SkipType) and converter.is_instance_getter:
                 return True
 
         return False
@@ -115,8 +117,10 @@
         self, odoo_env: api.Environment, message_data
     ) -> models.BaseModel | NewinstanceType | None:
         for _out_cond, in_cond, converter in self._converters:
-            if converter.is_instance_getter and (
-                in_cond is None or in_cond(message_data)
+            if (
+                not isinstance(converter, SkipType)
+                and converter.is_instance_getter
+                and (in_cond is None or in_cond(message_data))
             ):
                 return converter.get_instance(odoo_env, message_data)
         return super().get_instance(odoo_env, message_data)
@@ -125,16 +129,19 @@
         # also set validator on any converters in our switch, in case they care
         super()._set_validator(value)
         for _out_cond, _in_cond, converter in self._converters:
-            converter.validator = value
+            if not isinstance(converter, SkipType):
+                converter.validator = value
 
     def _set_validation(self, value: str) -> None:
         # also set validation on any converters in our switch
         super()._set_validation(value)
         for _out_cond, _in_cond, converter in self._converters:
-            converter.validation = value
+            if not isinstance(converter, SkipType):
+                converter.validation = value
 
     def get__type__(self) -> set[str]:
         types = set()
         for _out_cond, _in_cond, converter in self._converters:
-            types.update(converter.get__type__())
+            if not isinstance(converter, SkipType):
+                types.update(converter.get__type__())
         return types
diff --git a/xref.py b/xref.py
--- a/xref.py
+++ b/xref.py
@@ -20,7 +20,7 @@
 
 from typing import Any
 
-from odoo import models  # type: ignore[import-untyped]
+from odoo import api, models  # type: ignore[import-untyped]
 
 from .base import Context, NewinstanceType, PostHookConverter
 from .models.ir_model_data import _XREF_IMD_MODULE
@@ -47,8 +47,8 @@
         )
 
     def get_instance(
-        self, odoo_env, message_data
-    ) -> None | models.BaseModel | NewinstanceType:
+        self, odoo_env: api.Environment, message_data
+    ) -> models.BaseModel | NewinstanceType | None:
         if self._is_instance_getter:
             return odoo_env.ref(
                 ".".join(["" if self._module is None else self._module, message_data]),