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([{