diff --git a/CHANGELOG b/CHANGELOG
index 4c0374004e..7c99dbab05 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Add symbol widget
* Add sql_pairing tool
* Add format_datetime to Report
* Allow sharing view searches
diff --git a/COPYRIGHT b/COPYRIGHT
index 439a6eca2f..41a07a86ea 100644
--- a/COPYRIGHT
+++ b/COPYRIGHT
@@ -4,6 +4,8 @@ Copyright (C) 2007-2013 Bertrand Chenal.
Copyright (C) 2008-2020 B2CK SPRL.
Copyright (C) 2011 Openlabs Technologies & Consulting (P) Ltd.
Copyright (C) 2011-2020 Nicolas Évrard.
+Copyright (C) 2020 Maxime Richez
+Copyright (C) 2020 SALUC SA
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
diff --git a/doc/ref/models/models.rst b/doc/ref/models/models.rst
index 57ec28ca04..246cfd3a8e 100644
--- a/doc/ref/models/models.rst
+++ b/doc/ref/models/models.rst
@@ -762,6 +762,23 @@ Class methods:
Return the SQL table and columns to use for the UNION for the model name.
+===========
+SymbolMixin
+===========
+
+.. class:: SymbolMixin
+
+A mixin_ to manage the display of symbols on the client side.
+
+Instance methods:
+
+.. method:: SymbolMixin.get_symbol(sign, [symbol])
+
+ Return a symbol and its position.
+ The position indicates whether the symbol should appear before (0) or after
+ (1) the value. If no symbol parameter is supplied then the mixin uses the
+ value of attribute named `symbol`.
+
================
sequence_ordered
================
diff --git a/doc/topics/views/index.rst b/doc/topics/views/index.rst
index 7763ce8be4..699fd0323a 100644
--- a/doc/topics/views/index.rst
+++ b/doc/topics/views/index.rst
@@ -162,6 +162,10 @@ List of attributes shared by many form elements:
:class:`trytond.model.fields.Numeric` to display on the widget. The
default value is 1.
+ .. _common-attributes-symbol:
+
+ * ``symbol``: Only on numerical fields, the name of field which provides
+ the symbol to display.
form
^^^^
@@ -283,6 +287,8 @@ Display a field of the object with the value of the current record.
that will be evaluated to the language code for which spell checking must
be done.
+ * ``symbol``: see in common-attributes-symbol_.
+
image
^^^^^
@@ -601,6 +607,8 @@ field
* ``factor``: see in common-attributes-factor_.
+ * ``symbol``: see in common-attributes-symbol_.
+
prefix or suffix
^^^^^^^^^^^^^^^^
diff --git a/trytond/ir/ui/form.rnc b/trytond/ir/ui/form.rnc
index 5b501964f7..458b95f146 100644
--- a/trytond/ir/ui/form.rnc
+++ b/trytond/ir/ui/form.rnc
@@ -107,6 +107,7 @@ attlist.field &= [a:defaultValue = "1"] attribute factor { text }?
attlist.field &= attribute filename { text }?
attlist.field &=
[a:defaultValue = "0"] attribute toolbar { "0" | "1" }?
+attlist.field &= attribute symbol { text }?
image = element image { attlist.image, empty }
attlist.image &= attribute name { text }
attlist.image &= [ a:defaultValue = "1" ] attribute colspan { text }?
diff --git a/trytond/ir/ui/form.rng b/trytond/ir/ui/form.rng
index 0b01e8f976..0567c3e455 100644
--- a/trytond/ir/ui/form.rng
+++ b/trytond/ir/ui/form.rng
@@ -476,6 +476,14 @@
+
+
+
+ symbol
+
+
+
+
image
diff --git a/trytond/ir/ui/tree.rnc b/trytond/ir/ui/tree.rnc
index ba8c4f7134..9e72de412a 100644
--- a/trytond/ir/ui/tree.rnc
+++ b/trytond/ir/ui/tree.rnc
@@ -64,6 +64,7 @@ attlist.field &= attribute string { text }?
attlist.field &= [a:defaultValue = "1"] attribute factor { text }?
attlist.field &= attribute filename { text }?
attlist.field &= attribute view_ids { text }?
+attlist.field &= attribute symbol { text }?
prefix = element prefix { attlist.affix, empty }
suffix = element suffix { attlist.affix, empty }
attlist.affix &= attribute string { text }?
diff --git a/trytond/ir/ui/tree.rng b/trytond/ir/ui/tree.rng
index 5938647a92..674d77d23a 100644
--- a/trytond/ir/ui/tree.rng
+++ b/trytond/ir/ui/tree.rng
@@ -256,6 +256,14 @@
+
+
+
+ symbol
+
+
+
+
prefix
diff --git a/trytond/model/__init__.py b/trytond/model/__init__.py
index 1dc2d12016..2725fab849 100644
--- a/trytond/model/__init__.py
+++ b/trytond/model/__init__.py
@@ -9,6 +9,7 @@ from .workflow import Workflow
from .dictschema import DictSchemaMixin
from .match import MatchMixin
from .union import UnionMixin
+from .symbol import SymbolMixin
from .multivalue import MultiValueMixin, ValueMixin
from .descriptors import dualmethod
from .order import sequence_ordered
@@ -18,5 +19,5 @@ from .tree import tree
__all__ = ['Model', 'ModelView', 'ModelStorage', 'ModelSingleton', 'ModelSQL',
'Check', 'Unique', 'Exclude',
'Workflow', 'DictSchemaMixin', 'MatchMixin', 'UnionMixin', 'dualmethod',
- 'MultiValueMixin', 'ValueMixin',
+ 'MultiValueMixin', 'ValueMixin', 'SymbolMixin',
'EvalEnvironment', 'sequence_ordered', 'DeactivableMixin', 'tree']
diff --git a/trytond/model/modelview.py b/trytond/model/modelview.py
index c39c75f063..2bc14d4663 100644
--- a/trytond/model/modelview.py
+++ b/trytond/model/modelview.py
@@ -578,7 +578,7 @@ class ModelView(Model):
break
return views
- for attr in ('name', 'icon'):
+ for attr in ('name', 'icon', 'symbol'):
if not element.get(attr):
continue
fields_attrs.setdefault(element.get(attr), {})
diff --git a/trytond/model/symbol.py b/trytond/model/symbol.py
new file mode 100644
index 0000000000..f0bf6807cf
--- /dev/null
+++ b/trytond/model/symbol.py
@@ -0,0 +1,21 @@
+# 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 trytond.rpc import RPC
+
+
+class SymbolMixin(object):
+ __slots__ = ()
+
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ cls.__rpc__.update({
+ 'get_symbol': RPC(instantiate=0, cache=dict(days=1)),
+ })
+
+ def get_symbol(self, sign, symbol=None):
+ 'Return the symbol and its position'
+ position = 1
+ if symbol is None:
+ symbol = getattr(self, 'symbol', None)
+ return symbol, position
diff --git a/trytond/tests/test_tryton.py b/trytond/tests/test_tryton.py
index 532785496d..c2d3178c79 100644
--- a/trytond/tests/test_tryton.py
+++ b/trytond/tests/test_tryton.py
@@ -280,7 +280,7 @@ class ModuleTestCase(unittest.TestCase):
for element in tree_root.iter():
if element.tag in ('field', 'label', 'separator', 'group'):
- for attr in ('name', 'icon'):
+ for attr in ['name', 'icon', 'symbol']:
field = element.get(attr)
if field:
self.assertIn(field, res['fields'],