diff --git a/CHANGELOG b/CHANGELOG
index f526b4b4bf..0bd0ff6d3e 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Add help for each value to selection and multiselection
* Use safe_join in SharedDataMiddlewareIndex (issue10068)
* Use menu name for window opened from menu
* Add trigonometric functions to sqlite backend
diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst
index 47dcb0b134..0f4707a6e2 100644
--- a/doc/ref/models/fields.rst
+++ b/doc/ref/models/fields.rst
@@ -446,7 +446,7 @@ A binary field. It will be represented in Python by a ``bytes`` instance.
Selection
---------
-.. class:: Selection(selection, string[, sort[, selection_change_with[, translate[, \**options]]]])
+.. class:: Selection(selection, string[, sort[, selection_change_with[, translate[, help_selection[, \**options]]]]])
A string field with limited values to choose from.
@@ -494,6 +494,10 @@ A string field with limited values to choose from.
If true, the human-readable values will be translated. Default value is
``True``.
+.. attribute:: Selection.help_selection
+
+ A dictionary mapping the selection value with its help string.
+
Instance methods:
.. method:: Selection.translated([name])
@@ -506,7 +510,7 @@ Instance methods:
MultiSelection
--------------
-.. class:: MultiSelection(selection, string[, sort[, translate[, \**options]]])
+.. class:: MultiSelection(selection, string[, sort[, translate[, help_selection[, \**options]]]])
A list field with limited values to choose from.
@@ -526,6 +530,10 @@ A list field with limited values to choose from.
Same as :attr:`Selection.translate_selection`
+.. attribute:: MultiSelection.help_selection
+
+ Same as :attr:`Selection.help_selection`
+
Instance methods:
.. method:: MultiSelection.translated([name])
@@ -536,7 +544,7 @@ Instance methods:
Reference
---------
-.. class:: Reference(string[, selection[, sort[, selection_change_with[, translated[,search_order[, search_context[, \**options]]]]]]])
+.. class:: Reference(string[, selection[, sort[, selection_change_with[, translate[, help_selection[,search_order[, search_context[, \**options]]]]]]]])
A field that refers to a record of a model. It will be represented in Python by
a ``str`` instance like this::
@@ -563,6 +571,10 @@ But a ``tuple`` can be used to search or set value.
Same as :attr:`Selection.translate_selection`.
+.. attribute:: Reference.help_selection
+
+ Same as :attr:`Selection.help_selection`.
+
.. attribute:: Reference.datetime_field
Same as :attr:`Many2One.datetime_field`
diff --git a/doc/topics/views/index.rst b/doc/topics/views/index.rst
index 65cf247281..56a00e8e9f 100644
--- a/doc/topics/views/index.rst
+++ b/doc/topics/views/index.rst
@@ -167,6 +167,11 @@ List of attributes shared by many form elements:
* ``symbol``: Only on numerical fields, the name of field which provides
the symbol to display.
+ .. _common-attributes-help_field:
+
+ * ``help_field``: The name of Dict field mapping the Selection value with
+ its help string.
+
form
^^^^
@@ -289,6 +294,8 @@ Display a field of the object with the value of the current record.
* ``symbol``: see in common-attributes-symbol_.
+ * ``help_field``: see in common-attributes-help_field_.
+
image
^^^^^
@@ -609,6 +616,8 @@ field
* ``symbol``: see in common-attributes-symbol_.
+ * ``help_field``: see in common-attributes-help_field_.
+
prefix or suffix
^^^^^^^^^^^^^^^^
diff --git a/trytond/ir/message.xml b/trytond/ir/message.xml
index f64364dc47..e3bda1061d 100644
--- a/trytond/ir/message.xml
+++ b/trytond/ir/message.xml
@@ -108,9 +108,18 @@ this repository contains the full copyright notices and license terms. -->
If the selection must be sorted on label.
+
+ Help Selection
+
+
+ The key followed by the help text separated by a ":", one per line.
+
Selection JSON
+
+ Help Selection JSON
+
Sequence
@@ -199,6 +208,9 @@ this repository contains the full copyright notices and license terms. -->
Invalid selection in schema "%(schema)s".
+
+ Invalid help selection in schema "%(schema)s".
+
Recursion error: Record "%(rec_name)s" with parent "%(parent_rec_name)s" was configured as ancestor of itself.
diff --git a/trytond/ir/ui/form.rnc b/trytond/ir/ui/form.rnc
index 458b95f146..f22a637b76 100644
--- a/trytond/ir/ui/form.rnc
+++ b/trytond/ir/ui/form.rnc
@@ -105,6 +105,7 @@ attlist.field &= [a:defaultValue = "1"] attribute completion { "0" | "1" }?
attlist.field &= attribute string { text }?
attlist.field &= [a:defaultValue = "1"] attribute factor { text }?
attlist.field &= attribute filename { text }?
+attlist.field &= attribute help_field { text }?
attlist.field &=
[a:defaultValue = "0"] attribute toolbar { "0" | "1" }?
attlist.field &= attribute symbol { text }?
diff --git a/trytond/ir/ui/form.rng b/trytond/ir/ui/form.rng
index 0567c3e455..af0e3b5c07 100644
--- a/trytond/ir/ui/form.rng
+++ b/trytond/ir/ui/form.rng
@@ -465,6 +465,14 @@
+
+
+
+ help_field
+
+
+
+
diff --git a/trytond/ir/ui/tree.rnc b/trytond/ir/ui/tree.rnc
index 9e72de412a..a24cb8a3ee 100644
--- a/trytond/ir/ui/tree.rnc
+++ b/trytond/ir/ui/tree.rnc
@@ -63,6 +63,7 @@ attlist.field &= [a:defaultValue = "1"] attribute completion { "0" | "1" }?
attlist.field &= attribute string { text }?
attlist.field &= [a:defaultValue = "1"] attribute factor { text }?
attlist.field &= attribute filename { text }?
+attlist.field &= attribute help_field { text }?
attlist.field &= attribute view_ids { text }?
attlist.field &= attribute symbol { text }?
prefix = element prefix { attlist.affix, empty }
diff --git a/trytond/ir/ui/tree.rng b/trytond/ir/ui/tree.rng
index 674d77d23a..132b320efe 100644
--- a/trytond/ir/ui/tree.rng
+++ b/trytond/ir/ui/tree.rng
@@ -248,6 +248,14 @@
+
+
+
+ help_field
+
+
+
+
diff --git a/trytond/model/dictschema.py b/trytond/model/dictschema.py
index d468f6aac7..a0ca8bb332 100644
--- a/trytond/model/dictschema.py
+++ b/trytond/model/dictschema.py
@@ -63,6 +63,13 @@ class DictSchemaMixin(object):
'invisible': ~Eval('type_').in_(['selection', 'multiselection']),
}, depends=['type_'],
help=lazy_gettext('ir.msg_dict_schema_selection_sorted_help'))
+ help_selection = fields.Text(
+ lazy_gettext('ir.msg_dict_schema_help_selection'), translate=True,
+ states={
+ 'invisible': ~Eval('type_').in_(['selection', 'multiselection']),
+ },
+ depends=['type_'],
+ help=lazy_gettext('is.msg_dict_schema_help_selection_help'))
selection_json = fields.Function(fields.Char(
lazy_gettext('ir.msg_dict_schema_selection_json'),
states={
@@ -70,6 +77,13 @@ class DictSchemaMixin(object):
['selection', 'multiselection']),
},
depends=['type_']), 'get_selection_json')
+ help_selection_json = fields.Function(fields.Char(
+ lazy_gettext('ir.msg_dict_schema_help_selection_json'),
+ states={
+ 'invisible': ~Eval('type_').in_(
+ ['selection', 'multiselection']),
+ },
+ depends=['type_']), 'get_selection_json')
_relation_fields_cache = Cache('_dict_schema_mixin.get_relation_fields')
@classmethod
@@ -119,15 +133,17 @@ class DictSchemaMixin(object):
for schema in schemas:
if schema.type_ not in {'selection', 'multiselection'}:
continue
- try:
- dict(json.loads(schema.get_selection_json()))
- except Exception:
- raise SelectionError(
- gettext('ir.msg_dict_schema_invalid_selection',
- schema=schema.rec_name))
+ for name in ['selection', 'help_selection']:
+ try:
+ dict(json.loads(schema.get_selection_json(name + '_json')))
+ except Exception:
+ raise SelectionError(
+ gettext('ir.msg_dict_schema_invalid_%s' % name,
+ schema=schema.rec_name))
- def get_selection_json(self, name=None):
- db_selection = self.selection or ''
+ def get_selection_json(self, name):
+ field = name[:-len('_json')]
+ db_selection = getattr(self, field) or ''
selection = [[w.strip() for w in v.split(':', 1)]
for v in db_selection.splitlines() if v]
return json.dumps(selection, separators=(',', ':'))
@@ -154,6 +170,8 @@ class DictSchemaMixin(object):
english_key.selection_json))
selection.update(dict(json.loads(record.selection_json)))
new_key['selection'] = list(selection.items())
+ new_key['help_selection'] = dict(
+ json.loads(record.help_selection_json))
new_key['sort'] = record.selection_sorted
elif record.type_ in ('float', 'numeric'):
new_key['digits'] = (16, record.digits)
diff --git a/trytond/model/fields/multiselection.py b/trytond/model/fields/multiselection.py
index 4d1c3fbb68..060b6e57a4 100644
--- a/trytond/model/fields/multiselection.py
+++ b/trytond/model/fields/multiselection.py
@@ -21,9 +21,9 @@ class MultiSelection(SelectionMixin, Field):
_py_type = list
def __init__(self, selection, string='', sort=True, translate=True,
- help='', required=False, readonly=False, domain=None, states=None,
- select=False, on_change=None, on_change_with=None, depends=None,
- context=None, loading='eager'):
+ help='', help_selection=None, required=False, readonly=False,
+ domain=None, states=None, select=False, on_change=None,
+ on_change_with=None, depends=None, context=None, loading='eager'):
"""
:param selection: A list or a function name that returns a list.
The list must be a list of tuples. First member is the value
@@ -41,6 +41,7 @@ class MultiSelection(SelectionMixin, Field):
self.selection_change_with = set()
self.sort = sort
self.translate_selection = translate
+ self.help_selection = help_selection
__init__.__doc__ += Field.__init__.__doc__
def set_rpc(self, model):
diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py
index fec7bdc2ce..c71e1a07d2 100644
--- a/trytond/model/fields/reference.py
+++ b/trytond/model/fields/reference.py
@@ -24,10 +24,10 @@ class Reference(SelectionMixin, Field):
def __init__(self, string='', selection=None, sort=True,
selection_change_with=None, translate=True, search_order=None,
- search_context=None, help='', required=False, readonly=False,
- domain=None, states=None, select=False, on_change=None,
- on_change_with=None, depends=None, context=None, loading='lazy',
- datetime_field=None):
+ search_context=None, help='', help_selection=None, required=False,
+ readonly=False, domain=None, states=None, select=False,
+ on_change=None, on_change_with=None, depends=None, context=None,
+ loading='lazy', datetime_field=None):
'''
:param selection: A list or a function name that returns a list.
The list must be a list of tuples. First member is an internal name
@@ -57,6 +57,7 @@ class Reference(SelectionMixin, Field):
self.selection_change_with |= set(selection_change_with)
self.sort = sort
self.translate_selection = translate
+ self.help_selection = help_selection
self.__search_order = None
self.search_order = search_order
self.__search_context = None
diff --git a/trytond/model/fields/selection.py b/trytond/model/fields/selection.py
index c6d5c63141..be887038ef 100644
--- a/trytond/model/fields/selection.py
+++ b/trytond/model/fields/selection.py
@@ -1,6 +1,7 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import warnings
+from itertools import chain
from sql.conditionals import Case
@@ -40,19 +41,33 @@ class SelectionMixin(Field):
selection = self.selection.copy()
else:
selection = self.selection
+ if self.help_selection:
+ help_selection = {}
+ for key, source in self.help_selection.items():
+ if not isinstance(source, LazyString):
+ source = Translation.get_source(
+ name, 'selection', language, source) or source
+ help_selection[key] = str(source)
+ else:
+ help_selection = None
definition['selection'] = selection
definition['selection_change_with'] = list(self.selection_change_with)
definition['sort'] = self.sort
+ definition['help_selection'] = help_selection
return definition
def definition_translations(self, model, language):
name = '%s,%s' % (model.__name__, self.name)
selection = []
+ sources = []
if not isinstance(self.selection, str) and self.translate_selection:
- for key, source in self.selection:
- if not isinstance(source, LazyString):
- selection.append(
- (name, 'selection', language, source))
+ sources.append(self.selection)
+ if self.help_selection:
+ sources.append(self.help_selection.items())
+ for key, source in chain(*sources):
+ if not isinstance(source, LazyString):
+ selection.append(
+ (name, 'selection', language, source))
return super().definition_translations(model, language) + selection
@@ -66,9 +81,9 @@ class Selection(SelectionMixin, Field):
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,
- context=None, loading='eager'):
+ help_selection=None, required=False, readonly=False, domain=None,
+ states=None, select=False, on_change=None, on_change_with=None,
+ depends=None, context=None, loading='eager'):
'''
:param selection: A list or a function name that returns a list.
The list must be a list of tuples. First member is the value
@@ -91,6 +106,7 @@ class Selection(SelectionMixin, Field):
self.selection_change_with |= set(selection_change_with)
self.sort = sort
self.translate_selection = translate
+ self.help_selection = help_selection
__init__.__doc__ += Field.__init__.__doc__
def set_rpc(self, model):