diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 0150482eca..e8cda6a203 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Support Function field without getter but SQL expression * Add support for column_ method * Use tables and Model as arguments for sql_column of the Field * Add support for basic authentication for user application diff --git a/trytond/doc/ref/fields.rst b/trytond/doc/ref/fields.rst index 675b34b8e0..9b5ea5d83b 100644 --- a/trytond/doc/ref/fields.rst +++ b/trytond/doc/ref/fields.rst @@ -1081,7 +1081,7 @@ Instance methods: Function -------- -.. class:: Function(field, getter[, setter[, searcher[, getter_with_context]]]) +.. class:: Function(field, [getter[, setter[, searcher[, getter_with_context]]]]) A function field can emulate any other given :class:`field `. diff --git a/trytond/trytond/ir/message.xml b/trytond/trytond/ir/message.xml index 8d0eb3a38f..3f9fa48b3f 100644 --- a/trytond/trytond/ir/message.xml +++ b/trytond/trytond/ir/message.xml @@ -270,6 +270,9 @@ this repository contains the full copyright notices and license terms. --> Missing search function for field "%(field)s" in "%(model)s". + + Missing order function for field "%(field)s" in "%(model)s". + Missing setter function for field "%(field)s" in "%(model)s". diff --git a/trytond/trytond/model/fields/function.py b/trytond/trytond/model/fields/function.py index 78dc7f4f6a..6b7d08dc28 100644 --- a/trytond/trytond/model/fields/function.py +++ b/trytond/trytond/model/fields/function.py @@ -30,7 +30,7 @@ def getter_context(func): class Function(Field): - def __init__(self, field, getter, setter=None, searcher=None, + def __init__(self, field, getter=None, setter=None, searcher=None, getter_with_context=True, loading='lazy'): ''' :param field: The field of the function. @@ -88,19 +88,27 @@ class Function(Field): return self._field.sql_format(value) def sql_type(self): + if not self.getter: + return self._field.sql_type() return None @domain_method def convert_domain(self, domain, tables, Model): if self.searcher: return getattr(Model, self.searcher)(self.name, domain) + elif not self.getter: + return self._field.convert_domain(domain, tables, Model) raise NotImplementedError(gettext( 'ir.msg_search_function_missing', **Model.__names__(self.name))) @order_method def convert_order(self, name, tables, Model): - raise NotImplementedError + if not self.getter: + return self._field.convert_order(name, tables, Model) + raise NotImplementedError(gettext( + 'ir.msg_order_function_missing', + **Model.__names__(self.name))) @getter_context @without_check_access @@ -110,6 +118,15 @@ class Function(Field): If the function has ``names`` in the function definition then it will call it with a list of name. ''' + if not self.getter: + def get_values(name): + return {r['id']: r[name] for r in values} + if isinstance(name, list): + names = name + return {name: get_values(name) for name in names} + else: + return get_values(name) + method = getattr(Model, self.getter) instance_method = is_instance_method(Model, self.getter) multiple = self.getter_multiple(method) diff --git a/trytond/trytond/model/modelsql.py b/trytond/trytond/model/modelsql.py index 754fb6f65c..26ffbf5199 100644 --- a/trytond/trytond/model/modelsql.py +++ b/trytond/trytond/model/modelsql.py @@ -493,7 +493,7 @@ class ModelSQL(ModelStorage): if field_name == 'id': continue sql_type = field.sql_type() - if not sql_type: + if not sql_type or isinstance(field, fields.Function): continue if field_name in cls._defaults: @@ -610,7 +610,7 @@ class ModelSQL(ModelStorage): if cls._history: history_table = cls.__table_handler__(history=True) for field_name, field in cls._fields.items(): - if not field.sql_type(): + if not field.sql_type() or isinstance(field, fields.Function): continue history_table.add_column(field_name, field._sql_type) @@ -782,15 +782,15 @@ class ModelSQL(ModelStorage): columns = [] hcolumns = [] if not deleted: - fields = cls._fields + fields_ = cls._fields else: - fields = { + fields_ = { 'id': cls.id, 'write_uid': cls.write_uid, 'write_date': cls.write_date, } - for fname, field in sorted(fields.items()): - if not field.sql_type(): + for fname, field in sorted(fields_.items()): + if not field.sql_type() or isinstance(field, fields.Function): continue columns.append(Column(table, fname)) hcolumns.append(Column(history, fname)) @@ -834,7 +834,7 @@ class ModelSQL(ModelStorage): hcolumns = [] history_columns = [] fnames = sorted(n for n, f in cls._fields.items() - if f.sql_type()) + if f.sql_type() and not isinstance(f, fields.Function)) id_idx = fnames.index('id') for fname in fnames: columns.append(Column(table, fname)) diff --git a/trytond/trytond/model/modelstorage.py b/trytond/trytond/model/modelstorage.py index 4f09f86205..be3cc37bc7 100644 --- a/trytond/trytond/model/modelstorage.py +++ b/trytond/trytond/model/modelstorage.py @@ -1885,6 +1885,7 @@ class ModelStorage(Model): multiple_getter = None if (field.loading == 'lazy' and isinstance(field, fields.Function) + and field.getter and field.getter_multiple( getattr(self.__class__, field.getter))): multiple_getter = field.getter diff --git a/trytond/trytond/tests/field_function.py b/trytond/trytond/tests/field_function.py index 3253810444..0fca4741fe 100644 --- a/trytond/trytond/tests/field_function.py +++ b/trytond/trytond/tests/field_function.py @@ -1,6 +1,8 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. +from sql import Literal + from trytond.model import ModelSQL, ModelStorage, fields from trytond.pool import Pool from trytond.transaction import Transaction @@ -107,6 +109,63 @@ class FunctionGetterLocalCache(ModelSQL): return index +class FunctionNoGetter(ModelSQL): + __name__ = 'test.function.no_getter' + + value = fields.Integer("Value") + value_inc = fields.Function(fields.Integer("Value Inc")) + + @classmethod + def column_value_inc(cls, tables): + table, _ = tables[None] + return table.value + Literal(1) + + +class FunctionNoGetterRelation(ModelSQL): + __name__ = 'test.function.no_getter.relation' + + target = fields.Many2One( + 'test.function.no_getter.target', + "Target") + target_name = fields.Function(fields.Char("Target Name")) + target_target = fields.Function( + fields.Many2One('test.function.no_getter.target', "Target Target")) + + @classmethod + def column_target_name(cls, tables): + pool = Pool() + Target = pool.get('test.function.no_getter.target') + table, _ = tables[None] + if 'target' not in tables: + target = Target.__table__() + tables['target'] = { + None: (target, table.target == target.id), + } + else: + target, _ = tables['target'][None] + return target.name + + @classmethod + def column_target_target(cls, tables): + pool = Pool() + Target = pool.get('test.function.no_getter.target') + table, _ = tables[None] + if 'target' not in tables: + target = Target.__table__() + tables['target'] = { + None: (target, table.target == target.id), + } + else: + target, _ = tables['target'][None] + return target.id + + +class FunctionNoGetterTarget(ModelSQL): + __name__ = 'test.function.no_getter.target' + + name = fields.Char("Name") + + def register(module): Pool.register( FunctionDefinition, @@ -115,4 +174,7 @@ def register(module): FunctonGetter, FunctionGetterContext, FunctionGetterLocalCache, + FunctionNoGetter, + FunctionNoGetterRelation, + FunctionNoGetterTarget, module=module, type_='model') diff --git a/trytond/trytond/tests/test_field_function.py b/trytond/trytond/tests/test_field_function.py index 09c3535065..fb1e37888e 100644 --- a/trytond/trytond/tests/test_field_function.py +++ b/trytond/trytond/tests/test_field_function.py @@ -125,3 +125,141 @@ class FieldFunctionTestCase(TestCase): Model.read([record.id], ['function1', 'function2']) self.assertEqual(getter.call_count, 1) + + @with_transaction() + def test_no_getter(self): + "Test no getter" + pool = Pool() + Model = pool.get('test.function.no_getter') + + record = Model(value=42) + record.save() + + self.assertEqual(record.value_inc, 43) + + @with_transaction() + def test_no_getter_no_column(self): + "Test no column without getter" + pool = Pool() + Model = pool.get('test.function.no_getter') + + table_handler = Model.__table_handler__() + + self.assertFalse(table_handler.column_exist('value_inc')) + + @with_transaction() + def test_no_getter_search(self): + "Test search without getter" + pool = Pool() + Model = pool.get('test.function.no_getter') + + record = Model(value=42) + record.save() + + result = Model.search([('value_inc', '=', 43)]) + self.assertEqual(result, [record]) + + @with_transaction() + def test_no_getter_order(self): + "Test order without getter" + pool = Pool() + Model = pool.get('test.function.no_getter') + + Model.create([{'value': i} for i in range(10)]) + + asc = Model.search([], order=[('value_inc', 'ASC')]) + asc = [r.value_inc for r in asc] + desc = Model.search([], order=[('value_inc', 'DESC')]) + desc = [r.value_inc for r in desc] + + self.assertEqual(asc, sorted(asc)) + self.assertEqual(desc, sorted(asc, reverse=True)) + + @with_transaction() + def test_no_getter_relation(self): + "Test no getter with relation" + pool = Pool() + Model = pool.get('test.function.no_getter.relation') + Target = pool.get('test.function.no_getter.target') + + target = Target(name="Test") + target.save() + record = Model(target=target) + record.save() + + self.assertEqual(record.target_name, "Test") + self.assertEqual(record.target_target, target) + + @with_transaction() + def test_no_getter_search_relation(self): + "Test search on relation without getter" + pool = Pool() + Model = pool.get('test.function.no_getter.relation') + Target = pool.get('test.function.no_getter.target') + + target = Target(name="Test") + target.save() + record = Model(target=target) + record.save() + + result = Model.search([('target_name', '=', "Test")]) + + self.assertEqual(result, [record]) + + @with_transaction() + def test_no_getter_search_relation_dotted(self): + "Test search on dotted relation without getter" + pool = Pool() + Model = pool.get('test.function.no_getter.relation') + Target = pool.get('test.function.no_getter.target') + + target = Target(name="Test") + target.save() + record = Model(target=target) + record.save() + + result = Model.search([('target_target.name', '=', "Test")]) + + self.assertEqual(result, [record]) + + @with_transaction() + def test_no_getter_order_relation(self): + "Test order on relation without getter" + pool = Pool() + Model = pool.get('test.function.no_getter.relation') + Target = pool.get('test.function.no_getter.target') + + for i in range(10): + target = Target(name=str(i)) + target.save() + record = Model(target=target) + record.save() + + asc = Model.search([], order=[('target_name', 'ASC')]) + asc = [r.target_name for r in asc] + desc = Model.search([], order=[('target_name', 'DESC')]) + desc = [r.target_name for r in desc] + + self.assertEqual(asc, sorted(asc)) + self.assertEqual(desc, sorted(asc, reverse=True)) + + @with_transaction() + def test_no_getter_order_relation_dotted(self): + "Test order on dotted relation without getter" + pool = Pool() + Model = pool.get('test.function.no_getter.relation') + Target = pool.get('test.function.no_getter.target') + + for i in range(10): + target = Target(name=str(i)) + target.save() + record = Model(target=target) + record.save() + + asc = Model.search([], order=[('target_target.name', 'ASC')]) + asc = [r.target_target.name for r in asc] + desc = Model.search([], order=[('target_target.name', 'DESC')]) + desc = [r.target_target.name for r in desc] + + self.assertEqual(asc, sorted(asc)) + self.assertEqual(desc, sorted(asc, reverse=True)) diff --git a/trytond/trytond/tests/test_tryton.py b/trytond/trytond/tests/test_tryton.py index 445033fc21..3abe8c59c8 100644 --- a/trytond/trytond/tests/test_tryton.py +++ b/trytond/trytond/tests/test_tryton.py @@ -972,6 +972,12 @@ class ModuleTestCase(_DBTestCase): 'field': field_name, 'type': field._type, }) + if not field.getter and isinstance(model, ModelSQL): + func_name = f'column_{field_name}' + self.assertTrue( + getattr(model, func_name, None), + msg=f"Missing method {func_name!r} " + f"on model {mname!r} for field {field_name!r}") for func_name in [field.getter, field.setter, field.searcher]: if not func_name: continue