# 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]),