diff --git a/CHANGELOG b/CHANGELOG index 77a43abba1..f222a5bc7e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Added domain inversion feature * Handle loading attribute on fields * Use default format for value in wizard form * Add One2One field diff --git a/COPYRIGHT b/COPYRIGHT index 1a619f1e33..42539e2dbc 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -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. diff --git a/tryton/common/__init__.py b/tryton/common/__init__.py index afab529df9..68979ed8d6 100644 --- a/tryton/common/__init__.py +++ b/tryton/common/__init__.py @@ -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 diff --git a/tryton/common/domain_inversion.py b/tryton/common/domain_inversion.py new file mode 100644 index 0000000000..014ffbddd0 --- /dev/null +++ b/tryton/common/domain_inversion.py @@ -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() diff --git a/tryton/common/environment.py b/tryton/common/environment.py new file mode 100644 index 0000000000..296cf9eee6 --- /dev/null +++ b/tryton/common/environment.py @@ -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) diff --git a/tryton/gui/window/view_form/model/field.py b/tryton/gui/window/view_form/model/field.py index 8f82cadd77..5bb2017103 100644 --- a/tryton/gui/window/view_form/model/field.py +++ b/tryton/gui/window/view_form/model/field.py @@ -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): diff --git a/tryton/gui/window/view_form/model/group.py b/tryton/gui/window/view_form/model/group.py index 8014284257..c04636e9fe 100644 --- a/tryton/gui/window/view_form/model/group.py +++ b/tryton/gui/window/view_form/model/group.py @@ -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 diff --git a/tryton/gui/window/view_form/model/record.py b/tryton/gui/window/view_form/model/record.py index 066d4d62a5..0f7e9bca4d 100644 --- a/tryton/gui/window/view_form/model/record.py +++ b/tryton/gui/window/view_form/model/record.py @@ -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('.'): diff --git a/tryton/gui/window/view_form/screen/screen.py b/tryton/gui/window/view_form/screen/screen.py index 2bb1bc6948..bbef413001 100644 --- a/tryton/gui/window/view_form/screen/screen.py +++ b/tryton/gui/window/view_form/screen/screen.py @@ -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): diff --git a/tryton/gui/window/view_form/view/form_gtk/many2one.py b/tryton/gui/window/view_form/view/form_gtk/many2one.py index d5e66a36b9..f0597d5a1f 100644 --- a/tryton/gui/window/view_form/view/form_gtk/many2one.py +++ b/tryton/gui/window/view_form/view/form_gtk/many2one.py @@ -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)