Support Function field without getter but SQL expression

Closes #4369
This commit is contained in:
Cédric Krier
2026-01-30 18:06:45 +01:00
parent 52326c4b08
commit 57aabfb14e
9 changed files with 238 additions and 10 deletions
+1
View File
@@ -1,3 +1,4 @@
* Support Function field without getter but SQL expression
* Add support for column_<field name> method
* Use tables and Model as arguments for sql_column of the Field
* Add support for basic authentication for user application
+1 -1
View File
@@ -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 <Field>`.
+3
View File
@@ -270,6 +270,9 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.message" id="msg_search_function_missing">
<field name="text">Missing search function for field "%(field)s" in "%(model)s".</field>
</record>
<record model="ir.message" id="msg_order_function_missing">
<field name="text">Missing order function for field "%(field)s" in "%(model)s".</field>
</record>
<record model="ir.message" id="msg_setter_function_missing">
<field name="text">Missing setter function for field "%(field)s" in "%(model)s".</field>
</record>
+19 -2
View File
@@ -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)
+7 -7
View File
@@ -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))
+1
View File
@@ -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
+62
View File
@@ -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')
@@ -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))
+6
View File
@@ -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