diff --git a/CHANGELOG b/CHANGELOG
index 5e2b37bb86..fd163501f1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Add selection_change_with on Selection and Reference fields
* Add Dict fields
* Remove unique constraint on model and field access
* Use lists of values in create
diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst
index be9b64d328..b1f1c03aa0 100644
--- a/doc/ref/models/fields.rst
+++ b/doc/ref/models/fields.rst
@@ -310,7 +310,7 @@ A binary field. It will be represented in Python by a ``str`` instance.
Selection
---------
-.. class:: Selection(selection, string[, sort[, translate[, \**options]]])
+.. class:: Selection(selection, string[, sort[, selection_change_with[, translate[, \**options]]])
A string field with limited values to choice.
@@ -328,7 +328,7 @@ A string field with limited values to choice.
The first element in each tuple is the actual value stored. The second
element is the human-readable name.
- It can also be the name of a method on the model, that will return an
+ It can also be the name of a class method on the model, that will return an
appropriate list. The signature of the method is::
selection()
@@ -345,6 +345,13 @@ A string field with limited values to choice.
If true, the choices will be sorted by human-readable value. Default value
is ``True``.
+.. attribute:: Selection.selection_change_with
+
+ A list of field names. If this attribute is set, the client will call the
+ ``selection`` method of the model when the user changes on of the fields
+ defined in the list and will give the values of each fields in the list.
+ The ``selection`` method should be an instance method.
+
.. attribute:: Selection.translate_selection
If true, the human-readable values will be translated. Default value is
@@ -353,7 +360,7 @@ A string field with limited values to choice.
Reference
---------
-.. class:: Reference(string[, selection[, \**options]])
+.. class:: Reference(string[, selection[, selection_change_with[, \**options]])
A field that refers to a record of a model. It will be represented in Python by
a ``str`` instance like this::
@@ -368,6 +375,10 @@ But a ``tuple`` can be used to search or set value.
Same as :attr:`Selection.selection` but only for model name.
+.. attribute:: Reference.selection_change_with
+
+Same like :attr:`Selection.selection_change_with`.
+
Many2One
--------
diff --git a/trytond/ir/translation.py b/trytond/ir/translation.py
index 57012e4041..c7eb8f223b 100644
--- a/trytond/ir/translation.py
+++ b/trytond/ir/translation.py
@@ -965,6 +965,7 @@ class TranslationClean(Wizard):
'foreign_model_exist',
'search_function_missing',
'time_format_validation_record',
+ 'selection_value_notfound',
):
return False
if model_name in pool.object_name_list():
diff --git a/trytond/ir/translation.xml b/trytond/ir/translation.xml
index 45bc5ad4b4..be81d67d0a 100644
--- a/trytond/ir/translation.xml
+++ b/trytond/ir/translation.xml
@@ -229,6 +229,16 @@ this repository contains the full copyright notices and license terms. -->
ir
+
+ selection_value_notfound
+ en_US
+ error
+ Value not in the selection for field "%s".
+ Value not in the selection for field "%s".
+ ir
+
+
ir.translation.set_report.start
diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py
index b6cab0490c..3f07569b64 100644
--- a/trytond/model/fields/reference.py
+++ b/trytond/model/fields/reference.py
@@ -14,9 +14,9 @@ class Reference(Field):
'''
_type = 'reference'
- def __init__(self, string='', selection=None, help='', required=False,
- readonly=False, domain=None, states=None, select=False,
- on_change=None, on_change_with=None, depends=None,
+ def __init__(self, string='', selection=None, selection_change_with=None,
+ help='', required=False, readonly=False, domain=None, states=None,
+ select=False, on_change=None, on_change_with=None, depends=None,
order_field=None, context=None, loading='lazy'):
'''
:param selection: A list or a function name that returns a list.
@@ -29,6 +29,7 @@ class Reference(Field):
depends=depends, order_field=order_field, context=context,
loading=loading)
self.selection = selection or None
+ self.selection_change_with = selection_change_with
__init__.__doc__ += Field.__init__.__doc__
diff --git a/trytond/model/fields/selection.py b/trytond/model/fields/selection.py
index 5193723e6a..03e2d1fbe5 100644
--- a/trytond/model/fields/selection.py
+++ b/trytond/model/fields/selection.py
@@ -10,8 +10,9 @@ class Selection(Field):
'''
_type = 'selection'
- def __init__(self, selection, string='', sort=True, translate=True,
- help='', required=False, readonly=False, domain=None, states=None,
+ def __init__(self, selection, string='', sort=True,
+ selection_change_with=None, translate=True, help='',
+ required=False, readonly=False, domain=None, states=None,
select=False, on_change=None, on_change_with=None, depends=None,
order_field=None, context=None, loading='eager'):
'''
@@ -20,7 +21,7 @@ class Selection(Field):
to store and the second is the value to display.
:param sort: A boolean to sort or not the selections.
'''
- super(Selection, self).__init__(string=string, help=help,
+ super(Selection, self).__init__(string=string, help=help,
required=required, readonly=readonly, domain=domain, states=states,
select=select, on_change=on_change, on_change_with=on_change_with,
depends=depends, order_field=order_field, context=context,
@@ -29,6 +30,7 @@ class Selection(Field):
self.selection = selection.copy()
else:
self.selection = selection
+ self.selection_change_with = selection_change_with
self.sort = sort
self.translate_selection = translate
__init__.__doc__ += Field.__init__.__doc__
diff --git a/trytond/model/model.py b/trytond/model/model.py
index b00c1acf36..f80cd07959 100644
--- a/trytond/model/model.py
+++ b/trytond/model/model.py
@@ -475,6 +475,7 @@ class Model(WarningErrorMixin, URLMixin):
'datetime_field',
'loading',
'filename',
+ 'selection_change_with',
):
if getattr(cls._fields[field], arg, None) is not None:
res[field][arg] = copy.copy(getattr(cls._fields[field],
@@ -563,6 +564,8 @@ class Model(WarningErrorMixin, URLMixin):
cls._fields[field].field)
if res[field]['type'] in ('datetime', 'time'):
res[field]['format'] = copy.copy(cls._fields[field].format)
+ if res[field]['type'] == 'selection':
+ res[field]['context'] = copy.copy(cls._fields[field].context)
if res[field]['type'] == 'dict':
res[field]['context'] = copy.copy(cls._fields[field].context)
res[field]['create'] = accesses.get(field, {}).get('create',
diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py
index c87e886bb2..a5ebf86bc1 100644
--- a/trytond/model/modelstorage.py
+++ b/trytond/model/modelstorage.py
@@ -927,6 +927,13 @@ class ModelStorage(Model):
if isinstance(i, (list, tuple)):
if is_pyson(i):
return True
+ if isinstance(test, dict):
+ for key, value in test.items():
+ if isinstance(value, PYSON):
+ return True
+ if isinstance(value, (list, tuple, dict)):
+ if is_pyson(value):
+ return True
return False
with Transaction().set_context(ctx_pref):
@@ -1081,13 +1088,6 @@ class ModelStorage(Model):
if hasattr(field, 'selection') and field.selection:
if isinstance(field.selection, (tuple, list)):
test = set(dict(field.selection).keys())
- else:
- test = set(dict(getattr(cls,
- field.selection)()).keys())
- # None and '' are equivalent
- if '' in test or None in test:
- test.add('')
- test.add(None)
for record in records:
value = getattr(record, field_name)
if field._type == 'reference':
@@ -1095,6 +1095,17 @@ class ModelStorage(Model):
value = value.__class__.__name__
elif value:
value, _ = value.split(',')
+ if not isinstance(field.selection, (tuple, list)):
+ sel_func = getattr(cls, field.selection)
+ if field.selection_change_with:
+ test = sel_func(record)
+ else:
+ test = sel_func()
+ test = set(dict(test))
+ # None and '' are equivalent
+ if '' in test or None in test:
+ test.add('')
+ test.add(None)
if value not in test:
cls.raise_user_error('selection_validation_record',
error_args=cls._get_error_args(field_name))
diff --git a/trytond/test/__init__.py b/trytond/test/__init__.py
index 0e3046b27f..2b58f7147e 100644
--- a/trytond/test/__init__.py
+++ b/trytond/test/__init__.py
@@ -83,6 +83,8 @@ def register():
ReferenceTarget,
ReferenceRequired,
Property,
+ Selection,
+ SelectionRequired,
Dict,
DictDefault,
DictRequired,
diff --git a/trytond/test/test.py b/trytond/test/test.py
index 72902c6f30..7bba00369a 100644
--- a/trytond/test/test.py
+++ b/trytond/test/test.py
@@ -5,6 +5,7 @@ import datetime
from decimal import Decimal
from trytond.model import ModelSQL, fields
from trytond.pyson import Eval
+from trytond.transaction import Transaction
__all__ = [
'Boolean', 'BooleanDefault',
@@ -32,6 +33,7 @@ __all__ = [
'Many2ManySize', 'Many2ManySizeTarget', 'Many2ManySizeRelation',
'Reference', 'ReferenceTarget', 'ReferenceRequired',
'Property',
+ 'Selection', 'SelectionRequired',
'Dict', 'DictDefault', 'DictRequired',
]
@@ -589,6 +591,35 @@ class Property(ModelSQL):
], 'Test Selection'))
+class Selection(ModelSQL):
+ 'Selection'
+ __name__ = 'test.selection'
+ select = fields.Selection([
+ ('', ''), ('arabic', 'Arabic'), ('hexa', 'Hexadecimal')],
+ 'Selection')
+ dyn_select = fields.Selection('get_selection',
+ 'Instance Dynamic Selection', selection_change_with=['select'])
+ dyn_select_static = fields.Selection('static_selection',
+ 'Static Selection')
+
+ def get_selection(self):
+ if self.select == 'arabic':
+ return [('', '')] + [(str(i), str(i)) for i in range(1, 11)]
+ else:
+ return [('', '')] + [(hex(i), hex(i)) for i in range(1, 11)]
+
+ @staticmethod
+ def static_selection():
+ return [('', '')] + [(str(i), str(i)) for i in range(1, 11)]
+
+
+class SelectionRequired(ModelSQL):
+ 'Selection Required'
+ __name__ = 'test.selection_required'
+ select = fields.Selection([('arabic', 'Arabic'), ('latin', 'Latin')],
+ 'Selection', required=True)
+
+
class Dict(ModelSQL):
'Dict'
__name__ = 'test.dict'
diff --git a/trytond/tests/test_fields.py b/trytond/tests/test_fields.py
index 7c24ab9dbc..7eb7dd7467 100644
--- a/trytond/tests/test_fields.py
+++ b/trytond/tests/test_fields.py
@@ -102,6 +102,9 @@ class FieldsTestCase(unittest.TestCase):
self.ir_property = POOL.get('ir.property')
self.model_field = POOL.get('ir.model.field')
+ self.selection = POOL.get('test.selection')
+ self.selection_required = POOL.get('test.selection_required')
+
self.dict_ = POOL.get('test.dict')
self.dict_default = POOL.get('test.dict_default')
self.dict_required = POOL.get('test.dict_required')
@@ -3133,7 +3136,55 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
- def test0160dict(self):
+ def test_0160selection(self):
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ selection1, = self.selection.create([{'select': 'arabic'}])
+ self.assert_(selection1)
+ self.assertEqual(selection1.select, 'arabic')
+
+ selection2, = self.selection.create([{'select': None}])
+ self.assert_(selection2)
+ self.assertEqual(selection2.select, None)
+
+ self.assertRaises(Exception, self.selection.create,
+ [{'select': 'chinese'}])
+
+ selection3, = self.selection.create(
+ [{'select': 'arabic', 'dyn_select': '1'}])
+ self.assert_(selection3)
+ self.assertEqual(selection3.select, 'arabic')
+ self.assertEqual(selection3.dyn_select, '1')
+
+ selection4, = self.selection.create(
+ [{'select': 'hexa', 'dyn_select': '0x3'}])
+ self.assert_(selection4)
+ self.assertEqual(selection4.select, 'hexa')
+ self.assertEqual(selection4.dyn_select, '0x3')
+
+ selection5, = self.selection.create(
+ [{'select': 'hexa', 'dyn_select': None}])
+ self.assert_(selection5)
+ self.assertEqual(selection5.select, 'hexa')
+ self.assertEqual(selection5.dyn_select, None)
+
+ self.assertRaises(Exception, self.selection.create,
+ [{'select': 'arabic', 'dyn_select': '0x3'}])
+ self.assertRaises(Exception, self.selection.create,
+ [{'select': 'hexa', 'dyn_select': '3'}])
+
+ self.assertRaises(Exception, self.selection_required.create, [{}])
+ transaction.cursor.rollback()
+
+ self.assertRaises(Exception, self.selection_required.create,
+ [{'select': None}])
+ transaction.cursor.rollback()
+
+ selection6, = self.selection_required.create([{'select': 'latin'}])
+ self.assert_(selection6)
+ self.assertEqual(selection6.select, 'latin')
+
+ def test0170dict(self):
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
dict1, = self.dict_.create([{