第十二章 Onchange方法详解

对于onchange方法,相信很多写过代码的同学已经很熟悉了,那么为什么这里要单独列出一章来讲述且放到了第三部分中?这是因为看似简单的onchange内部实际并不简单,想要真正认识清楚onchange同学还是需要费一番周折的。

回顾

我们先来回顾一下onchange的用法:


@api.onchange("some_fields")
def _onchange_some_fields(self):
    ...

一如既往的的简单,逻辑也很清晰。但是这里隐藏着一个坑,就是要求计算中的字段出现在视图中,并且不可以是只读的。如果被设置的字段不在视图中或者被设置为了只读,你会发现onchange方法第一次失效了。

为什么会这样,这就要从onchange的工作原理说起了。

内部机制

阅读源码是理解onchange本质的最佳方法,本书使用的是odoo13版本中的代码(是的,不用怀疑,onchange虽然比较底层,但是官方还是在每个版本中进行了各种修改)。

先来看onchange的定义

def onchange(*args):
    """Return a decorator to decorate an onchange method for given fields.

    In the form views where the field appears, the method will be called
    when one of the given fields is modified. The method is invoked on a
    pseudo-record that contains the values present in the form. Field
    assignments on that record are automatically sent back to the client.
    ..."""
    return attrsetter('_onchange', args)

onchange的装饰器只有一句话, 给装饰的方法设置了一个_onchange属性。那么_onchange属性的作用是什么呢?

我们来看模型中的相关源码

@property
def _onchange_methods(self):
    """ Return a dictionary mapping field names to onchange methods. """
    def is_onchange(func):
        return callable(func) and hasattr(func, '_onchange')

    # collect onchange methods on the model's class
    cls = type(self)
    methods = defaultdict(list)
    for attr, func in getmembers(cls, is_onchange):
        missing = []
        for name in func._onchange:
            if name not in cls._fields:
                missing.append(name)
            methods[name].append(func)
        if missing:
            _logger.warning(
                "@api.onchange%r parameters must be field names -> not valid: %s",
                func._onchange, missing
            )

    # add onchange methods to implement "change_default" on fields
    def onchange_default(field, self):
        value = field.convert_to_write(self[field.name], self)
        condition = "%s=%s" % (field.name, value)
        defaults = self.env['ir.default'].get_model_defaults(self._name, condition)
        self.update(defaults)

    for name, field in cls._fields.items():
        if field.change_default:
            methods[name].append(functools.partial(onchange_default, field))

    # optimization: memoize result on cls, it will not be recomputed
    cls._onchange_methods = methods
    return methods

原来在BaseModel中有一个属性叫_onchange_methods, 这个属性里存储这所有的onchange装饰字段名和对应的方法组成的字典。

当界面的字段发生改变,由WebClient通过RPC方法通知到后台时,后台检查该字段或其关联的字段是否有发生变化,如果是,则调用该方法

def _onchange_eval(self, field_name, onchange, result):
    """ Apply onchange method(s) for field ``field_name`` with spec ``onchange``
        on record ``self``. Value assignments are applied on ``self``, while
        domain and warning messages are put in dictionary ``result``.
    """
    onchange = onchange.strip()

    def process(res):
        if not res:
            return
        if res.get('value'):
            res['value'].pop('id', None)
            self.update({key: val for key, val in res['value'].items() if key in self._fields})
        if res.get('domain'):
            result.setdefault('domain', {}).update(res['domain'])
        if res.get('warning'):
            result['warnings'].add((
                res['warning'].get('title') or _("Warning"),
                res['warning'].get('message') or "",
                res['warning'].get('type') or "",
            ))

    if onchange in ("1", "true"):
        for method in self._onchange_methods.get(field_name, ()):
            method_res = method(self)
            process(method_res)
        return

从这里我们也可以看出来,onchange支持的返回结果结构,

  • 可以是None(即通常我们的赋值语句)
  • 也可以是一个value字段,其中的值将被更新到界面上。
  • 如果是返回的是个domain,那么将更新该字段的domain。
  • 如果是warning,则显示弹窗提示。
def onchange(self, values, field_name, field_onchange):
    """ Perform an onchange on the given field.
        :param values: dictionary mapping field names to values, giving the
            current state of modification
        :param field_name: name of the modified field, or list of field
            names (in view order), or False
        :param field_onchange: dictionary mapping field names to their
            on_change attribute
        When ``field_name`` is falsy, the method first adds default values
        to ``values``, computes the remaining fields, applies onchange
        methods to them, and return all the fields in ``field_onchange``.
    """

注释写得很简单,values是一个字典,包含了发生变化得字段得名字和值,field_name是发生变化的字段名或者列表,field_onchange 属性中指定了on_change方法的字段名称字典。其实内部有些坑,官方并没有在任何文档中说明。

奇怪的onchange

由于onchange的复杂实现机制,导致onchange在宏观行为上有些特立独行。

  1. 对于 One2many类型的字段而言,从onchange返回给前段的结果分析,第一条永远是(5,)。这就意味着,Odoo再更新One2many类型的字段时,并非直接更新其字段的值,而是先删除然后新增了一条。注意这里的新增,并非一定是创建,还有可能是关联。与之对应的是,Onchange再操作时,并不直接操作原有记录,而是复制了一份,这就是为什么,你在onchange方法中操作one2many字段时,打印出来的永远是一个NewId

  2. 如果你的页面布局写的有问题,比如像我一样在表单视图中将headers写到了sheet外面,页面不会有任何异常提醒,但是onchange却不能触发了。(貌似只能在sheet页面内进行?待进一步探究)

results matching ""

    No results matching ""