From bc5a28a50c5692774f907d4e7515e213a0ab9874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 16 Aug 2023 16:11:51 +0200 Subject: [PATCH] Use _changed_values to return result of on_change_with This allows to support add/update/remove/delete for xxx2many fields and rec_name for xxx2one and reference fields. Closes #12473 --- sao/src/model.js | 30 +++++++++------- .../gui/window/view_form/model/record.py | 28 +++++++-------- trytond/CHANGELOG | 1 + trytond/trytond/model/fields/field.py | 32 ++++++++--------- trytond/trytond/model/fields/function.py | 36 ++++++++++++++++--- trytond/trytond/model/modelview.py | 34 +++++------------- trytond/trytond/tests/test_field_function.py | 2 +- 7 files changed, 88 insertions(+), 75 deletions(-) diff --git a/sao/src/model.js b/sao/src/model.js index b55571c5f3..fa5f8fa64a 100644 --- a/sao/src/model.js +++ b/sao/src/model.js @@ -1129,27 +1129,29 @@ delete this._values[fieldname + '.']; } } - var result; + var changed; fieldnames = Object.keys(fieldnames); if (fieldnames.length) { try { if ((fieldnames.length == 1) || (values.id === undefined)) { - result = {}; + changed = {}; for (const fieldname of fieldnames) { - result[fieldname] = this.model.execute( - 'on_change_with_' + fieldname, - [values], this.get_context(), false); + changed = jQuery.extend( + changed, + this.model.execute( + 'on_change_with_' + fieldname, + [values], this.get_context(), false)); } } else { - result = this.model.execute( + changed = this.model.execute( 'on_change_with', [values, fieldnames], this.get_context(), false); } } catch (e) { return; } - this.set_on_change(result); + this.set_on_change(changed); } if (!jQuery.isEmptyObject(later)) { values = {}; @@ -1164,21 +1166,23 @@ try { if ((fieldnames.length == 1) || (values.id === undefined)) { - result = {}; + changed = {}; for (const fieldname of fieldnames) { - result[fieldname] = this.model.execute( - 'on_change_with_' + fieldname, - [values], this.get_context(), false); + changed = jQuery.extend( + changed, + this.model.execute( + 'on_change_with_' + fieldname, + [values], this.get_context(), false)); } } else { - result = this.model.execute( + changed = this.model.execute( 'on_change_with', [values, fieldnames], this.get_context(), false); } } catch (e) { return; } - this.set_on_change(result); + this.set_on_change(changed); } }, set_on_change: function(values) { diff --git a/tryton/tryton/gui/window/view_form/model/record.py b/tryton/tryton/gui/window/view_form/model/record.py index e898b454c2..7d6c364f0e 100644 --- a/tryton/tryton/gui/window/view_form/model/record.py +++ b/tryton/tryton/gui/window/view_form/model/record.py @@ -621,19 +621,19 @@ class Record: if fieldnames: try: if len(fieldnames) == 1 or 'id' not in values: - result = {} + changed = {} for fieldname in fieldnames: - result[fieldname] = RPCExecute( - 'model', self.model_name, - 'on_change_with_' + fieldname, - values, context=self.get_context()) + changed.update(RPCExecute( + 'model', self.model_name, + 'on_change_with_' + fieldname, + values, context=self.get_context())) else: - result = RPCExecute( + changed = RPCExecute( 'model', self.model_name, 'on_change_with', values, list(fieldnames), context=self.get_context()) except RPCException: return - self.set_on_change(result) + self.set_on_change(changed) if later: values = {} for fieldname in later: @@ -642,19 +642,19 @@ class Record: values.update(self._get_on_change_args(on_change_with)) try: if len(later) == 1 or 'id' not in values: - result = {} + changed = {} for fieldname in fieldnames: - result[fieldname] = RPCExecute( - 'model', self.model_name, - 'on_change_with_' + fieldname, - values, context=self.get_context()) + changed.update(RPCExecute( + 'model', self.model_name, + 'on_change_with_' + fieldname, + values, context=self.get_context())) else: - result = RPCExecute( + changed = RPCExecute( 'model', self.model_name, 'on_change_with', values, list(later), context=self.get_context()) except RPCException: return - self.set_on_change(result) + self.set_on_change(changed) def autocomplete_with(self, field_name): for fieldname, fieldinfo in self.group.fields.items(): diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 4e667c0f1a..9bd22613a9 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Support add/update/remove/delete for on_change_with of xxx2Many * Add decorator on RPC * Remove translations with empty source * Run doc tests with different globals diff --git a/trytond/trytond/model/fields/field.py b/trytond/trytond/model/fields/field.py index db08e41914..0549f0a4d1 100644 --- a/trytond/trytond/model/fields/field.py +++ b/trytond/trytond/model/fields/field.py @@ -1,7 +1,7 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import warnings -from functools import partial, wraps +from functools import wraps import sql from sql import ( @@ -195,18 +195,15 @@ def on_change_result(record): return record._changed_values -def on_change_with_result(field, value): - from ..modelstorage import ModelStorage - if field._type in {'many2one', 'one2one', 'reference'}: - if isinstance(value, ModelStorage): - if field._type == 'reference': - value = str(value) - else: - value = value.id - elif field._type in {'one2many', 'many2many'}: - if isinstance(value, (list, tuple)): - value = [int(r) for r in value] - return value +def on_change_with_result(fieldname): + def decorator(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + value = func(self, *args, **kwargs) + setattr(self, fieldname, value) + return self._changed_values + return wrapper + return decorator def domain_method(func): @@ -471,9 +468,9 @@ class Field(object): return [self.sql_column(table)] def set_rpc(self, model): - for attribute, result in ( - ('on_change', on_change_result), - ('on_change_with', partial(on_change_with_result, self)), + for attribute, decorator, result in ( + ('on_change', None, on_change_result), + ('on_change_with', on_change_with_result(self.name), None), ): if not getattr(self, attribute): continue @@ -481,7 +478,8 @@ class Field(object): assert hasattr(model, func_name), \ 'Missing %s on model %s' % (func_name, model.__name__) model.__rpc__.setdefault( - func_name, RPC(instantiate=0, result=result)) + func_name, + RPC(instantiate=0, decorator=decorator, result=result)) def definition(self, model, language): pool = Pool() diff --git a/trytond/trytond/model/fields/function.py b/trytond/trytond/model/fields/function.py index e909b30cfd..eba08bd669 100644 --- a/trytond/trytond/model/fields/function.py +++ b/trytond/trytond/model/fields/function.py @@ -9,7 +9,7 @@ from trytond.i18n import gettext from trytond.tools import is_instance_method from trytond.transaction import Transaction, without_check_access -from .field import Field, domain_method, on_change_with_result +from .field import Field, domain_method def getter_context(func): @@ -124,11 +124,37 @@ class Function(Field): def call(name): if not instance_method: - return on_change_with_result(self, method(records, name)) + values = method(records, name) + if isinstance(name, str): + return convert_dict(values) + else: + return {k: convert_dict(v, k) for k, v in values.items()} else: - return { - r.id: on_change_with_result(self, method(r, name)) - for r in records} + return {r.id: convert(method(r, name)) for r in records} + + def convert(value, name=None): + from ..model import Model as BaseModel + if name: + field = Model._fields[name]._field + else: + field = self._field + if field._type in {'many2one', 'one2one', 'reference'}: + if isinstance(value, BaseModel): + if field._type == 'reference': + value = str(value) + else: + value = int(value) + elif field._type in {'one2many', 'many2many'}: + if value: + value = [int(r) for r in value] + return value + + def convert_dict(values, name=None): + # Keep the same class + values = values.copy() + values.update((k, convert(v, name)) for k, v in values.items()) + return values + if isinstance(name, list): names = name if multiple: diff --git a/trytond/trytond/model/modelview.py b/trytond/trytond/model/modelview.py index 5c3ee8ea7a..4a22890341 100644 --- a/trytond/trytond/model/modelview.py +++ b/trytond/trytond/model/modelview.py @@ -69,7 +69,8 @@ class ModelView(Model): cls.__rpc__['fields_view_get'] = RPC(cache=dict(days=1)) cls.__rpc__['view_toolbar_get'] = RPC(cache=dict(days=1)) cls.__rpc__['on_change'] = RPC(instantiate=0, result=on_change_result) - cls.__rpc__['on_change_with'] = RPC(instantiate=0) + cls.__rpc__['on_change_with'] = RPC( + instantiate=0, result=on_change_result) cls.__rpc__['on_change_notify'] = RPC(instantiate=0) cls._buttons = {} @@ -758,6 +759,13 @@ class ModelView(Model): return func return decorator + @on_change + def on_change_with(self, fieldnames): + for fieldname in fieldnames: + method_name = 'on_change_with_%s' % fieldname + value = getattr(self, method_name)() + setattr(self, fieldname, value) + @on_change def on_change(self, fieldnames): for fieldname in sorted(fieldnames): @@ -765,30 +773,6 @@ class ModelView(Model): if method: method() - def on_change_with(self, fieldnames): - from .modelstorage import ModelStorage - changes = {} - for fieldname in fieldnames: - field = self._fields[fieldname] - method_name = 'on_change_with_%s' % fieldname - value = getattr(self, method_name)() - setattr(self, fieldname, value) - if field._type in {'many2one', 'one2one', 'reference'}: - if isinstance(value, ModelStorage): - if value.id and value.id >= 0: - changes[f'%{fieldname}.'] = { - 'rec_name': value.rec_name, - } - if field._type == 'reference': - value = str(value) - else: - value = value.id - elif field._type in {'one2many', 'many2many'}: - if isinstance(value, (list, tuple)): - value = [int(r) for r in value] - changes[fieldname] = value - return changes - def on_change_notify(self): """Return a list of type and message couples. Available types are info, warning and error. diff --git a/trytond/trytond/tests/test_field_function.py b/trytond/trytond/tests/test_field_function.py index 6f53f12e3a..f57921fc49 100644 --- a/trytond/trytond/tests/test_field_function.py +++ b/trytond/trytond/tests/test_field_function.py @@ -103,7 +103,7 @@ class FieldFunctionTestCase(unittest.TestCase): record = Model() record.save() - with patch.object(Model, 'get_function1') as getter: + with patch.object(Model, 'get_function1', autospec=True) as getter: getter.return_value = 'test' Model.read([record.id], ['function1', 'function2'])