diff --git a/sao/src/common.js b/sao/src/common.js index f4c9d5585a..84f5cd3153 100644 --- a/sao/src/common.js +++ b/sao/src/common.js @@ -1953,10 +1953,13 @@ .replace(escape + '%', '%') .replace(escape + '_', '_'); }, - quote: function(value) { + quote: function(value, empty=false) { if (typeof value != 'string') { return value; } + if (empty && value === '') { + return '""'; + } if (value.contains('\\')) { value = value.replace(new RegExp('\\\\', 'g'), '\\\\'); } @@ -2115,7 +2118,8 @@ return value; } }, - format_value: function(field, value, target=null, context={}) { + format_value: function( + field, value, target=null, context={}, _quote_empty=false) { var format_float = function() { if (!value && value !== 0 && value !== new Sao.Decimal(0)) { return ''; @@ -2226,7 +2230,9 @@ }; converts.timestamp = converts.datetime; if (value instanceof Array) { - return value.map(v => this.format_value(field, v)).join(';'); + return value.map( + v => this.format_value(field, v, null, context, true)) + .join(';'); } else { var func = converts[field.type]; if (func) { @@ -2234,7 +2240,7 @@ } else if (value === null) { return ''; } else { - return this.quote(value); + return this.quote(value, _quote_empty); } } }, diff --git a/sao/tests/sao.js b/sao/tests/sao.js index 2f1b58ff08..61a151185f 100644 --- a/sao/tests/sao.js +++ b/sao/tests/sao.js @@ -2266,6 +2266,7 @@ [[['name', 'ilike', 'Doe\\%']], 'Name: =Doe%'], [[['name', 'not ilike', '%Doe%']], 'Name: !Doe'], [[['name', 'in', ['John', 'Jane']]], 'Name: John;Jane'], + [[['name', 'in', ['John', '']]], 'Name: John;""'], [[['name', 'in', ['John']]], 'Name: =John'], [[['name', 'not in', ['John', 'Jane']]], 'Name: !John;Jane'], [[['name', 'not in', ['John']]], 'Name: !=John'], diff --git a/tryton/tryton/common/domain_parser.py b/tryton/tryton/common/domain_parser.py index 4ef6f8ce7c..9b74d7c354 100644 --- a/tryton/tryton/common/domain_parser.py +++ b/tryton/tryton/common/domain_parser.py @@ -128,10 +128,12 @@ def unescape(value, escape='\\'): return value.replace(escape + '%', '%').replace(escape + '_', '_') -def quote(value): +def quote(value, empty=False): "Quote string if needed" if not isinstance(value, str): return value + if empty and value == '': + return '""' if '\\' in value: value = value.replace('\\', '\\\\') if '"' in value: @@ -307,7 +309,7 @@ def convert_value(field, value, context=None): return converts.get(field['type'], lambda: value)() -def format_value(field, value, target=None, context=None): +def format_value(field, value, target=None, context=None, _quote_empty=False): "Format value for field" if context is None: context = {} @@ -410,9 +412,11 @@ def format_value(field, value, target=None, context=None): 'many2one': format_many2one, } if isinstance(value, (list, tuple)): - return ';'.join(format_value(field, x, context=context) for x in value) + return ';'.join( + format_value(field, x, context=context, _quote_empty=True) + for x in value) return quote(converts.get(field['type'], - lambda: value if value is not None else '')()) + lambda: value if value is not None else '')(), empty=_quote_empty) def complete_value(field, value): diff --git a/tryton/tryton/tests/test_common_domain_parser.py b/tryton/tryton/tests/test_common_domain_parser.py index 055904031e..f1abab338d 100644 --- a/tryton/tryton/tests/test_common_domain_parser.py +++ b/tryton/tryton/tests/test_common_domain_parser.py @@ -681,6 +681,8 @@ class DomainParserTestCase(TestCase): dom.string([('name', 'not ilike', '%Doe%')]), 'Name: !Doe') self.assertEqual( dom.string([('name', 'in', ['John', 'Jane'])]), 'Name: John;Jane') + self.assertEqual( + dom.string([('name', 'in', ['John', ''])]), 'Name: John;""') self.assertEqual( dom.string([('name', 'in', ['John'])]), 'Name: =John') self.assertEqual(