From 4cd093a0013752ddb140fe0f0f8928bc001543f6 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 29 Jan 2019 16:32:14 +0000 Subject: [PATCH] [IMP] fields: extend selection with a given order When extended with the selection_add parameter, a singleton (value,) where `value` appears in the overriden selection, can be used. The new values are inserted in an order that is consistent with the overridden selection and this list. selection = [('a', 'A'), ('b', 'B')] selection_add = [('c', 'C'), ('b',)] > result = [('a', 'A'), ('c', 'C'), ('b', 'B')] --- odoo/addons/base/tests/test_misc.py | 30 ++++++++++++- odoo/addons/test_inherit/models.py | 4 +- .../addons/test_inherit/tests/test_inherit.py | 2 +- odoo/fields.py | 42 +++++++++++++++---- odoo/tools/misc.py | 29 +++++++++++++ 5 files changed, 94 insertions(+), 13 deletions(-) diff --git a/odoo/addons/base/tests/test_misc.py b/odoo/addons/base/tests/test_misc.py index 440e6899ede..34bd040eff8 100644 --- a/odoo/addons/base/tests/test_misc.py +++ b/odoo/addons/base/tests/test_misc.py @@ -5,7 +5,7 @@ import datetime from dateutil.relativedelta import relativedelta import pytz -from odoo.tools import misc, date_utils +from odoo.tools import misc, date_utils, merge_sequences from odoo.tests.common import TransactionCase, BaseCase @@ -40,6 +40,34 @@ class TestCountingStream(BaseCase): self.assertEqual(s.index, 0) +class TestMergeSequences(BaseCase): + def test_merge_sequences(self): + # base case + seq = merge_sequences(['A', 'B', 'C']) + self.assertEqual(seq, ['A', 'B', 'C']) + + # 'Z' can be anywhere + seq = merge_sequences(['A', 'B', 'C'], ['Z']) + self.assertEqual(seq, ['A', 'B', 'C', 'Z']) + + # 'Y' must precede 'C'; + seq = merge_sequences(['A', 'B', 'C'], ['Y', 'C']) + self.assertEqual(seq, ['A', 'B', 'Y', 'C']) + + # 'X' must follow 'A' and precede 'C' + seq = merge_sequences(['A', 'B', 'C'], ['A', 'X', 'C']) + self.assertEqual(seq, ['A', 'B', 'X', 'C']) + + # all cases combined + seq = merge_sequences( + ['A', 'B', 'C'], + ['Z'], # 'Z' can be anywhere + ['Y', 'C'], # 'Y' must precede 'C'; + ['A', 'X', 'Y'], # 'X' must follow 'A' and precede 'Y' + ) + self.assertEqual(seq, ['A', 'B', 'X', 'Y', 'C', 'Z']) + + class TestDateRangeFunction(BaseCase): """ Test on date_range generator. """ diff --git a/odoo/addons/test_inherit/models.py b/odoo/addons/test_inherit/models.py index e4b09e292c7..5d6c7a76436 100644 --- a/odoo/addons/test_inherit/models.py +++ b/odoo/addons/test_inherit/models.py @@ -55,8 +55,8 @@ class mother(models.Model): class mother(models.Model): _inherit = 'test.inherit.mother' - # extend again the selection of the state field - state = fields.Selection(selection_add=[('d', 'D')]) + # extend again the selection of the state field: 'd' must precede 'b' + state = fields.Selection(selection_add=[('d', 'D'), ('b',)]) class daughter(models.Model): diff --git a/odoo/addons/test_inherit/tests/test_inherit.py b/odoo/addons/test_inherit/tests/test_inherit.py index 568ef5fb47b..d47f9b0dc93 100644 --- a/odoo/addons/test_inherit/tests/test_inherit.py +++ b/odoo/addons/test_inherit/tests/test_inherit.py @@ -66,7 +66,7 @@ class test_inherits(common.TransactionCase): # the extra values are added, both in the field and the column self.assertEqual(mother._fields['state'].selection, - [('a', 'A'), ('b', 'B'), ('c', 'C'), ('d', 'D')]) + [('a', 'A'), ('d', 'D'), ('b', 'B'), ('c', 'C')]) def test_50_search_one2many(self): """ check search on one2many field based on inherited many2one field. """ diff --git a/odoo/fields.py b/odoo/fields.py index 3c24223d083..44cbc97d9d4 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -23,7 +23,7 @@ except ImportError: import psycopg2 from .tools import float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar, \ - ustr, OrderedSet, pycompat, sql, date_utils, unique, IterableGenerator, image_process + ustr, OrderedSet, pycompat, sql, date_utils, unique, IterableGenerator, image_process, merge_sequences from .tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT from .tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT from .tools.translate import html_translate, _ @@ -1995,10 +1995,18 @@ class Image(Binary): class Selection(Field): """ :param selection: specifies the possible values for this field. - It is given as either a list of pairs (``value``, ``string``), or a - model method, or a method name. + It is given as either a list of pairs ``(value, label)``, or a model + method, or a method name. + :param selection_add: provides an extension of the selection in the case - of an overridden field. It is a list of pairs (``value``, ``string``). + of an overridden field. It is a list of pairs ``(value, label)`` or + singletons ``(value,)``, where singleton values must appear in the + overridden selection. The new values are inserted in an order that is + consistent with the overridden selection and this list:: + + selection = [('a', 'A'), ('b', 'B')] + selection_add = [('c', 'C'), ('b',)] + > result = [('a', 'A'), ('c', 'C'), ('b', 'B')] The attribute ``selection`` is mandatory except in the case of :ref:`related fields ` or :ref:`field extensions @@ -2029,16 +2037,31 @@ class Selection(Field): def _setup_attrs(self, model, name): super(Selection, self)._setup_attrs(model, name) + # determine selection (applying 'selection_add' extensions) + values = None + labels = {} + for field in reversed(resolve_mro(model, name, self._can_setup_from)): # We cannot use field.selection or field.selection_add here # because those attributes are overridden by ``_setup_attrs``. if 'selection' in field.args: - self.selection = field.args['selection'] + selection = field.args['selection'] + if isinstance(selection, list): + values = [kv[0] for kv in selection] + labels.update(selection) + else: + self.selection = selection + if 'selection_add' in field.args: - # use an OrderedDict to update existing values selection_add = field.args['selection_add'] - self.selection = list(OrderedDict(self.selection + selection_add).items()) + assert isinstance(selection_add, list) + assert values is not None + values = merge_sequences(values, [kv[0] for kv in selection_add]) + labels.update(kv for kv in selection_add if len(kv) == 2) + + if values is not None: + self.selection = [(value, labels[value]) for value in values] def _selection_modules(self, model): """ Return a mapping from selection values to modules defining each value. """ @@ -2055,8 +2078,9 @@ class Selection(Field): for value, label in field.args['selection']: value_modules[value].add(module) if 'selection_add' in field.args: - for value, label in field.args['selection_add']: - value_modules[value].add(module) + for value_label in field.args['selection_add']: + if len(value_label) > 1: + value_modules[value_label[0]].add(module) return value_modules def _description_selection(self, env): diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index 14e207d7db1..3139cc1b526 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -349,6 +349,35 @@ def topological_sort(elems): return result +def merge_sequences(*iterables): + """ Merge several iterables into a list. The result is the union of the + iterables, ordered following the partial order given by the iterables, + with a bias towards the end for the last iterable:: + + seq = merge_sequences(['A', 'B', 'C']) + assert seq == ['A', 'B', 'C'] + + seq = merge_sequences( + ['A', 'B', 'C'], + ['Z'], # 'Z' can be anywhere + ['Y', 'C'], # 'Y' must precede 'C'; + ['A', 'X', 'Y'], # 'X' must follow 'A' and precede 'Y' + ) + assert seq == ['A', 'B', 'X', 'Y', 'C', 'Z'] + """ + # we use an OrderedDict to keep elements in order by default + deps = OrderedDict() # {item: elems_before_item} + for iterable in iterables: + prev = None + for index, item in enumerate(iterable): + if not index: + deps.setdefault(item, []) + else: + deps.setdefault(item, []).append(prev) + prev = item + return topological_sort(deps) + + try: import xlwt