Added domain inversion feature
This commit is contained in:
1 parent
6573eff501
commit
1f277dd067
10 files changed
+559
-123
No files matched your search
@@ -1,3 +1,4 @@
|
||||
* Added domain inversion feature
|
||||
* Handle loading attribute on fields
|
||||
* Use default format for value in wizard form
|
||||
* Add One2One field
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
Copyright (C) 2010- Nicolas Évrard.
|
||||
Copyright (C) 2007-2010 Cédric Krier.
|
||||
Copyright (C) 2007-2010 Bertrand Chenal.
|
||||
Copyright (C) 2008-2010 B2CK SPRL.
|
||||
|
||||
@@ -2,3 +2,6 @@
|
||||
#this repository contains the full copyright notices and license terms.
|
||||
from common import *
|
||||
from datetime_strftime import *
|
||||
from domain_inversion import domain_inversion, eval_domain, localize_domain, \
|
||||
unlocalize_domain, merge, inverse_leaf
|
||||
from environment import EvalEnvironment
|
||||
@@ -0,0 +1,405 @@
|
||||
#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 operator
|
||||
import types
|
||||
|
||||
OPERATORS = {
|
||||
'=': operator.eq,
|
||||
'>': operator.gt,
|
||||
'<': operator.lt,
|
||||
'<=': operator.le,
|
||||
'>=': operator.ge,
|
||||
'!=': operator.ne,
|
||||
'in': lambda a, b: operator.contains(b, a),
|
||||
'not in': lambda a, b: not operator.contains(b, a),
|
||||
# Those operators are not supported (yet ?)
|
||||
'like': lambda a, b: True,
|
||||
'ilike': lambda a, b: True,
|
||||
'not like': lambda a, b: True,
|
||||
'not ilike': lambda a, b: True,
|
||||
'child_of': lambda a, b: True,
|
||||
'not child_of': lambda a, b: True,
|
||||
}
|
||||
|
||||
def locale_part(expression):
|
||||
if '.' in expression:
|
||||
fieldname, local = expression.split('.')
|
||||
return local
|
||||
return expression
|
||||
|
||||
def is_leaf(expression):
|
||||
return (isinstance(expression, (list, tuple))
|
||||
and len(expression) > 2
|
||||
and isinstance(expression[1], basestring)
|
||||
and expression[1] in OPERATORS)
|
||||
|
||||
def eval_leaf(part, context):
|
||||
field, operand, value = part[:3]
|
||||
if '.' in field:
|
||||
# In the case where the leaf concerns a m2o then having a value in the
|
||||
# evaluation context is deemed suffisant
|
||||
return bool(context.get(field.split('.')[0]))
|
||||
return OPERATORS[operand](context[field], value)
|
||||
|
||||
def inverse_leaf(domain):
|
||||
if domain in ('AND', 'OR'):
|
||||
return domain
|
||||
elif is_leaf(domain):
|
||||
if 'child_of' in domain[1]:
|
||||
if len(domain) == 3:
|
||||
return domain
|
||||
else:
|
||||
return [domain[3]] + domain[1:]
|
||||
return domain
|
||||
else:
|
||||
return map(inverse_leaf, domain)
|
||||
|
||||
def eval_domain(domain, context, boolop=operator.and_):
|
||||
"compute domain boolean value according to the context"
|
||||
if is_leaf(domain):
|
||||
return eval_leaf(domain, context)
|
||||
elif not domain and boolop is operator.and_:
|
||||
return True
|
||||
elif not domain and boolop is operator.or_:
|
||||
return False
|
||||
elif domain[0] == 'AND':
|
||||
return eval_domain(domain[1:], context)
|
||||
elif domain[0] == 'OR':
|
||||
return eval_domain(domain[1:], context, operator.or_)
|
||||
else:
|
||||
return boolop(eval_domain(domain[0], context),
|
||||
eval_domain(domain[1:], context, boolop))
|
||||
|
||||
def localize_domain(domain):
|
||||
"returns only locale part of domain. eg: langage.code -> code"
|
||||
if domain in ('AND', 'OR', True, False):
|
||||
return domain
|
||||
elif is_leaf(domain):
|
||||
return [locale_part(domain[0])] + domain[1:]
|
||||
else:
|
||||
return [localize_domain(part) for part in domain]
|
||||
|
||||
def unlocalize_domain(domain, fieldname):
|
||||
if domain in ('AND', 'OR', True, False):
|
||||
return domain
|
||||
elif is_leaf(domain):
|
||||
return ['%s.%s' % (fieldname, domain[0])] + domain[1:]
|
||||
else:
|
||||
return [unlocalize_domain(part, fieldname) for part in domain]
|
||||
|
||||
def simplify(domain):
|
||||
"remove unused domain delimiter"
|
||||
if is_leaf(domain):
|
||||
return domain
|
||||
elif domain in ('OR', 'AND'):
|
||||
return domain
|
||||
elif (isinstance(domain, list) and len(domain) == 1
|
||||
and not is_leaf(domain[0])):
|
||||
return simplify(domain[0])
|
||||
elif (isinstance(domain, list) and len(domain) == 2
|
||||
and domain[0] in ('AND', 'OR')):
|
||||
return [simplify(domain[1])]
|
||||
else:
|
||||
return [simplify(branch) for branch in domain]
|
||||
|
||||
def merge(domain, domoperator=None):
|
||||
if not domain or domain in ('AND', 'OR'):
|
||||
return []
|
||||
domain_type = 'OR' if domain[0] == 'OR' else 'AND'
|
||||
if is_leaf(domain):
|
||||
return [domain]
|
||||
elif domoperator is None:
|
||||
return [domain_type] + reduce(operator.add,
|
||||
[merge(e, domain_type) for e in domain])
|
||||
elif domain_type == domoperator:
|
||||
return reduce(operator.add, [merge(e, domain_type) for e in domain])
|
||||
else:
|
||||
# without setting the domoperator
|
||||
return [merge(domain)]
|
||||
|
||||
|
||||
def parse(domain):
|
||||
if is_leaf(domain):
|
||||
return domain
|
||||
elif not domain:
|
||||
return And([])
|
||||
elif domain[0] == 'OR':
|
||||
return Or(domain[1:])
|
||||
else:
|
||||
return And(domain[1:] if domain[0] == 'AND' else domain)
|
||||
|
||||
def domain_inversion(domain, symbol, context=None):
|
||||
"""compute an inversion of the domain eventually the context is used to
|
||||
simplify the expression"""
|
||||
if context is None:
|
||||
context = {}
|
||||
expression = parse(domain)
|
||||
if symbol not in expression.variables:
|
||||
return True
|
||||
return expression.inverse(symbol, context)
|
||||
|
||||
|
||||
class And(object):
|
||||
|
||||
def __init__(self, expressions):
|
||||
self.branches = map(parse, expressions)
|
||||
self.variables = set()
|
||||
for expression in self.branches:
|
||||
if is_leaf(expression):
|
||||
self.variables.add(self.base(expression[0]))
|
||||
elif isinstance(expression, And):
|
||||
self.variables |= expression.variables
|
||||
|
||||
def base(self, expression):
|
||||
return expression if '.' not in expression else expression.split('.')[0]
|
||||
|
||||
def inverse(self, symbol, context):
|
||||
result = []
|
||||
for part in self.branches:
|
||||
if isinstance(part, And):
|
||||
part_inversion = part.inverse(symbol, context)
|
||||
evaluated = isinstance(part_inversion, types.BooleanType)
|
||||
if not evaluated:
|
||||
result.append(part_inversion)
|
||||
elif part_inversion:
|
||||
continue
|
||||
else:
|
||||
return False
|
||||
elif is_leaf(part) and self.base(part[0]) == symbol:
|
||||
result.append(part)
|
||||
else:
|
||||
field, _, _ = part
|
||||
if (field not in context
|
||||
or field in context and eval_leaf(part, context)):
|
||||
result.append(True)
|
||||
else:
|
||||
return False
|
||||
|
||||
result = filter(lambda e: e is not True, result)
|
||||
if result == []:
|
||||
return True
|
||||
else:
|
||||
return simplify(result)
|
||||
|
||||
|
||||
class Or(And):
|
||||
|
||||
def inverse(self, symbol, context):
|
||||
result = []
|
||||
known_variables = set(context.keys())
|
||||
if (symbol not in self.variables
|
||||
and not known_variables >= self.variables):
|
||||
# In this case we don't know anything about this OR part, we
|
||||
# consider it to be True (because people will have the constraint on
|
||||
# this part later).
|
||||
return True
|
||||
for part in self.branches:
|
||||
if isinstance(part, And):
|
||||
part_inversion = part.inverse(symbol, context)
|
||||
evaluated = isinstance(part_inversion, types.BooleanType)
|
||||
if symbol not in part.variables:
|
||||
if evaluated and part_inversion:
|
||||
return True
|
||||
continue
|
||||
if not evaluated:
|
||||
result.append(part_inversion)
|
||||
elif part_inversion:
|
||||
return True
|
||||
else:
|
||||
continue
|
||||
elif is_leaf(part) and self.base(part[0]) == symbol:
|
||||
result.append(part)
|
||||
else:
|
||||
field, _, _ = part
|
||||
field = self.base(field)
|
||||
if field in context and eval_leaf(part, context):
|
||||
return True
|
||||
elif field in context and not eval_leaf(part, context):
|
||||
result.append(False)
|
||||
|
||||
result = filter(lambda e: e is not False, result)
|
||||
if result == []:
|
||||
return False
|
||||
else:
|
||||
return simplify(['OR'] + result)
|
||||
|
||||
# Test stuffs
|
||||
def test_simple_inversion():
|
||||
domain = [['x', '=', 3]]
|
||||
assert domain_inversion(domain, 'x') == [['x', '=', 3]]
|
||||
|
||||
domain = []
|
||||
assert domain_inversion(domain, 'x') == True
|
||||
assert domain_inversion(domain, 'y') == True
|
||||
assert domain_inversion(domain, 'x', {'x': 5}) == True
|
||||
assert domain_inversion(domain, 'z', {'x': 7}) == True
|
||||
|
||||
domain = [['x.id', '>', 5]]
|
||||
assert domain_inversion(domain, 'x') == [['x.id', '>', 5]]
|
||||
|
||||
def test_and_inversion():
|
||||
domain = [['x', '=', 3], ['y', '>', 5]]
|
||||
assert domain_inversion(domain, 'x') == [['x', '=', 3]]
|
||||
assert domain_inversion(domain, 'x', {'y': 4}) == False
|
||||
assert domain_inversion(domain, 'x', {'y': 6}) == [['x', '=', 3]]
|
||||
|
||||
domain = [['x', '=', 3], ['y', '=', 5]]
|
||||
assert domain_inversion(domain, 'z') == True
|
||||
assert domain_inversion(domain, 'z', {'x': 2, 'y': 7}) == True
|
||||
|
||||
domain = [['x.id', '>', 5], ['y', '<', 3]]
|
||||
assert domain_inversion(domain, 'y') == [['y', '<', 3]]
|
||||
assert domain_inversion(domain, 'y', {'x': 3}) == [['y', '<', 3]]
|
||||
assert domain_inversion(domain, 'x') == [['x.id', '>', 5]]
|
||||
|
||||
def test_or_inversion():
|
||||
domain = ['OR', ['x', '=', 3], ['y', '>', 5], ['z', '=', 'abc']]
|
||||
assert domain_inversion(domain, 'x') == [['x', '=', 3]]
|
||||
assert domain_inversion(domain, 'x', {'y': 4}) == [['x', '=', 3]]
|
||||
assert domain_inversion(domain, 'x', {'y': 4, 'z': 'ab'}) == [['x', '=', 3]]
|
||||
assert domain_inversion(domain, 'x', {'y': 7}) == True
|
||||
assert domain_inversion(domain, 'x', {'y': 7, 'z': 'b'}) == True
|
||||
assert domain_inversion(domain, 'x', {'z': 'abc'}) == True
|
||||
assert domain_inversion(domain, 'x', {'y': 4, 'z': 'abc'}) == True
|
||||
|
||||
domain = ['OR', ['x', '=', 3], ['y', '>', 5]]
|
||||
assert domain_inversion(domain, 'z') == True
|
||||
|
||||
domain = ['OR', ['x.id', '>', 5], ['y', '<', 3]]
|
||||
assert domain_inversion(domain, 'y') == [['y', '<', 3]]
|
||||
assert domain_inversion(domain, 'y', {'z': 4}) == [['y', '<', 3]]
|
||||
assert domain_inversion(domain, 'y', {'x': 3}) == True
|
||||
|
||||
domain = [u'OR', [u'length', u'>', 5], [u'language.code', u'=', u'de_DE']]
|
||||
assert domain_inversion(domain, 'length', {'length': 0, 'name': 'n'}) ==\
|
||||
[['length', '>', 5]]
|
||||
|
||||
def test_orand_inversion():
|
||||
domain = ['OR', [['x', '=', 3], ['y', '>', 5], ['z', '=', 'abc']],
|
||||
[['x', '=', 4]], [['y', '>', 6]]]
|
||||
assert domain_inversion(domain, 'x') == True
|
||||
assert domain_inversion(domain, 'x', {'y': 4}) == [[['x', '=', 4]]]
|
||||
assert domain_inversion(domain, 'x', {'z': 'abc', 'y': 7}) == True
|
||||
assert domain_inversion(domain, 'x', {'y': 7}) == True
|
||||
assert domain_inversion(domain, 'x', {'z': 'ab'}) == True
|
||||
|
||||
def test_andor_inversion():
|
||||
domain = [['OR', ['x', '=', 4], ['y', '>', 6]], ['z', '=', 3]]
|
||||
assert domain_inversion(domain, 'z') == [['z', '=', 3]]
|
||||
assert domain_inversion(domain, 'z', {'x': 5}) == [['z', '=', 3]]
|
||||
assert domain_inversion(domain, 'z', {'x': 5, 'y': 5}) == False
|
||||
assert domain_inversion(domain, 'z', {'x': 5, 'y': 7}) == [['z', '=', 3]]
|
||||
|
||||
def test_andand_inversion():
|
||||
domain = [[['x', '=', 4], ['y', '>', 6]], ['z', '=', 3]]
|
||||
assert domain_inversion(domain, 'z') == [['z', '=', 3]]
|
||||
assert domain_inversion(domain, 'z', {'x': 5}) == False
|
||||
assert domain_inversion(domain, 'z', {'y': 5}) == False
|
||||
assert domain_inversion(domain, 'z', {'x': 4, 'y': 7}) == [['z', '=', 3]]
|
||||
|
||||
domain = [[['x', '=', 4], ['y', '>', 6], ['z', '=', 2]], [['w', '=', 2]]]
|
||||
assert domain_inversion(domain, 'z', {'x': 4}) == [['z', '=', 2]]
|
||||
|
||||
def test_oror_inversion():
|
||||
domain = ['OR', ['OR', ['x', '=', 3], ['y', '>', 5]],
|
||||
['OR', ['x', '=', 2], ['z', '=', 'abc']],
|
||||
['OR', ['y', '=', 8], ['z', '=', 'y']]]
|
||||
assert domain_inversion(domain, 'x') == True
|
||||
assert domain_inversion(domain, 'x', {'y': 4}) == True
|
||||
assert domain_inversion(domain, 'x', {'z': 'ab'}) == True
|
||||
assert domain_inversion(domain, 'x', {'y': 7}) == True
|
||||
assert domain_inversion(domain, 'x', {'z': 'abc'}) == True
|
||||
assert domain_inversion(domain, 'x', {'z': 'y'}) == True
|
||||
assert domain_inversion(domain, 'x', {'y': 8}) == True
|
||||
assert domain_inversion(domain, 'x', {'y': 8, 'z': 'b'}) == True
|
||||
assert domain_inversion(domain, 'x', {'y': 4, 'z': 'y'}) == True
|
||||
assert domain_inversion(domain, 'x', {'y': 7, 'z': 'abc'}) == True
|
||||
assert domain_inversion(domain, 'x', {'y': 4, 'z': 'b'}) == \
|
||||
['OR', [['x', '=', 3]], [['x', '=', 2]]]
|
||||
|
||||
def test_parse():
|
||||
domain = parse([['x', '=', 5]])
|
||||
assert domain.variables == set('x')
|
||||
domain = parse(['OR', ['x', '=', 4], ['y', '>', 6]])
|
||||
assert domain.variables == set('xy')
|
||||
domain = parse([['OR', ['x', '=', 4], ['y', '>', 6]], ['z', '=', 3]])
|
||||
assert domain.variables == set('xyz')
|
||||
domain = parse([[['x', '=', 4], ['y', '>', 6]], ['z', '=', 3]])
|
||||
assert domain.variables == set('xyz')
|
||||
|
||||
def test_simplify():
|
||||
domain = [['x', '=', 3]]
|
||||
assert simplify(domain) == [['x', '=', 3]]
|
||||
domain = [[['x', '=', 3]]]
|
||||
assert simplify(domain) == [['x', '=', 3]]
|
||||
domain = ['OR', ['x', '=', 3]]
|
||||
assert simplify(domain) == [['x', '=', 3]]
|
||||
domain = ['OR', [['x', '=', 3]], [['y', '=', 5]]]
|
||||
assert simplify(domain) == ['OR', [['x', '=', 3]], [['y', '=', 5]]]
|
||||
domain = ['OR', ['x', '=', 3], ['AND', ['y', '=', 5]]]
|
||||
assert simplify(domain) == ['OR', ['x', '=', 3], ['y', '=', 5]]
|
||||
|
||||
def test_merge():
|
||||
domain = [['x', '=', 6], ['y', '=', 7]]
|
||||
assert merge(domain) == ['AND', ['x', '=', 6], ['y', '=', 7]]
|
||||
domain = ['AND', ['x', '=', 6], ['y', '=', 7]]
|
||||
assert merge(domain) == ['AND', ['x', '=', 6], ['y', '=', 7]]
|
||||
domain = [['z', '=', 8], ['AND', ['x', '=', 6], ['y', '=', 7]]]
|
||||
assert merge(domain) == ['AND', ['z', '=', 8], ['x', '=', 6],
|
||||
['y', '=', 7]]
|
||||
domain = ['OR', ['x', '=', 1], ['y', '=', 2], ['z', '=', 3]]
|
||||
assert merge(domain) == ['OR', ['x', '=', 1], ['y', '=', 2],
|
||||
['z', '=', 3]]
|
||||
domain = ['OR', ['x', '=', 1], ['OR', ['y', '=', 2], ['z', '=', 3]]]
|
||||
assert merge(domain) == ['OR', ['x', '=', 1], ['y', '=', 2],
|
||||
['z', '=', 3]]
|
||||
domain = ['OR', ['x', '=', 1], ['AND', ['y', '=', 2], ['z', '=', 3]]]
|
||||
assert merge(domain) == ['OR', ['x', '=', 1], ['AND', ['y', '=', 2],
|
||||
['z', '=', 3]]]
|
||||
domain = [['z', '=', 8], ['OR', ['x', '=', 6], ['y', '=', 7]]]
|
||||
assert merge(domain) == ['AND', ['z', '=', 8], ['OR', ['x', '=', 6],
|
||||
['y', '=', 7]]]
|
||||
domain = ['AND', ['OR', ['a', '=', 1], ['b', '=', 2]],
|
||||
['OR', ['c', '=', 3], ['AND', ['d', '=', 4], ['d2', '=', 6]]],
|
||||
['AND', ['d', '=', 5], ['e', '=', 6]], ['f', '=', 7]]
|
||||
assert merge(domain) == ['AND', ['OR', ['a', '=', 1], ['b', '=', 2]],
|
||||
['OR', ['c', '=', 3], ['AND', ['d', '=', 4], ['d2', '=', 6]]],
|
||||
['d', '=', 5], ['e', '=', 6], ['f', '=', 7]]
|
||||
|
||||
def test_evaldomain():
|
||||
domain = [['x', '>', 5]]
|
||||
assert eval_domain(domain, {'x': 6})
|
||||
assert not eval_domain(domain, {'x': 4})
|
||||
|
||||
domain = ['OR', ['x', '>', 10], ['x', '<', 0]]
|
||||
assert eval_domain(domain, {'x': 11})
|
||||
assert eval_domain(domain, {'x': -4})
|
||||
assert not eval_domain(domain, {'x': 5})
|
||||
|
||||
domain = [['x', '>', 0], ['OR', ['x', '=', 3], ['x', '=', 2]]]
|
||||
assert not eval_domain(domain, {'x': 1})
|
||||
assert eval_domain(domain, {'x': 3})
|
||||
assert eval_domain(domain, {'x': 2})
|
||||
assert not eval_domain(domain, {'x': 4})
|
||||
assert not eval_domain(domain, {'x': 5})
|
||||
assert not eval_domain(domain, {'x': 6})
|
||||
|
||||
domain = ['OR', ['x', '=', 4], [['x', '>', 6], ['x', '<', 10]]]
|
||||
assert eval_domain(domain, {'x': 4})
|
||||
assert eval_domain(domain, {'x': 7})
|
||||
assert not eval_domain(domain, {'x': 3})
|
||||
assert not eval_domain(domain, {'x': 5})
|
||||
assert not eval_domain(domain, {'x': 11})
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_simple_inversion()
|
||||
test_and_inversion()
|
||||
test_or_inversion()
|
||||
test_orand_inversion()
|
||||
test_andor_inversion()
|
||||
test_andand_inversion()
|
||||
test_oror_inversion()
|
||||
test_parse()
|
||||
test_simplify()
|
||||
test_evaldomain()
|
||||
@@ -0,0 +1,38 @@
|
||||
#This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
#this repository contains the full copyright notices and license terms.
|
||||
|
||||
|
||||
class EvalEnvironment(dict):
|
||||
|
||||
def __init__(self, parent, check_load):
|
||||
super(EvalEnvironment, self).__init__()
|
||||
self.parent = parent
|
||||
self.check_load = check_load
|
||||
|
||||
def __getitem__(self, item):
|
||||
if item == '_parent_' + self.parent.parent_name and self.parent.parent:
|
||||
return EvalEnvironment(self.parent.parent, self.check_load)
|
||||
return self.parent.get_eval(check_load=self.check_load)[item]
|
||||
|
||||
def __getattr__(self, item):
|
||||
return self.__getitem__(item)
|
||||
|
||||
def get(self, item, default=None):
|
||||
try:
|
||||
return self.__getattr__(item)
|
||||
except Exception:
|
||||
pass
|
||||
return super(EvalEnvironment, self).get(item, default)
|
||||
|
||||
def __nonzero__(self):
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return str(self.parent)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __contains__(self, item):
|
||||
if item == '_parent_' + self.parent.parent_name and self.parent.parent:
|
||||
return item in EvalEnvironment(self.parent.parent, self.check_load)
|
||||
return item in self.parent.get_eval(check_load=self.check_load)
|
||||
@@ -1,7 +1,9 @@
|
||||
#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 tryton.rpc as rpc
|
||||
from tryton.common import DT_FORMAT, DHM_FORMAT, HM_FORMAT, datetime_strftime
|
||||
from tryton.common import DT_FORMAT, DHM_FORMAT, HM_FORMAT, datetime_strftime, \
|
||||
domain_inversion, eval_domain, localize_domain, unlocalize_domain, \
|
||||
merge, inverse_leaf, EvalEnvironment
|
||||
import tryton.common as common
|
||||
from tryton.pyson import PYSONDecoder
|
||||
import time
|
||||
@@ -41,8 +43,27 @@ class CharField(object):
|
||||
record.cond_default(self.attrs['name'], self.get(record))
|
||||
record.on_change_with(self.name)
|
||||
|
||||
def domains_get(self, record):
|
||||
screen_domain = domain_inversion(record.group.domain,
|
||||
self.name, EvalEnvironment(record, False))
|
||||
if isinstance(screen_domain, bool) and not screen_domain:
|
||||
screen_domain = [('id', '=', False)]
|
||||
elif isinstance(screen_domain, bool) and screen_domain:
|
||||
screen_domain = []
|
||||
attr_domain = record.expr_eval(self.attrs.get('domain', []))
|
||||
return screen_domain, attr_domain
|
||||
|
||||
def domain_get(self, record):
|
||||
return record.expr_eval(self.attrs.get('domain', []))
|
||||
screen_domain, attr_domain = self.domains_get(record)
|
||||
return localize_domain(screen_domain) + attr_domain
|
||||
|
||||
def validation_domains(self, record):
|
||||
screen_domain, attr_domain = self.domains_get(record)
|
||||
if attr_domain:
|
||||
return screen_domain, screen_domain + unlocalize_domain(attr_domain,
|
||||
self.name)
|
||||
else:
|
||||
return screen_domain, screen_domain
|
||||
|
||||
def context_get(self, record, check_load=True, eval_context=True):
|
||||
context = {}
|
||||
@@ -52,13 +73,39 @@ class CharField(object):
|
||||
check_load=check_load))
|
||||
return context
|
||||
|
||||
def validate(self, record):
|
||||
def validate(self, record, softvalidation=False):
|
||||
res = True
|
||||
if bool(int(self.get_state_attrs(record).get('required') or 0)):
|
||||
if not self.get(record) \
|
||||
and not bool(int(self.get_state_attrs(record
|
||||
).get('readonly') or 0)):
|
||||
res = False
|
||||
inverted_domain, domain = self.validation_domains(record)
|
||||
if not softvalidation:
|
||||
if bool(int(self.get_state_attrs(record).get('required') or 0)):
|
||||
if not self.get(record) \
|
||||
and not bool(int(self.get_state_attrs(record
|
||||
).get('readonly') or 0)):
|
||||
res = False
|
||||
if isinstance(domain, bool):
|
||||
res = res and domain
|
||||
else:
|
||||
if (isinstance(inverted_domain, list) \
|
||||
and len(inverted_domain) == 1 and inverted_domain[0][1] == '='):
|
||||
# If the inverted domain is so constraint that only one value is
|
||||
# possible we should use it. But we must also pay attention to
|
||||
# the fact that the original domain might be a 'OR' domain and
|
||||
# thus not preventing the modification of fields.
|
||||
leftpart, _, value = inverted_domain[0][:3]
|
||||
setdefault = True
|
||||
if '.' in leftpart:
|
||||
recordpart, localpart = leftpart.split('.', 1)
|
||||
original_domain = merge(record.group.admin)
|
||||
constraintfields = set()
|
||||
if original_domain[0] == 'AND':
|
||||
for leaf in localize_domain(original_domain[1:]):
|
||||
constraintfields.add(leaf[0])
|
||||
if localpart != 'id' or recordpart not in constraintfields:
|
||||
setdefault = False
|
||||
if setdefault:
|
||||
self.set_client(record, value)
|
||||
self.get_state_attrs(record)['domain_readonly'] = True
|
||||
res = res and eval_domain(domain, EvalEnvironment(record, False))
|
||||
self.get_state_attrs(record)['valid'] = res
|
||||
return res
|
||||
|
||||
@@ -92,6 +139,7 @@ class CharField(object):
|
||||
record.value[self.name] = internal
|
||||
record.modified = prev_modified
|
||||
return
|
||||
record.validate(softvalidation=True)
|
||||
record.signal('record-changed')
|
||||
|
||||
def get_client(self, record):
|
||||
@@ -117,7 +165,8 @@ class CharField(object):
|
||||
self.get_state_attrs(record)[key] = state_changes[key]
|
||||
elif key in self.attrs:
|
||||
self.get_state_attrs(record)[key] = self.attrs[key]
|
||||
if record.group.readonly:
|
||||
if (record.group.readonly
|
||||
or self.get_state_attrs(record).get('domain_readonly')):
|
||||
self.get_state_attrs(record)['readonly'] = True
|
||||
if 'value' in state_changes:
|
||||
value = state_changes['value']
|
||||
@@ -194,6 +243,7 @@ class FloatField(CharField):
|
||||
except Exception:
|
||||
record.value[self.name] = internal
|
||||
record.modified = prev_modified
|
||||
record.validate(softvalidation=True)
|
||||
record.signal('record-changed')
|
||||
|
||||
|
||||
@@ -217,6 +267,7 @@ class NumericField(CharField):
|
||||
record.value[self.name] = internal
|
||||
record.modified = prev_modified
|
||||
return
|
||||
record.validate(softvalidation=True)
|
||||
record.signal('record-changed')
|
||||
|
||||
|
||||
@@ -244,6 +295,7 @@ class BooleanField(CharField):
|
||||
record.value[self.name] = internal
|
||||
record.modified = prev_modified
|
||||
return
|
||||
record.validate(softvalidation=True)
|
||||
record.signal('record-changed')
|
||||
|
||||
def get(self, record, check_load=True, readonly=True, modified=False):
|
||||
@@ -314,6 +366,7 @@ class M2OField(CharField):
|
||||
record.value[self.name] = internal
|
||||
record.modified = prev_modified
|
||||
return
|
||||
record.validate(softvalidation=True)
|
||||
record.signal('record-changed')
|
||||
elif force_change:
|
||||
try:
|
||||
@@ -321,6 +374,7 @@ class M2OField(CharField):
|
||||
except Exception:
|
||||
record.value[self.name] = internal
|
||||
return
|
||||
record.validate(softvalidation=True)
|
||||
record.signal('record-changed')
|
||||
|
||||
def context_get(self, record, check_load=True, eval_context=True):
|
||||
@@ -331,6 +385,14 @@ class M2OField(CharField):
|
||||
check_load=check_load)[self.attrs.get('datetime_field')]
|
||||
return context
|
||||
|
||||
def validation_domains(self, record):
|
||||
screen_domain, attr_domain = self.domains_get(record)
|
||||
return screen_domain, screen_domain
|
||||
|
||||
def domain_get(self, record):
|
||||
screen_domain, attr_domain = self.domains_get(record)
|
||||
return localize_domain(inverse_leaf(screen_domain)) + attr_domain
|
||||
|
||||
|
||||
class O2OField(M2OField):
|
||||
pass
|
||||
@@ -349,6 +411,7 @@ class O2MField(CharField):
|
||||
record.parent.modified = True
|
||||
record.parent.modified_fields.setdefault(self.name)
|
||||
self.sig_changed(record.parent)
|
||||
record.parent.validate(softvalidation=True)
|
||||
record.parent.signal('record-changed')
|
||||
|
||||
def _set_default_value(self, record):
|
||||
@@ -432,9 +495,11 @@ class O2MField(CharField):
|
||||
def set_default(self, record, value, modified=False):
|
||||
from group import Group
|
||||
|
||||
# value is a list of id
|
||||
if value and len(value) and isinstance(value[0], (int, long)):
|
||||
return self.set(record, value, modified=modified)
|
||||
|
||||
# value is a list of dict
|
||||
fields = {}
|
||||
if value and len(value):
|
||||
context = self.context_get(record)
|
||||
@@ -529,9 +594,13 @@ class O2MField(CharField):
|
||||
res = [x.get_default() for x in record.value.get(self.name) or []]
|
||||
return res
|
||||
|
||||
def validate(self, record):
|
||||
def validation_domains(self, record):
|
||||
screen_domain, attr_domain = self.domains_get(record)
|
||||
return screen_domain, screen_domain
|
||||
|
||||
def validate(self, record, softvalidation=False):
|
||||
res = True
|
||||
for record2 in record.value[self.name] or []:
|
||||
for record2 in record.value.get(self.name, []):
|
||||
if not record2.loaded:
|
||||
continue
|
||||
if not record2.validate():
|
||||
@@ -539,7 +608,7 @@ class O2MField(CharField):
|
||||
record.value[self.name].remove(record2)
|
||||
else:
|
||||
res = False
|
||||
if not super(O2MField, self).validate(record):
|
||||
if not super(O2MField, self).validate(record, softvalidation):
|
||||
res = False
|
||||
self.get_state_attrs(record)['valid'] = res
|
||||
return res
|
||||
@@ -555,6 +624,10 @@ class O2MField(CharField):
|
||||
def get_removed_ids(self, record):
|
||||
return [x.id for x in record.value[self.name].record_removed]
|
||||
|
||||
def domain_get(self, record):
|
||||
screen_domain, attr_domain = self.domains_get(record)
|
||||
return localize_domain(inverse_leaf(screen_domain)) + attr_domain
|
||||
|
||||
|
||||
class M2MField(O2MField):
|
||||
|
||||
@@ -615,6 +688,7 @@ class ReferenceField(CharField):
|
||||
record.value[self.name] = internal
|
||||
record.modified = prev_modified
|
||||
return
|
||||
record.validate(softvalidation=True)
|
||||
record.signal('record-changed')
|
||||
|
||||
def set(self, record, value, modified=False):
|
||||
|
||||
@@ -10,9 +10,12 @@ import tryton.common as common
|
||||
class Group(SignalEvent, list):
|
||||
|
||||
def __init__(self, model_name, fields, window, ids=None, parent=None,
|
||||
parent_name='', context=None, readonly=False,
|
||||
parent_name='', context=None, domain=None, readonly=False,
|
||||
parent_datetime_field=None):
|
||||
super(Group, self).__init__()
|
||||
if domain is None:
|
||||
domain = []
|
||||
self.domain = domain
|
||||
self.lock_signal = False
|
||||
self.__window = window
|
||||
self.parent = parent
|
||||
|
||||
@@ -10,45 +10,10 @@ import logging
|
||||
import time
|
||||
|
||||
|
||||
class EvalEnvironment(dict):
|
||||
|
||||
def __init__(self, parent, check_load):
|
||||
super(EvalEnvironment, self).__init__()
|
||||
self.parent = parent
|
||||
self.check_load = check_load
|
||||
|
||||
def __getitem__(self, item):
|
||||
if item == '_parent_' + self.parent.parent_name and self.parent.parent:
|
||||
return EvalEnvironment(self.parent.parent, self.check_load)
|
||||
return self.parent.get_eval(check_load=self.check_load)[item]
|
||||
|
||||
def __getattr__(self, item):
|
||||
return self.__getitem__(item)
|
||||
|
||||
def get(self, item, default=None):
|
||||
try:
|
||||
return self.__getattr__(item)
|
||||
except Exception:
|
||||
pass
|
||||
return super(EvalEnvironment, self).get(item, default)
|
||||
|
||||
def __nonzero__(self):
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return str(self.parent)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.parent.group.fields
|
||||
|
||||
|
||||
class Record(SignalEvent):
|
||||
|
||||
id = -1
|
||||
|
||||
|
||||
def __init__(self, model_name, obj_id, window, group=None, parent=None,
|
||||
parent_name=''):
|
||||
super(Record, self).__init__()
|
||||
@@ -262,32 +227,16 @@ class Record(SignalEvent):
|
||||
return ''
|
||||
return res['rec_name']
|
||||
|
||||
def validate_set(self, fields=None):
|
||||
if isinstance(fields, list) and fields:
|
||||
self._check_load(fields)
|
||||
elif fields is None:
|
||||
self._check_load()
|
||||
change = False
|
||||
for name, field in self.group.fields.iteritems():
|
||||
if fields and name not in fields:
|
||||
continue
|
||||
change = change or \
|
||||
not field.get_state_attrs(self).get('valid', True)
|
||||
field.get_state_attrs(self)['valid'] = True
|
||||
if change:
|
||||
self.signal('record-changed')
|
||||
return change
|
||||
|
||||
def validate(self, fields=None):
|
||||
def validate(self, fields=None, softvalidation=False):
|
||||
if isinstance(fields, list) and fields:
|
||||
self._check_load(fields)
|
||||
elif fields is None:
|
||||
self._check_load()
|
||||
res = True
|
||||
for name, field in self.group.fields.iteritems():
|
||||
if fields and name not in fields:
|
||||
for field_name, field in self.group.fields.iteritems():
|
||||
if fields and field_name not in fields:
|
||||
continue
|
||||
if not field.validate(self):
|
||||
if not field.validate(self, softvalidation):
|
||||
res = False
|
||||
return res
|
||||
|
||||
@@ -325,8 +274,9 @@ class Record(SignalEvent):
|
||||
else:
|
||||
value = ref_model, (ref_id, ref_id)
|
||||
self.group.fields[fieldname].set_default(self, value,
|
||||
modified=modified)
|
||||
modified=modified)
|
||||
self._loaded.add(fieldname)
|
||||
self.validate(softvalidation=True)
|
||||
if signal:
|
||||
self.signal('record-changed')
|
||||
|
||||
@@ -386,8 +336,8 @@ class Record(SignalEvent):
|
||||
ctx['active_id'] = self.id
|
||||
ctx['_user'] = rpc._USER
|
||||
if self.parent and self.parent_name:
|
||||
ctx['_parent_' + self.parent_name] = EvalEnvironment(self.parent,
|
||||
check_load)
|
||||
ctx['_parent_' + self.parent_name] = \
|
||||
common.EvalEnvironment(self.parent, check_load)
|
||||
val = PYSONDecoder(ctx).decode(expr)
|
||||
return val
|
||||
|
||||
@@ -397,8 +347,8 @@ class Record(SignalEvent):
|
||||
for name, field in self.group.fields.iteritems():
|
||||
values[name] = field.get_on_change_value(self, check_load=False)
|
||||
if self.parent and self.parent_name:
|
||||
values['_parent_' + self.parent_name] = EvalEnvironment(self.parent,
|
||||
False)
|
||||
values['_parent_' + self.parent_name] = \
|
||||
common.EvalEnvironment(self.parent, False)
|
||||
for arg in args:
|
||||
scope = values
|
||||
for i in arg.split('.'):
|
||||
|
||||
@@ -194,8 +194,8 @@ class Screen(SignalEvent):
|
||||
group = property(__get_group, __set_group)
|
||||
|
||||
def new_group(self, readonly=False):
|
||||
self.group = Group(self.model_name, {}, self.window,
|
||||
context=self.context, readonly=readonly)
|
||||
self.group = Group(self.model_name, {}, self.window, domain=self.domain,
|
||||
context=self.context, readonly=readonly)
|
||||
|
||||
def _group_cleared(self, group, signal):
|
||||
for view in self.views:
|
||||
@@ -288,11 +288,8 @@ class Screen(SignalEvent):
|
||||
elif self.current_view.view_type == view_type:
|
||||
break
|
||||
self.screen_container.set(self.current_view.widget)
|
||||
if self.current_record:
|
||||
self.current_record.validate_set(fields)
|
||||
else:
|
||||
if self.current_view.view_type == 'form':
|
||||
self.new(default=default, context=context)
|
||||
if not self.current_record and self.current_view.view_type == 'form':
|
||||
self.new(default=default, context=context)
|
||||
self.current_view.cancel()
|
||||
self.display(set_cursor=True)
|
||||
|
||||
@@ -332,34 +329,8 @@ class Screen(SignalEvent):
|
||||
def add_view(self, arch, fields, display=False, toolbar=None, context=None):
|
||||
if toolbar is None:
|
||||
toolbar = {}
|
||||
|
||||
xml_dom = xml.dom.minidom.parseString(arch)
|
||||
|
||||
for dom in common.filter_domain(self.domain):
|
||||
if '.' in dom[0]:
|
||||
field1, field2 = dom[0].split('.', 1)
|
||||
else:
|
||||
field1, field2 = dom[0], 'id'
|
||||
if field1 in fields:
|
||||
field_dom = fields[field1].setdefault('domain', [])
|
||||
if dom[1] in ('child_of', 'not child_of') \
|
||||
and field2 == 'id':
|
||||
dom = copy.copy(dom)
|
||||
if len(dom) == 4:
|
||||
field2 = dom[3]
|
||||
dom = (dom[0], dom[1], dom[2])
|
||||
else:
|
||||
field2 = field1
|
||||
if isinstance(field_dom, basestring):
|
||||
fields[field1]['domain'] = '[' \
|
||||
+ str(tuple([field2] + list(dom[1:]))) \
|
||||
+ ',' + field_dom + ']'
|
||||
else:
|
||||
fields[field1]['domain'] = [
|
||||
tuple([field2] + list(dom[1:])),
|
||||
field_dom]
|
||||
if dom[1] == '!=' and dom[2] == False:
|
||||
fields[field1]['required'] = True
|
||||
|
||||
for node in xml_dom.childNodes:
|
||||
if node.localName == 'tree':
|
||||
self.fields_view_tree = {'arch': arch, 'fields': fields}
|
||||
@@ -396,11 +367,9 @@ class Screen(SignalEvent):
|
||||
self.__current_view = len(self.views) - 1
|
||||
self.screen_container.set(self.current_view.widget)
|
||||
fields = self.current_view.get_fields()
|
||||
if self.current_record:
|
||||
self.current_record.validate_set(fields)
|
||||
else:
|
||||
if self.current_view.view_type == 'form':
|
||||
self.new()
|
||||
if (not self.current_record
|
||||
and self.current_view.view_type == 'form'):
|
||||
self.new()
|
||||
self.current_view.set_cursor()
|
||||
self.current_view.cancel()
|
||||
self.display()
|
||||
@@ -442,7 +411,6 @@ class Screen(SignalEvent):
|
||||
fields = None
|
||||
if self.current_view:
|
||||
fields = self.current_view.get_fields()
|
||||
self.current_record.validate_set(fields)
|
||||
self.display()
|
||||
if self.current_view:
|
||||
self.current_view.set_cursor(new=True)
|
||||
@@ -671,9 +639,6 @@ class Screen(SignalEvent):
|
||||
self.current_record = len(self.group) \
|
||||
and self.group[0]
|
||||
self.current_view.set_cursor(reset_view=False)
|
||||
if self.current_record:
|
||||
fields = self.current_view.get_fields()
|
||||
self.current_record.validate_set(fields)
|
||||
self.current_view.display()
|
||||
|
||||
def display_prev(self):
|
||||
@@ -701,9 +666,6 @@ class Screen(SignalEvent):
|
||||
self.current_record = len(self.group) \
|
||||
and self.group[-1]
|
||||
self.current_view.set_cursor(reset_view=False)
|
||||
if self.current_record:
|
||||
fields = self.current_view.get_fields()
|
||||
self.current_record.validate_set(fields)
|
||||
self.current_view.display()
|
||||
|
||||
def sel_ids_get(self):
|
||||
|
||||
@@ -26,19 +26,18 @@ class Many2One(WidgetInterface):
|
||||
|
||||
self.widget = gtk.HBox(spacing=0)
|
||||
self.widget.set_property('sensitive', True)
|
||||
self.widget.connect('focus-in-event', lambda x, y: self._focus_in())
|
||||
self.widget.connect('focus-out-event', lambda x, y: self._focus_out())
|
||||
|
||||
self.wid_text = gtk.Entry()
|
||||
self.wid_text.set_property('width-chars', 13)
|
||||
self.wid_text.set_property('activates_default', True)
|
||||
self.wid_text.connect_after('key_press_event', self.sig_key_press)
|
||||
self.wid_text.connect('populate-popup', self._populate_popup)
|
||||
self.wid_text.connect('focus-in-event', lambda x, y: self._focus_in())
|
||||
self.wid_text.connect('focus-out-event', lambda x, y: self._focus_out())
|
||||
self.wid_text.connect_after('changed', self.sig_changed)
|
||||
self.changed = True
|
||||
self.wid_text.connect_after('activate', self.sig_activate)
|
||||
self.wid_text.connect_after('focus-out-event',
|
||||
self.sig_activate)
|
||||
self.wid_text.connect_after('focus-out-event', self.sig_activate)
|
||||
self.focus_out = True
|
||||
self.widget.pack_start(self.wid_text, expand=True, fill=True)
|
||||
|
||||
|
||||
Reference in new issue
Block a user