diff --git a/CHANGELOG b/CHANGELOG
index 6fafb29105..6581fb64a9 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Add PySON to replace python string evaluation on client side
* Add JSON-RPC
* Add groups on Sequence Types and add rules on sequence based on it.
* Ignore Fields that starts with "_"
diff --git a/INSTALL b/INSTALL
index 4a6eb2bf9f..3fd31829fe 100644
--- a/INSTALL
+++ b/INSTALL
@@ -8,6 +8,7 @@ Prerequisites
* lxml (http://codespeak.net/lxml/)
* relatorio 0.2.0 or later (http://relatorio.openhex.org/)
* python-dateutil (http://labix.org/python-dateutil)
+ * simplejson (http://undefined.org/python/#simplejson)
* Optional: psycopg 2 or later (http://www.initd.org/)
* Optional: pywebdav 0.9.3 or later (http://code.google.com/p/pywebdav/)
* Optional: pydot (http://code.google.com/p/pydot/)
diff --git a/doc/models.rst b/doc/models.rst
index ed8eacbb23..a0cbeb2760 100644
--- a/doc/models.rst
+++ b/doc/models.rst
@@ -347,7 +347,7 @@ the field.
'Sale'
_name = 'sale.sale'
sale_date = fields.Date('Sale Date', required=True, states={
- 'readonly': "state != 'draft'",
+ 'readonly': Not(Equal(Eval('state'), 'draft')),
})
def default_sale_date(self, cursor, user, context=None):
date_obj = self.pool.get('ir.date')
@@ -390,7 +390,8 @@ many dependencies there are.
customer = fields.Many2One('party.party', 'Customer', required=True,
states={
- 'readonly': "state != 'draft' or bool(outgoing_moves)",
+ 'readonly': Or(Not(Equal(Eval('state'), 'draft')),
+ Bool(Eval('outgoing_moves'))),
}, on_change=['customer'])
def on_change_customer(self, cursor, user, ids, values, context=None):
@@ -415,11 +416,11 @@ will be updated with the new value.
_name = "product.template"
purchase_uom = fields.Many2One('product.uom', 'Purchase UOM', states={
- 'readonly': "active == False",
- 'invisible': "not purchasable",
- 'required': "purchasable",
- }, domain=["('category', '=', (default_uom, 'uom.category'))"],
- context="{'category': (default_uom, 'uom.category')}",
+ 'readonly': Not(Bool(Eval('active'))),
+ 'invisible': Not(Bool(Eval('purchasable'))),
+ 'required': Bool(Eval('purchasable')),
+ }, domain=[('category', '=', (Eval('default_uom'), 'uom.category'))],
+ context={'category': (Eval('default_uom'), 'uom.category')},
on_change_with=['default_uom', 'purchase_uom', 'purchasable'])
def on_change_with_purchase_uom(self, cursor, user, ids, vals,
diff --git a/doc/views.rst b/doc/views.rst
index bf2289fc09..522d9db578 100644
--- a/doc/views.rst
+++ b/doc/views.rst
@@ -291,7 +291,8 @@ Each tree view must start with this tag.
* ``sequence``: The name of the field that is used for sorting. So this field must be an interger and it will be updated to match the new sort when the user use the ``Drag and Drop`` between rows of the list.
- * ``colors``: A string that is a list of color specification separated by ';'. The specifications have this form: ``color name:test``. The tests is evaluated on each rows and when one return True, than the color is used to highlight the row.
+ * ``colors``: A PySON string that will be evaluated for each record. A string containing the
+ name of the color will be returned.
* ``fill``: A boolean to specify if the last column must fill the remain free space in the view.
@@ -378,9 +379,8 @@ field
* ``key``: can be used to distinguish fields with the same name but
that are different with domain
- * ``domain``: a string that is evaluate with the object value as
- context. If the result is true the field value is added to the
- graph otherwise not
+ * ``domain``: a PySON string which is evaluated with the object value as
+ context. If the result is true the field value is added to the graph.
* ``fill``: defined if the graph must be fill
diff --git a/setup.py b/setup.py
index 688d5d3ace..565bc110e4 100644
--- a/setup.py
+++ b/setup.py
@@ -46,6 +46,7 @@ setup(name=PACKAGE,
'relatorio >= 0.2.0',
'Genshi',
'python-dateutil',
+ 'simplejson',
],
extras_require={
'PostgreSQL': ['psycopg2 >= 2.0'],
diff --git a/trytond/ir/action.py b/trytond/ir/action.py
index e0cb97e6fd..07df5f0898 100644
--- a/trytond/ir/action.py
+++ b/trytond/ir/action.py
@@ -2,8 +2,9 @@
#this repository contains the full copyright notices and license terms.
"Action"
from trytond.model import ModelView, ModelSQL, ModelStorage, fields
-from trytond.tools import file_open, Cache
+from trytond.tools import file_open, Cache, safe_eval
from trytond.backend import TableHandler
+from trytond.pyson import PYSONEncoder, CONTEXT, PYSON
import base64
import os
@@ -180,6 +181,7 @@ class ActionKeyword(ModelSQL, ModelView):
('keyword', '=', keyword),
('model', '=', model + ',0'),
], context=context))
+ encoder = PYSONEncoder()
for action_keyword in self.browse(cursor, user, action_keyword_ids,
context=context):
try:
@@ -196,6 +198,10 @@ class ActionKeyword(ModelSQL, ModelView):
del res[-1]['report_content_data']
del res[-1]['report_content']
del res[-1]['style_content']
+ res[-1]['email'] = encoder.encode(res[-1]['email'])
+ elif action_keyword.action.type == 'ir.action.act_window':
+ for field in ('domain', 'context', 'search_value'):
+ del res[-1][field]
return res
ActionKeyword()
@@ -401,6 +407,25 @@ class ActionActWindow(ModelSQL, ModelView):
help='Use the action name as window name')
search_value = fields.Char('Search Criteria',
help='Default search criteria for the list view')
+ pyson_domain = fields.Function('get_pyson', type='string',
+ string='PySON Domain')
+ pyson_context = fields.Function('get_pyson', type='string',
+ string='PySON Context')
+ pyson_search_value = fields.Function('get_pyson', type='string',
+ string='PySON Search Criteria')
+
+ def __init__(self):
+ super(ActionActWindow, self).__init__()
+ self._constraints += [
+ ('check_domain', 'invalid_domain'),
+ ('check_context', 'invalid_context'),
+ ('check_search_value', 'invalid_search_value'),
+ ]
+ self._error_messages.update({
+ 'invalid_domain': 'Invalid domain!',
+ 'invalid_context': 'Invalid context!',
+ 'invalid_search_value': 'Invalid search criteria!',
+ })
def default_type(self, cursor, user, context=None):
return 'ir.action.act_window'
@@ -423,6 +448,61 @@ class ActionActWindow(ModelSQL, ModelView):
def default_search_value(self, cursor, user, context=None):
return '{}'
+ def check_domain(self, cursor, user, ids):
+ "Check domain"
+ for action in self.browse(cursor, user, ids):
+ if action.domain:
+ try:
+ value = safe_eval(action.domain, CONTEXT)
+ except:
+ return False
+ if isinstance(value, PYSON):
+ if not value.types() == set([list]):
+ return False
+ elif not isinstance(value, list):
+ return False
+ else:
+ try:
+ fields.domain_validate(value)
+ except:
+ return False
+ return True
+
+ def check_context(self, cursor, user, ids):
+ "Check context"
+ for action in self.browse(cursor, user, ids):
+ if action.context:
+ try:
+ value = safe_eval(action.context, CONTEXT)
+ except:
+ return False
+ if isinstance(value, PYSON):
+ if not value.types() == set([dict]):
+ return False
+ elif not isinstance(value, dict):
+ return False
+ else:
+ try:
+ field.context_validate(value)
+ except:
+ return False
+ return True
+
+ def check_search_value(self, cursor, user, ids):
+ "Check search_value"
+ for action in self.browse(cursor, user, ids):
+ if action.search_value:
+ try:
+ value = safe_eval(action.search_value, CONTEXT)
+ except:
+ return False
+ if isinstance(value, PYSON):
+ if not value.types() == set([dict]):
+ return False
+ elif not isinstance(value, dict):
+ return False
+ return True
+
def views_get_fnc(self, cursor, user, ids, name, arg, context=None):
res = {}
for act in self.browse(cursor, user, ids, context=context):
@@ -430,6 +510,20 @@ class ActionActWindow(ModelSQL, ModelView):
for view in act.act_window_views]
return res
+ def get_pyson(self, cursor, user, ids, name, arg, context=None):
+ res = {}
+ encoder = PYSONEncoder()
+ field = name[6:]
+ defaults = {
+ 'domain': '[]',
+ 'context': '{}',
+ 'search_value': '{}',
+ }
+ for act in self.browse(cursor, user, ids, context=context):
+ res[act.id] = encoder.encode(safe_eval(act[field] or
+ defaults.get(field, 'False'), CONTEXT))
+ return res
+
def create(self, cursor, user, vals, context=None):
later = {}
vals = vals.copy()
diff --git a/trytond/ir/default.py b/trytond/ir/default.py
index 683695b26f..820eacda61 100644
--- a/trytond/ir/default.py
+++ b/trytond/ir/default.py
@@ -3,6 +3,7 @@
"Default"
from trytond.model import ModelView, ModelSQL, fields
from trytond.tools import Cache, safe_eval
+from trytond.pyson import Eval
from decimal import Decimal
import datetime
import time
@@ -16,7 +17,7 @@ class Default(ModelSQL, ModelView):
model = fields.Many2One('ir.model', 'Model', required=True,
ondelete='CASCADE')
field = fields.Many2One('ir.model.field', 'Field', required=True,
- ondelete='CASCADE', domain=["('model', '=', model)"])
+ ondelete='CASCADE', domain=[('model', '=', Eval('model'))])
value = fields.Text('Value')
clause = fields.Text('Clause')
user = fields.Many2One('res.user', 'User', ondelete='CASCADE')
diff --git a/trytond/ir/module/module.xml b/trytond/ir/module/module.xml
index 7d6adfff3b..d62f47b637 100644
--- a/trytond/ir/module/module.xml
+++ b/trytond/ir/module/module.xml
@@ -38,17 +38,17 @@ this repository contains the full copyright notices and license terms. -->
+ type="object"/>
+ type="object"/>
+ type="object"/>
+ type="object"/>
+ type="object"/>
+ type="object"/>
]]>
@@ -60,7 +60,7 @@ this repository contains the full copyright notices and license terms. -->
+ colors="If(In(Eval('state', ''), ['to upgrade', 'to install']), 'blue', If(Equal(Eval('state', ''), 'uninstalled'), 'grey', 'black'))">
diff --git a/trytond/ir/rule.py b/trytond/ir/rule.py
index 4c98b3e92e..46c288d5fd 100644
--- a/trytond/ir/rule.py
+++ b/trytond/ir/rule.py
@@ -3,6 +3,7 @@
"Rule"
from trytond.model import ModelView, ModelSQL, fields
from trytond.tools import Cache, safe_eval
+from trytond.pyson import Eval, Get
import time
@@ -87,8 +88,8 @@ class Rule(ModelSQL, ModelView):
_rec_name = 'field'
_description = __doc__
field = fields.Many2One('ir.model.field', 'Field',
- domain=["('model', '=', _parent_rule_group.model)"], select=1,
- required=True)
+ domain=[('model', '=', Get(Eval('_parent_rule_group', {}), 'model'))],
+ select=1, required=True)
operator = fields.Selection([
('=', '='),
('<>', '<>'),
diff --git a/trytond/ir/sequence.py b/trytond/ir/sequence.py
index ed6c294048..939d06bab4 100644
--- a/trytond/ir/sequence.py
+++ b/trytond/ir/sequence.py
@@ -4,6 +4,7 @@ from trytond.model import ModelView, ModelSQL, fields
from string import Template
import datetime
from trytond.tools import datetime_strftime
+from trytond.pyson import In, Eval
class SequenceType(ModelSQL, ModelView):
@@ -23,7 +24,7 @@ class Sequence(ModelSQL, ModelView):
name = fields.Char('Sequence Name', required=True, translate=True)
code = fields.Selection('code_get', 'Sequence Type', required=True,
states={
- 'readonly': "context.get('code')",
+ 'readonly': In('code', Eval('context', {})),
})
active = fields.Boolean('Active')
prefix = fields.Char('Prefix')
diff --git a/trytond/ir/translation.py b/trytond/ir/translation.py
index 0bff641279..f7d17d6123 100644
--- a/trytond/ir/translation.py
+++ b/trytond/ir/translation.py
@@ -17,6 +17,7 @@ from trytond.wizard import Wizard
from trytond import tools
from trytond.tools import file_open, reduce_ids
from trytond.backend import TableHandler
+from trytond.pyson import PYSONEncoder
import os
TRANSLATION_TYPE = [
@@ -1055,8 +1056,10 @@ class TranslationUpdate(Wizard):
model_data = model_data_obj.browse(cursor, user, model_data_ids[0],
context=context)
res = act_window_obj.read(cursor, user, model_data.db_id, context=context)
- res['domain'] = res['domain'][:-1] + ',' + \
- str(('lang', '=', data['form']['lang'])) + res['domain'][-1:]
+ res['pyson_domain'] = PYSONEncoder().encode([
+ ('module', '!=', False),
+ ('lang', '=', data['form']['lang']),
+ ])
return res
states = {
diff --git a/trytond/ir/ui/view.py b/trytond/ir/ui/view.py
index 3d6de4df52..b455d84cb9 100644
--- a/trytond/ir/ui/view.py
+++ b/trytond/ir/ui/view.py
@@ -7,6 +7,8 @@ import os
import logging
from lxml import etree
from trytond.backend import TableHandler
+from trytond.pyson import PYSONEncoder, CONTEXT, Eval, Not, Bool, Equal
+from trytond.tools import safe_eval
class View(ModelSQL, ModelView):
@@ -27,11 +29,11 @@ class View(ModelSQL, ModelView):
inherit = fields.Many2One('ir.ui.view', 'Inherited View', select=1,
ondelete='CASCADE')
field_childs = fields.Char('Children Field', states={
- 'invisible': "type != 'tree'",
+ 'invisible': Not(Equal(Eval('type'), 'tree')),
})
module = fields.Char('Module', readonly=True)
domain = fields.Char('Domain', states={
- 'invisible': "not bool(inherit)",
+ 'invisible': Not(Bool(Eval('inherit'))),
})
def __init__(self):
@@ -93,6 +95,36 @@ class View(ModelSQL, ModelView):
'Invalid xml view:\n%s' % (str(error_log) + '\n' + xml))
return False
root_element = tree.getroottree().getroot()
+
+ # validate pyson attributes
+ validates = {
+ 'states': fields.states_validate,
+ 'domain': fields.domain_validate,
+ 'context': fields.context_validate,
+ 'digits': fields.digits_validate,
+ 'add_remove': fields.add_remove_validate,
+ }
+ def encode(element):
+ for attr in ('states', 'domain', 'context', 'digits',
+ 'add_remove', 'spell', 'colors'):
+ if element.get(attr):
+ try:
+ value = safe_eval(element.get(attr), CONTEXT)
+ validates.get(attr, lambda a: True)(value)
+ except Exception, e:
+ logger = logging.getLogger('ir')
+ logger.error('Invalid pyson view element "%s:%s":' \
+ '\n%s\n%s' % \
+ (element.get('id') or element.get('name'),
+ attr, str(e), xml))
+ return False
+ for child in element:
+ if not encode(child):
+ return False
+ return True
+ if not encode(root_element):
+ return False
+
strings = self._translate_view(root_element)
view_ids = self.search(cursor, 0, [
('model', '=', view.model),
diff --git a/trytond/model/browse.py b/trytond/model/browse.py
index 0265ba5d0f..c608122141 100644
--- a/trytond/model/browse.py
+++ b/trytond/model/browse.py
@@ -279,7 +279,7 @@ class EvalEnvironment(dict):
return self.__getitem__(item)
except:
pass
- return super(EvalEnvironment, self).get(item, default=default)
+ return super(EvalEnvironment, self).get(item, default)
def __nonzero__(self):
return bool(self._record)
diff --git a/trytond/model/fields/field.py b/trytond/model/fields/field.py
index 8157b879f6..006c3f4ae6 100644
--- a/trytond/model/fields/field.py
+++ b/trytond/model/fields/field.py
@@ -1,6 +1,52 @@
#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.pyson import PYSON
+
+def domain_validate(value):
+ from trytond.model.modelstorage import OPERATORS
+ assert isinstance(value, list), 'domain must be a list'
+ def test_domain(dom):
+ for arg in dom:
+ if isinstance(arg, basestring):
+ if arg not in ('AND', 'OR'):
+ return False
+ elif isinstance(arg, tuple) or \
+ (isinstance(arg, list) and len(arg) > 2 and \
+ (arg[1] in OPERATORS) or \
+ (isinstance(arg[1], PYSON) and \
+ arg[1].types() == set([str]))):
+ pass
+ elif isinstance(arg, list):
+ if not test_domain(arg):
+ return False
+ return True
+ assert test_domain(value), 'invalid domain'
+
+def states_validate(value):
+ assert isinstance(value, dict), 'states must be a dict'
+ for state in value:
+ if state == 'icon':
+ continue
+ assert isinstance(value[state], PYSON), \
+ 'values of states must be PYSON'
+ assert value[state].types() == set([bool]), \
+ 'values of states must return boolean'
+
+def on_change_validate(value):
+ if value:
+ assert isinstance(value, list), 'on_change must be a list'
+
+def on_change_with_validate(value):
+ if value:
+ assert isinstance(value, list), 'on_change_with must be a list'
+
+def depends_validate(value):
+ assert isinstance(value, list), 'depends must be a list'
+
+def context_validate(value):
+ assert isinstance(value, dict), 'context must be a dict'
+
class Field(object):
_type = None
@@ -18,9 +64,9 @@ class Field(object):
:param domain: A list that defines a domain constraint. See domain on
ModelStorage.search.
:param states: A dictionary. Possible keys are ``required``,
- ``readonly`` and ``invisible``. Values are strings of python
- statements that can be evaluated with record values. This allows to
- change dynamically the attributes of the field.
+ ``readonly`` and ``invisible``. Values are pyson expressions that
+ will be evaluated with record values. This allows to change
+ dynamically the attributes of the field.
:param priority: Give the order in which set methods are called.
:param change_default: A boolean. If ``True`` the field can be used as
condition for a custom default value.
@@ -38,22 +84,80 @@ class Field(object):
:param depends: A list of field name on which this one depends.
:param order_field: A string. If set it will use the string when
ordering records on the field.
- :param context: A string defining a dictionary which will be given
- to open the relation fields.
+ :param context: A dictionary which will be given to open the relation
+ fields.
'''
self.string = string
self.help = help
self.required = required
self.readonly = readonly
+ self.__domain = None
self.domain = domain or []
- assert isinstance(self.domain, list), 'domain must be a list'
+ self.__states = None
self.states = states or {}
self.priority = priority
self.change_default = change_default
self.translate = translate
self.select = select
+ self.__on_change = None
self.on_change = on_change
+ self.__on_change_with = None
self.on_change_with = on_change_with
+ self.__depends = None
self.depends = depends or []
self.order_field = order_field
- self.context = context or ''
+ self.__context = None
+ self.context = context or {}
+
+ def _get_domain(self):
+ return self.__domain
+
+ def _set_domain(self, value):
+ domain_validate(value)
+ self.__domain = value
+
+ domain = property(_get_domain, _set_domain)
+
+ def _get_states(self):
+ return self.__states
+
+ def _set_states(self, value):
+ states_validate(value)
+ self.__states = value
+
+ states = property(_get_states, _set_states)
+
+ def _get_on_change(self):
+ return self.__on_change
+
+ def _set_on_change(self, value):
+ on_change_validate(value)
+ self.__on_change = value
+ on_change = property(_get_on_change, _set_on_change)
+
+ def _get_on_change_with(self):
+ return self.__on_change_with
+
+ def _set_on_change_with(self, value):
+ on_change_with_validate(value)
+ self.__on_change_with = value
+
+ on_change_with = property(_get_on_change_with, _set_on_change_with)
+
+ def _get_depends(self):
+ return self.__depends
+
+ def _set_depends(self, value):
+ depends_validate(value)
+ self.__depends = value
+
+ depends = property(_get_depends, _set_depends)
+
+ def _get_context(self):
+ return self.__context
+
+ def _set_context(self, value):
+ context_validate(value)
+ self.__context = value
+
+ context = property(_get_context, _set_context)
diff --git a/trytond/model/fields/float.py b/trytond/model/fields/float.py
index e55e64c92a..582f956a8e 100644
--- a/trytond/model/fields/float.py
+++ b/trytond/model/fields/float.py
@@ -2,6 +2,17 @@
#this repository contains the full copyright notices and license terms.
from trytond.model.fields.field import Field
+from trytond.pyson import PYSON
+
+def digits_validate(value):
+ if value:
+ assert isinstance(value, tuple), 'digits must be a tuple'
+ for i in value:
+ assert isinstance(i, (int, long, PYSON)), \
+ 'digits must be tuple of integers or PYSON'
+ if isinstance(i, PYSON):
+ assert i.types().issubset(set([int, long])), \
+ 'PYSON digits must return an integer'
class Float(Field):
@@ -24,5 +35,16 @@ class Float(Field):
translate=translate, select=select, on_change=on_change,
on_change_with=on_change_with, depends=depends,
order_field=order_field, context=context)
+ self.__digits = None
self.digits = digits
+
__init__.__doc__ += Field.__init__.__doc__
+
+ def _get_digits(self):
+ return self.__digits
+
+ def _set_digits(self, value):
+ digits_validate(value)
+ self.__digits = value
+
+ digits = property(_get_digits, _set_digits)
diff --git a/trytond/model/fields/function.py b/trytond/model/fields/function.py
index c68b15f0ff..16b911f173 100644
--- a/trytond/model/fields/function.py
+++ b/trytond/model/fields/function.py
@@ -2,6 +2,9 @@
#this repository contains the full copyright notices and license terms.
from trytond.model.fields.field import Field
+from trytond.model.fields.float import digits_validate
+from trytond.model.fields.one2many import add_remove_validate
+from trytond.pyson import PYSON
import inspect
@@ -54,13 +57,34 @@ class Function(Field):
self._type = type
self.fnct_search = fnct_search
self.selection = selection
+ self.__digits = None
self.digits = digits
if relation:
self.model_name = relation
+ self.__add_remove = None
self.add_remove = add_remove
self.datetime_field = datetime_field
+
__init__.__doc__ += Field.__init__.__doc__
+ def _get_digits(self):
+ return self.__digits
+
+ def _set_digits(self, value):
+ digits_validate(value)
+ self.__digits = value
+
+ digits = property(_get_digits, _set_digits)
+
+ def _get_add_remove(self):
+ return self.__add_remove
+
+ def _set_add_remove(self, value):
+ add_remove_validate(value)
+ self.__add_remove = value
+
+ add_remove = property(_get_add_remove, _set_add_remove)
+
def search(self, cursor, user, model, name, args, context=None):
'''
Call the fnct_search.
diff --git a/trytond/model/fields/one2many.py b/trytond/model/fields/one2many.py
index d382cf902f..5b0269cf81 100644
--- a/trytond/model/fields/one2many.py
+++ b/trytond/model/fields/one2many.py
@@ -4,6 +4,10 @@
from trytond.model.fields.field import Field
from itertools import chain
+def add_remove_validate(value):
+ if value:
+ assert isinstance(value, list), 'add_remove must be a list'
+
class One2Many(Field):
'''
@@ -40,11 +44,22 @@ class One2Many(Field):
order_field=order_field, context=context)
self.model_name = model_name
self.field = field
+ self.__add_remove = None
self.add_remove = add_remove
self.order = order
self.datetime_field = datetime_field
+
__init__.__doc__ += Field.__init__.__doc__
+ def _get_add_remove(self):
+ return self.__add_remove
+
+ def _set_add_remove(self, value):
+ add_remove_validate(value)
+ self.__add_remove = value
+
+ add_remove = property(_get_add_remove, _set_add_remove)
+
def get(self, cursor, user, ids, model, name, values=None, context=None):
'''
Return target records ordered.
diff --git a/trytond/model/model.py b/trytond/model/model.py
index 0beb1f7c62..c11be55784 100644
--- a/trytond/model/model.py
+++ b/trytond/model/model.py
@@ -3,6 +3,7 @@
from trytond.model import fields
from trytond.pool import Pool
+from trytond.pyson import PYSONEncoder, CONTEXT
import copy
@@ -667,6 +668,8 @@ class Model(object):
val))
translation_obj._get_sources(cursor, trans_args)
+ encoder = PYSONEncoder()
+
for field in (x for x in self._columns.keys()
if ((not fields_names) or x in fields_names)):
res[field] = {'type': self._columns[field]._type}
@@ -757,6 +760,11 @@ class Model(object):
res[field]['relation_field'] = copy.copy(
self._columns[field].field)
+ # convert attributes into pyson
+ for attr in ('states', 'domain', 'context', 'digits', 'add_remove'):
+ if attr in res[field]:
+ res[field][attr] = encoder.encode(res[field][attr])
+
if fields_names:
# filter out fields which aren't in the fields_names list
for i in res.keys():
diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py
index 341f3b7f38..15f647c201 100644
--- a/trytond/model/modelstorage.py
+++ b/trytond/model/modelstorage.py
@@ -5,6 +5,7 @@ from trytond.model import fields
from trytond.model.browse import BrowseRecordList, BrowseRecord, BrowseRecordNull
from trytond.model.browse import EvalEnvironment
from trytond.tools import safe_eval
+from trytond.pyson import PYSONEncoder, PYSONDecoder, PYSON
import datetime
import time
from decimal import Decimal
@@ -918,24 +919,17 @@ class ModelStorage(Model):
ctx_pref = user_obj.get_preferences(cursor, user,
context_only=True, context=context)
- def eval_domain(domain, env=None):
- res = []
- for arg in domain:
- if isinstance(arg, basestring):
- if arg in ('AND', 'OR'):
- res.append(arg)
- else:
- if not env:
- return True
- res.append(safe_eval(arg, env))
- elif isinstance(arg, tuple):
- res.append(arg)
- elif isinstance(arg, list):
- arg = eval_domain(arg, env=env)
- if arg == True:
+ def is_pyson(test):
+ if isinstance(test, PYSON):
+ return True
+ if isinstance(test, (list, tuple)):
+ for i in test:
+ if isinstance(i, PYSON):
return True
- res.append(arg)
- return res
+ if isinstance(i, (list, tuple)):
+ if is_pyson(i):
+ return True
+ return False
context.update(ctx_pref)
records = self.browse(cursor, user, ids, context=context)
@@ -947,7 +941,8 @@ class ModelStorage(Model):
relation_obj = self.pool.get(field.model_name)
else:
relation_obj = field.get_target(self.pool)
- if eval_domain(field.domain) == True:
+ if is_pyson(field.domain):
+ pyson_domain = PYSONEncoder().encode(field.domain)
ctx = context.copy()
ctx.update(ctx_pref)
for record in records:
@@ -957,7 +952,7 @@ class ModelStorage(Model):
env['time'] = time
env['context'] = context
env['active_id'] = record.id
- domain = eval_domain(field.domain, env=env)
+ domain = PYSONDecoder(env).decode(pyson_domain)
relation_ids = []
if record[field_name]:
if field._type in ('many2one',):
@@ -999,9 +994,11 @@ class ModelStorage(Model):
context=context)
# validate states required
if field.states and 'required' in field.states:
- if isinstance(field.states['required'], basestring):
+ if is_pyson(field.states['required']):
ctx = context.copy()
ctx.update(ctx_pref)
+ pyson_required = PYSONEncoder().encode(
+ field.states['required'])
for record in records:
env = EvalEnvironment(record, self)
env.update(ctx)
@@ -1009,7 +1006,7 @@ class ModelStorage(Model):
env['time'] = time
env['context'] = context
env['active_id'] = record.id
- required = safe_eval(field.states['required'], env)
+ required = PYSONDecoder(env).decode(pyson_required)
if required and not record[field_name]:
self.raise_user_error(cursor,
'required_validation_record',
diff --git a/trytond/model/modelview.py b/trytond/model/modelview.py
index d743bc6f5d..7b6bffa0cc 100644
--- a/trytond/model/modelview.py
+++ b/trytond/model/modelview.py
@@ -3,6 +3,7 @@
from trytond.model import Model
from trytond.tools import Cache, safe_eval
from trytond.modules import create_graph, get_module_list
+from trytond.pyson import PYSONEncoder, CONTEXT
from lxml import etree
try:
import hashlib
@@ -374,6 +375,14 @@ class ModelView(Model):
if element.get('name') in fields_width:
element.set('width', str(fields_width[element.get('name')]))
+ # convert attributes into pyson
+ encoder = PYSONEncoder()
+ for attr in ('states', 'domain', 'context', 'digits', 'add_remove',
+ 'spell', 'colors'):
+ if element.get(attr):
+ element.set(attr, encoder.encode(safe_eval(element.get(attr),
+ CONTEXT)))
+
# translate view
if ('language' in context) and not result:
for attr in ('string', 'sum', 'confirm', 'help'):
diff --git a/trytond/pyson.py b/trytond/pyson.py
new file mode 100644
index 0000000000..67d0a2392a
--- /dev/null
+++ b/trytond/pyson.py
@@ -0,0 +1,474 @@
+#This file is part of Tryton. The COPYRIGHT file at the top level of
+#this repository contains the full copyright notices and license terms.
+try:
+ import json
+except ImportError:
+ import simplejson as json
+import datetime
+
+
+class PYSON(object):
+
+ def pyson(self):
+ raise
+
+ def types(self):
+ raise
+
+ @staticmethod
+ def eval(dct, context):
+ raise
+
+
+class PYSONEncoder(json.JSONEncoder):
+
+ def default(self, o):
+ if isinstance(o, PYSON):
+ return o.pyson()
+ return super(PYSONEncoder, self).default(o)
+
+
+class PYSONDecoder(json.JSONDecoder):
+
+ def __init__(self, context=None):
+ self.__context = context or {}
+ return super(PYSONDecoder, self).__init__(object_hook=self.object_hook)
+
+ def object_hook(self, dct):
+ if '__class__' in dct:
+ klass = globals().get(dct['__class__'])
+ if klass and hasattr(klass, 'eval'):
+ return klass.eval(dct, self.__context)
+ return dct
+
+
+class Eval(PYSON):
+
+ def __init__(self, value, default=''):
+ self._value = value
+ self._default = default
+
+ def pyson(self):
+ return {
+ '__class__': 'Eval',
+ 'v': self._value,
+ 'd': self._default,
+ }
+
+ def types(self):
+ if isinstance(self._default, PYSON):
+ return self._default.types()
+ else:
+ return set([type(self._default)])
+
+ @staticmethod
+ def eval(dct, context):
+ return context.get(dct['v'], dct['d'])
+
+
+class Not(PYSON):
+
+ def __init__(self, value):
+ if isinstance(value, PYSON):
+ assert value.types() == set([bool]), 'value must be boolean'
+ else:
+ assert isinstance(value, bool), 'value must be boolean'
+ self._value = value
+
+ def pyson(self):
+ return {
+ '__class__': 'Not',
+ 'v': self._value,
+ }
+
+ def types(self):
+ return set([bool])
+
+ @staticmethod
+ def eval(dct, context):
+ return not dct['v']
+
+
+class Bool(PYSON):
+
+ def __init__(self, value):
+ self._value = value
+
+ def pyson(self):
+ return {
+ '__class__': 'Bool',
+ 'v': self._value,
+ }
+
+ def types(self):
+ return set([bool])
+
+ @staticmethod
+ def eval(dct, context):
+ return bool(dct['v'])
+
+
+class And(PYSON):
+
+ def __init__(self, *statements):
+ for statement in statements:
+ if isinstance(statement, PYSON):
+ assert statement.types() == set([bool]), \
+ 'statement must be boolean'
+ else:
+ assert isinstance(statement, bool), \
+ 'statement must be boolean'
+ assert len(statements) >= 2, 'must have at least 2 statements'
+ self._statements = statements
+
+ def pyson(self):
+ return {
+ '__class__': 'And',
+ 's': self._statements,
+ }
+
+ def types(self):
+ return set([bool])
+
+ @staticmethod
+ def eval(dct, context):
+ return bool(reduce(lambda x, y: x and y, dct['s']))
+
+
+class Or(And):
+
+ def pyson(self):
+ res = super(Or, self).pyson()
+ res['__class__'] = 'Or'
+ return res
+
+ @staticmethod
+ def eval(dct, context):
+ return bool(reduce(lambda x, y: x or y, dct['s']))
+
+
+class Equal(PYSON):
+
+ def __init__(self, statement1, statement2):
+ if isinstance(statement1, PYSON):
+ types1 = statement1.types()
+ else:
+ types1 = set([type(statement1)])
+ if isinstance(statement2, PYSON):
+ types2 = statement2.types()
+ else:
+ types2 = set([type(statement2)])
+ assert types1 == types2, 'statements must have the same type'
+ self._statement1 = statement1
+ self._statement2 = statement2
+
+ def pyson(self):
+ return {
+ '__class__': 'Equal',
+ 's1': self._statement1,
+ 's2': self._statement2,
+ }
+
+ def types(self):
+ return set([bool])
+
+ @staticmethod
+ def eval(dct, context):
+ return dct['s1'] == dct['s2']
+
+
+class Greater(PYSON):
+
+ def __init__(self, statement1, statement2, equal=False):
+ for i in (statement1, statement2):
+ if isinstance(i, PYSON):
+ assert i.types().issubset(set([int, long, float])), \
+ 'statement must be an integer or a float'
+ else:
+ assert isinstance(i, (int, long, float)), \
+ 'statement must be an integer or a float'
+ if isinstance(equal, PYSON):
+ assert equal.types() == set([bool])
+ else:
+ assert isinstance(equal, bool)
+ self._statement1 = statement1
+ self._statement2 = statement2
+ self._equal = equal
+
+ def pyson(self):
+ return {
+ '__class__': 'Greater',
+ 's1': self._statement1,
+ 's2': self._statement2,
+ 'e': self._equal,
+ }
+
+ def types(self):
+ return set([bool])
+
+ @staticmethod
+ def eval(dct, context):
+ if dct['e']:
+ return dct['s1'] >= dct['s2']
+ else:
+ return dct['s1'] > dct['s2']
+
+
+class Less(Greater):
+
+ def pyson(self):
+ res = super(Less, self).pyson()
+ res['__class__'] = 'Less'
+ return res
+
+ @staticmethod
+ def eval(dct, context):
+ if dct['e']:
+ return dct['s1'] <= dct['s2']
+ else:
+ return dct['s1'] < dct['s2']
+
+
+class If(PYSON):
+
+ def __init__(self, condition, then_statement, else_statement=None):
+ if isinstance(condition, PYSON):
+ assert condition.types() == set([bool]), 'condition must be boolean'
+ else:
+ assert isinstance(condition, bool), 'condition must be boolean'
+ if isinstance(then_statement, PYSON):
+ then_types = then_statement.types()
+ else:
+ then_types = set([type(then_statement)])
+ if isinstance(else_statement, PYSON):
+ assert then_types == else_statement.types(), \
+ 'then and else statements must be the same type'
+ else:
+ assert then_types == set([type(else_statement)]), \
+ 'then and else statements must be the same type'
+ self._condition = condition
+ self._then_statement = then_statement
+ self._else_statement = else_statement
+
+ def pyson(self):
+ return {
+ '__class__': 'If',
+ 'c': self._condition,
+ 't': self._then_statement,
+ 'e': self._else_statement,
+ }
+
+ def types(self):
+ if isinstance(self._then_statement, PYSON):
+ return self._then_statement.types()
+ else:
+ return set([type(self._then_statement)])
+
+ @staticmethod
+ def eval(dct, context):
+ if dct['c']:
+ return dct['t']
+ else:
+ return dct['e']
+
+
+class Get(PYSON):
+
+ def __init__(self, obj, key, default=''):
+ if isinstance(obj, PYSON):
+ assert obj.types() == set([dict]), 'obj must be a dict'
+ else:
+ assert isinstance(obj, dict), 'obj must be a dict'
+ self._obj = obj
+ if isinstance(key, PYSON):
+ assert key.types() == set([str]), 'key must be a string'
+ else:
+ assert type(key) == str, 'key must be a string'
+ self._key = key
+ self._default = default
+
+ def pyson(self):
+ return {
+ '__class__': 'Get',
+ 'v': self._obj,
+ 'k': self._key,
+ 'd': self._default,
+ }
+
+ def types(self):
+ if isinstance(self._default, PYSON):
+ return self._default.types()
+ else:
+ return set([type(self._default)])
+
+ @staticmethod
+ def eval(dct, context):
+ return dct['v'].get(dct['k'], dct['d'])
+
+
+class In(PYSON):
+
+ def __init__(self, key, obj):
+ if isinstance(key, PYSON):
+ assert key.types().issubset(set([str, int, long])), \
+ 'key must be a string or an integer or a long'
+ else:
+ assert type(key) in [str, int, long], \
+ 'key must be a string or an integer or a long'
+ if isinstance(obj, PYSON):
+ assert obj.types().issubset(set([dict, list])), \
+ 'obj must be a dict or a list'
+ if obj.types() == set([dict]):
+ assert type(key) == str, 'key must be a string'
+ else:
+ assert type(obj) in [dict, list]
+ if type(obj) == dict:
+ assert type(key) == str, 'key must be a string'
+ self._key = key
+ self._obj = obj
+
+ def pyson(self):
+ return {
+ '__class__': 'In',
+ 'k': self._key,
+ 'v': self._obj,
+ }
+
+ def types(self):
+ return set([bool])
+
+ @staticmethod
+ def eval(dct, context):
+ return dct['k'] in dct['v']
+
+
+class Date(PYSON):
+
+ def __init__(self, year=None, month=None, day=None,
+ delta_years=0, delta_months=0, delta_days=0):
+ for i in (year, month, day, delta_years, delta_months, delta_days):
+ if isinstance(i, PYSON):
+ assert i.types().issubset(set([int, long, type(None)])), \
+ '%s must be an integer or None' % (i,)
+ else:
+ assert isinstance(i, (int, long, type(None))), \
+ '%s must be an integer or None' % (i,)
+ self._year = year
+ self._month = month
+ self._day = day
+ self._delta_years = delta_years
+ self._delta_months = delta_months
+ self._delta_days = delta_days
+
+ def pyson(self):
+ return {
+ '__class__': 'Date',
+ 'y': self._year,
+ 'M': self._month,
+ 'd': self._day,
+ 'dy': self._delta_years,
+ 'dM': self._delta_months,
+ 'dd': self._delta_days,
+ }
+
+ def types(self):
+ return set([datetime.date])
+
+ @staticmethod
+ def eval(dct, context):
+ date = datetime.date.today()
+ replace = {}
+ for i, j in (('y', 'year'), ('M', 'month'), ('d', 'day')):
+ if dct[i] is not None:
+ replace[j] = dct[i]
+ date = date.replace(**replace)
+ if dct['dy']:
+ year = date.year + dct['dy']
+ date = date.replace(year=year)
+ if dct['dM']:
+ month = date.month + dct['dM']
+ year = date.year + month/12
+ month = month%12
+ date = date.replace(year=year, month=month)
+ if dct['dd']:
+ date += datetime.timedelta(days=dct['dd'])
+ return date
+
+
+class DateTime(Date):
+
+ def __init__(self, year=None, month=None, day=None,
+ hour=None, minute=None, second=None, microsecond=None,
+ delta_years=0, delta_months=0, delta_days=0,
+ delta_hours=0, delta_minutes=0, delta_seconds=0,
+ delta_microseconds=0):
+ super(DateTime, self).__init__(year=year, month=month, day=day,
+ delta_years=delta_years, delta_months=delta_months,
+ delta_days=delta_days)
+ for i in (hour, minute, second, microsecond,
+ delta_hours, delta_minutes, delta_seconds, delta_microseconds):
+ if isinstance(i, PYSON):
+ assert i.types() == set([int, long, type(None)]), \
+ '%s must be an integer or None' % (i,)
+ else:
+ assert isinstance(i, (int, long, type(None))), \
+ '%s must be an integer or None' % (i,)
+ self._hour = hour
+ self._minute = minute
+ self._second = second
+ self._microsecond = microsecond
+ self._delta_hours = delta_hours
+ self._delta_minutes = delta_minutes
+ self._delta_seconds = delta_seconds
+ self._delta_microseconds = delta_microseconds
+
+ def pyson(self):
+ res = super(DateTime, self).pyson()
+ res['__class__'] = 'DateTime'
+ res['h'] = self._hour
+ res['m'] = self._minute
+ res['s'] = self._second
+ res['ms'] = self._microsecond
+ res['dh'] = self._delta_hours
+ res['dm'] = self._delta_minutes
+ res['ds'] = self._delta_seconds
+ res['dms'] = self._delta_microseconds
+ return res
+
+ def types(self):
+ return set([datetime.datetime])
+
+ @staticmethod
+ def eval(dct, context):
+ date = Date.eval(dct, context)
+ datetime_ = datetime.datetime.combine(date,
+ datetime.datetime.now().time())
+ replace = {}
+ for i, j in (('h', 'hour'), ('m', 'minute'), ('s', 'second'),
+ ('ms', 'microsecond')):
+ if dct[i] is not None:
+ replace[j] = dct[i]
+ datetime_ = datetime_.replace(**replace)
+ delta={}
+ for i, j in (('dh', 'hours'), ('dm', 'minutes'), ('ds', 'seconds'),
+ ('dms', 'microseconds')):
+ if dct[i]:
+ delta[j] = dct[i]
+ datetime_ += datetime.timedelta(**delta)
+ return datetime_
+
+CONTEXT = {
+ 'Eval': Eval,
+ 'Not': Not,
+ 'Bool': Bool,
+ 'And': And,
+ 'Or': Or,
+ 'Equal': Equal,
+ 'Greater': Greater,
+ 'Less': Less,
+ 'If': If,
+ 'Get': Get,
+ 'In': In,
+ 'Date': Date,
+ 'DateTime': DateTime,
+}
diff --git a/trytond/report/report.py b/trytond/report/report.py
index 782363e161..6b5d41ed17 100644
--- a/trytond/report/report.py
+++ b/trytond/report/report.py
@@ -212,6 +212,9 @@ class Report(object):
rel_report = relatorio.reporting.Report(path, 'application/vnd.oasis.opendocument.text',
ReportFactory(), relatorio.reporting.MIMETemplateLoader())
rel_report.filters.insert(0, translator)
+ #convert unicode key into str
+ localcontext = dict(map(lambda x: (str(x[0]), x[1]),
+ localcontext.iteritems()))
#Test compatibility with old relatorio version <= 0.3.0
if len(inspect.getargspec(rel_report.__call__)[0]) == 2:
data = rel_report(objects, **localcontext).render().getvalue()
diff --git a/trytond/res/ir.xml b/trytond/res/ir.xml
index 1769f6b4a0..d43f52d8bc 100644
--- a/trytond/res/ir.xml
+++ b/trytond/res/ir.xml
@@ -408,37 +408,43 @@ this repository contains the full copyright notices and license terms. -->
expr="/form/group/button[@name="button_install"]"
position="replace">
+ type="object"
+ states="{'invisible': Not(Equal(Eval('state'), 'uninstalled')), 'readonly': Not(In(%(res.group_admin)d, Eval('groups', [])))}"/>
+ type="object"
+ states="{'invisible': Not(Equal(Eval('state'), 'to install')), 'readonly': Not(In(%(res.group_admin)d, Eval('groups', [])))}"/>
+ type="object"
+ states="{'invisible': Not(Equal(Eval('state'), 'installed')), 'readonly': Bool(True)}"/>
+ type="object"
+ states="{'invisible': Not(Equal(Eval('state'), 'to remove')), 'readonly': Not(In(%(res.group_admin)d, Eval('groups', [])))}"/>
+ type="object"
+ states="{'invisible': Not(Equal(Eval('state'), 'installed')), 'readonly': Not(In(%(res.group_admin)d, Eval('groups', [])))}"/>
+ type="object"
+ states="{'invisible': Not(Equal(Eval('state'), 'to upgrade')), 'readonly': Not(In(%(res.group_admin)d, Eval('groups', [])))}"/>
]]>
diff --git a/trytond/res/request.py b/trytond/res/request.py
index bf15e9c2b5..df191bf758 100644
--- a/trytond/res/request.py
+++ b/trytond/res/request.py
@@ -2,6 +2,7 @@
#this repository contains the full copyright notices and license terms.
"Request"
from trytond.model import ModelView, ModelSQL, fields
+from trytond.pyson import Eval, In, If, Not, Equal
import time
import datetime
@@ -18,39 +19,42 @@ _PRIORITIES = [
('2', 'High'),
]
+_READONLY = If(In(Eval('state'), ['waiting', 'closed']),
+ True,
+ If(Equal(Eval('state'), 'chatting'),
+ Not(Equal(Eval('act_from'), Eval('_user'))),
+ False))
+
class Request(ModelSQL, ModelView):
"Request"
_name = 'res.request'
_description = __doc__
name = fields.Char('Subject', states={
- 'readonly': "(state in ('waiting', 'closed')) or " \
- "(state == 'chatting' and act_from != _user)",
+ 'readonly': _READONLY,
}, required=True)
active = fields.Boolean('Active')
priority = fields.Selection(_PRIORITIES, 'Priority', states={
- 'readonly': "(state in ('waiting', 'closed')) or " \
- "(state == 'chatting' and act_from != _user)",
+ 'readonly': _READONLY,
}, required=True, order_field='priority')
act_from = fields.Many2One('res.user', 'From', required=True,
readonly=True)
act_to = fields.Many2One('res.user', 'To', required=True,
domain=[('active', '=', True)],
states={
- 'readonly': "(state in ('waiting', 'closed')) or " \
- "(state == 'chatting' and act_from != _user)",
+ 'readonly': _READONLY,
})
body = fields.Text('Body', states={
- 'readonly': "(state in ('waiting', 'closed')) or " \
- "(state == 'chatting' and act_from != _user)",
+ 'readonly': _READONLY,
})
date_sent = fields.DateTime('Date', readonly=True)
trigger_date = fields.DateTime('Trigger Date', states={
- 'readonly': "(state in ('waiting', 'closed')) or " \
- "(state == 'chatting' and act_from != _user)",
+ 'readonly': _READONLY,
})
references = fields.One2Many('res.request.reference', 'request',
'References', states={
- 'readonly': "state == 'closed' or act_from != _user",
+ 'readonly': If(Equal(Eval('state'), 'closed'),
+ True,
+ Not(Equal(Eval('act_from', 0), Eval('_user', 0)))),
})
number_references = fields.Function('get_number_references', type='integer',
string="Number of References", on_change_with=['references'])
diff --git a/trytond/res/request.xml b/trytond/res/request.xml
index 392f7cf0f3..4ce4eda723 100644
--- a/trytond/res/request.xml
+++ b/trytond/res/request.xml
@@ -52,17 +52,17 @@ this repository contains the full copyright notices and license terms. -->
diff --git a/trytond/tests/test_tryton.py b/trytond/tests/test_tryton.py
index 41ee105ffe..e4e0c8d9ba 100644
--- a/trytond/tests/test_tryton.py
+++ b/trytond/tests/test_tryton.py
@@ -14,6 +14,7 @@ import time
from decimal import Decimal
from trytond import pysocket
from trytond.tools import reduce_ids, safe_eval, datetime_strftime
+from trytond import pyson
from lxml import etree
import datetime
@@ -145,6 +146,520 @@ class ToolsTestCase(unittest.TestCase):
self.assert_(datetime_strftime(datetime.date(1805, 3, 2),
'%Y-%m-%d'), '1805-03-02')
+class PYSONTestCase(unittest.TestCase):
+ '''
+ Test PySON.
+ '''
+
+ def test0010Eval(self):
+ '''
+ Test pyson.Eval
+ '''
+ self.assert_(pyson.Eval('test').pyson() == {
+ '__class__': 'Eval',
+ 'v': 'test',
+ 'd': '',
+ })
+ self.assert_(pyson.Eval('test', 'foo').pyson() == {
+ '__class__': 'Eval',
+ 'v': 'test',
+ 'd': 'foo',
+ })
+
+ self.assert_(pyson.Eval('test', 'foo').types() == set([type('foo')]))
+ self.assert_(pyson.Eval('test', 1).types() == set([type(1)]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Eval('test', 0))
+ self.assert_(pyson.PYSONDecoder({'test': 1}).decode(eval) == 1)
+ self.assert_(pyson.PYSONDecoder().decode(eval) == 0)
+
+ def test0020Not(self):
+ '''
+ Test pyson.Not
+ '''
+ self.assert_(pyson.Not(True).pyson() == {
+ '__class__': 'Not',
+ 'v': True,
+ })
+
+ self.assertRaises(AssertionError, pyson.Not, 'foo')
+
+ self.assert_(pyson.Not(True).types() == set([bool]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Not(True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Not(False))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ def test0030Bool(self):
+ '''
+ Test pyson.Bool
+ '''
+ self.assert_(pyson.Bool('test').pyson() == {
+ '__class__': 'Bool',
+ 'v': 'test',
+ })
+
+ self.assert_(pyson.Bool('test').types() == set([bool]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Bool(True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Bool(False))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Bool('test'))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Bool(''))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Bool(1))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Bool(0))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Bool(['test']))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Bool([]))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Bool({'foo': 'bar'}))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Bool({}))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ def test0040And(self):
+ '''
+ Test pyson.And
+ '''
+ self.assert_(pyson.And(True, False).pyson() == {
+ '__class__': 'And',
+ 's': (True, False),
+ })
+
+ self.assertRaises(AssertionError, pyson.And, 'test', False)
+ self.assertRaises(AssertionError, pyson.And, True, 'test')
+ self.assertRaises(AssertionError, pyson.And, True, False, 'test')
+ self.assertRaises(AssertionError, pyson.And, True)
+ self.assertRaises(AssertionError, pyson.And)
+
+ self.assert_(pyson.And(True, False).types() == set([bool]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.And(True, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.And(True, True, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.And(True, False))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.And(False, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.And(False, False))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.And(False, False, False))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.And(True, False, False))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.And(False, True, False))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.And(False, False, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ def test0050Or(self):
+ '''
+ Test pyson.Or
+ '''
+ self.assert_(pyson.Or(True, False).pyson() == {
+ '__class__': 'Or',
+ 's': (True, False),
+ })
+
+ self.assertRaises(AssertionError, pyson.Or, 'test', False)
+ self.assertRaises(AssertionError, pyson.Or, True, 'test')
+ self.assertRaises(AssertionError, pyson.Or, True, False, 'test')
+ self.assertRaises(AssertionError, pyson.Or, True)
+ self.assertRaises(AssertionError, pyson.Or)
+
+ self.assert_(pyson.Or(True, False).types() == set([bool]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Or(True, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Or(True, True, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Or(True, False))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Or(False, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Or(False, False))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Or(False, False, False))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Or(True, False, False))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Or(False, True, False))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Or(False, False, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ def test0060Equal(self):
+ '''
+ Test pyson.Equal
+ '''
+ self.assert_(pyson.Equal('test', 'test').pyson() == {
+ '__class__': 'Equal',
+ 's1': 'test',
+ 's2': 'test',
+ })
+
+ self.assertRaises(AssertionError, pyson.Equal, 'test', True)
+
+ self.assert_(pyson.Equal('test', 'test').types() == set([bool]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Equal('test', 'test'))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Equal('foo', 'bar'))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ def test0070Greater(self):
+ '''
+ Test pyson.Greater
+ '''
+ self.assert_(pyson.Greater(1, 0).pyson() == {
+ '__class__': 'Greater',
+ 's1': 1,
+ 's2': 0,
+ 'e': False,
+ })
+
+ self.assertRaises(AssertionError, pyson.Greater, 'test', 0)
+ self.assertRaises(AssertionError, pyson.Greater, 1, 'test')
+ self.assertRaises(AssertionError, pyson.Greater, 1, 0, 'test')
+
+ self.assert_(pyson.Greater(1, 0).types() == set([bool]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Greater(1, 0))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Greater(0, 1))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Greater(1, 0, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Greater(0, 1, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Greater(1, 1))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Greater(1, 1, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ def test0080Less(self):
+ '''
+ Test pyson.Less
+ '''
+ self.assert_(pyson.Less(0, 1).pyson() == {
+ '__class__': 'Less',
+ 's1': 0,
+ 's2': 1,
+ 'e': False,
+ })
+
+ self.assertRaises(AssertionError, pyson.Less, 'test', 1)
+ self.assertRaises(AssertionError, pyson.Less, 0, 'test')
+ self.assertRaises(AssertionError, pyson.Less, 0, 1, 'test')
+
+ self.assert_(pyson.Less(0, 1).types() == set([bool]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Less(0, 1))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Less(1, 0))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Less(0, 1, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Less(1, 0, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Less(1, 1))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Less(1, 1, True))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ def test0090If(self):
+ '''
+ Test pyson.If
+ '''
+ self.assert_(pyson.If(True, 'foo', 'bar').pyson() == {
+ '__class__': 'If',
+ 'c': True,
+ 't': 'foo',
+ 'e': 'bar',
+ })
+
+ self.assertRaises(AssertionError, pyson.If, 'test', 'foo', 'bar')
+ self.assertRaises(AssertionError, pyson.If, True, 'foo', False)
+
+ self.assert_(pyson.If(True, 'foo', 'bar').types() == set([type('foo')]))
+ self.assert_(pyson.If(True, False, True).types() == set([bool]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.If(True, 'foo', 'bar'))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == 'foo')
+
+ eval = pyson.PYSONEncoder().encode(pyson.If(False, 'foo', 'bar'))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == 'bar')
+
+ def test0100Get(self):
+ '''
+ Test pyson.Get
+ '''
+ self.assert_(pyson.Get({'foo': 'bar'}, 'foo', 'default').pyson() == {
+ '__class__': 'Get',
+ 'v': {'foo': 'bar'},
+ 'k': 'foo',
+ 'd': 'default',
+ })
+
+ self.assertRaises(AssertionError, pyson.Get, 'test', 'foo', 'default')
+ self.assertRaises(AssertionError, pyson.Get, {}, 1, 'default')
+
+ self.assert_(pyson.Get({}, 'foo', 'default').types() == set([str]))
+ self.assert_(pyson.Get({}, 'foo', True).types() == set([bool]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Get(
+ {'foo': 'bar'}, 'foo', 'default'))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == 'bar')
+
+ eval = pyson.PYSONEncoder().encode(pyson.Get(
+ {'foo': 'bar'}, 'test', 'default'))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == 'default')
+
+ eval = pyson.PYSONEncoder().encode(pyson.Get(
+ {}, 'foo', 'default'))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == 'default')
+
+ def test0110In(self):
+ '''
+ Test pyson.In
+ '''
+ self.assert_(pyson.In('foo', {'foo': 'bar'}).pyson() == {
+ '__class__': 'In',
+ 'k': 'foo',
+ 'v': {'foo': 'bar'},
+ })
+
+ self.assertRaises(AssertionError, pyson.In, object(), {})
+ self.assertRaises(AssertionError, pyson.In, 'test', 'foo')
+
+ self.assert_(pyson.In('foo', {}).types() == set([bool]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.In('foo', {'foo': 'bar'}))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.In('1', {1: 'bar'}))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.In('test', {'foo': 'bar'}))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.In('1', {2: 'bar'}))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.In('test', {}))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.In('foo', ['foo']))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.In(1, [1]))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == True)
+
+ eval = pyson.PYSONEncoder().encode(pyson.In('test', ['foo']))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.In(1, [2]))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ eval = pyson.PYSONEncoder().encode(pyson.In('test', []))
+ self.assert_(pyson.PYSONDecoder().decode(eval) == False)
+
+ def test0120Date(self):
+ '''
+ Test pyson.Date
+ '''
+ self.assert_(pyson.Date(2010, 1, 12, -1, 12, -7).pyson() == {
+ '__class__': 'Date',
+ 'y': 2010,
+ 'M': 1,
+ 'd': 12,
+ 'dy': -1,
+ 'dM': 12,
+ 'dd': -7
+ })
+
+ self.assertRaises(AssertionError, pyson.Date, 'test', 1, 12, -1, 12, -7)
+ self.assertRaises(AssertionError, pyson.Date, 2010, 'test', 12, -1, 12, -7)
+ self.assertRaises(AssertionError, pyson.Date, 2010, 1, 'test', -1, 12, -7)
+ self.assertRaises(AssertionError, pyson.Date, 2010, 1, 12, 'test', 12, -7)
+ self.assertRaises(AssertionError, pyson.Date, 2010, 1, 12, -1, 'test', -7)
+ self.assertRaises(AssertionError, pyson.Date, 2010, 1, 12, -1, 12, 'test')
+
+ self.assert_(pyson.Date(2010, 1, 12, -1, 12, -7).types()
+ == set([datetime.date]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Date())
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.date.today())
+
+ eval = pyson.PYSONEncoder().encode(pyson.Date(2010, 1, 12))
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.date(2010, 1, 12))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Date(2010, 1, 12, -1))
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.date(2009, 1, 12))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Date(2010, 1, 12, 0, 12))
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.date(2011, 1, 12))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Date(2010, 1, 12, 0, 0, -7))
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.date(2010, 1, 5))
+
+ def test0130DateTime(self):
+ '''
+ Test pyson.DateTime
+ '''
+ self.assert_(pyson.DateTime(2010, 1, 12, 10, 30, 20, 0,
+ -1, 12, -7, 2, 15, 30, 1).pyson() == {
+ '__class__': 'DateTime',
+ 'y': 2010,
+ 'M': 1,
+ 'd': 12,
+ 'h': 10,
+ 'm': 30,
+ 's': 20,
+ 'ms': 0,
+ 'dy': -1,
+ 'dM': 12,
+ 'dd': -7,
+ 'dh': 2,
+ 'dm': 15,
+ 'ds': 30,
+ 'dms': 1,
+ })
+
+ self.assertRaises(AssertionError, pyson.DateTime, 'test', 1, 12, 10,
+ 30, 20, 0, -1, 12, -7, 2, 15, 30, 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 'test', 12, 10,
+ 30, 20, 0, -1, 12, -7, 2, 15, 30, 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 'test', 10,
+ 30, 20, 0, -1, 12, -7, 2, 15, 30, 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 'test',
+ 30, 20, 0, -1, 12, -7, 2, 15, 30, 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10,
+ 'test', 20, 0, -1, 12, -7, 2, 15, 30, 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30,
+ 'test', 0, -1, 12, -7, 2, 15, 30, 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30,
+ 20, 'test', -1, 12, -7, 2, 15, 30, 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30,
+ 20, 0, 'test', 12, -7, 2, 15, 30, 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30,
+ 20, 0, -1, 'test', -7, 2, 15, 30, 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30,
+ 20, 0, -1, 12, 'test', 2, 15, 30, 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30,
+ 20, 0, -1, 12, -7, 'test', 15, 30, 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30,
+ 20, 0, -1, 12, -7, 2, 'test', 30, 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30,
+ 20, 0, -1, 12, -7, 2, 15, 'test', 1)
+ self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30,
+ 20, 0, -1, 12, -7, 2, 15, 30, 'test')
+
+ self.assert_(pyson.DateTime(2010, 1, 12, 10, 30, 20, 0,
+ -1, 12, -7, 2, 15, 30, 1).types() == set([datetime.datetime]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
+ 10, 30, 20, 0))
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.datetime(2010, 1, 12, 10, 30, 20, 0))
+
+ eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
+ 10, 30, 20, 0, -1))
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.datetime(2009, 1, 12, 10, 30, 20, 0))
+
+ eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
+ 10, 30, 20, 0, 0, 12))
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.datetime(2011, 1, 12, 10, 30, 20, 0))
+
+ eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
+ 10, 30, 20, 0, 0, 0, -7))
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.datetime(2010, 1, 5, 10, 30, 20, 0))
+
+ eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
+ 10, 30, 20, 0, 0, 0, 0, 12))
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.datetime(2010, 1, 12, 22, 30, 20, 0))
+
+ eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
+ 10, 30, 20, 0, 0, 0, 0, 0, -30))
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.datetime(2010, 1, 12, 10, 0, 20, 0))
+
+ eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
+ 10, 30, 20, 0, 0, 0, 0, 0, 0, 30))
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.datetime(2010, 1, 12, 10, 30, 50, 0))
+
+ eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
+ 10, 30, 20, 0, 0, 0, 0, 0, 0, 0, 200))
+ self.assert_(pyson.PYSONDecoder().decode(eval)
+ == datetime.datetime(2010, 1, 12, 10, 30, 20, 200))
+
+ def test0900Composite(self):
+ '''
+ Test Composite
+ '''
+
+ eval = pyson.PYSONEncoder().encode(['id', pyson.If(pyson.Not(
+ pyson.In('company', pyson.Eval('context', {}))), '=', '!='),
+ pyson.Get(pyson.Eval('context', {}), 'company', 0)])
+ self.assert_(pyson.PYSONDecoder({'context': {'company': 1}}
+ ).decode(eval) == ['id', '!=', 1])
+ self.assert_(pyson.PYSONDecoder({'context': {}}
+ ).decode(eval) == ['id', '=', 0])
+
class FieldsTestCase(unittest.TestCase):
'''
@@ -2488,6 +3003,7 @@ def suite():
if __name__ == '__main__':
suite = suite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(ToolsTestCase))
+ suite.addTests(unittest.TestLoader().loadTestsFromTestCase(PYSONTestCase))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(FieldsTestCase))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(ModelSingletonTestCase))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(ModelViewTestCase))
diff --git a/trytond/workflow/workflow.py b/trytond/workflow/workflow.py
index 7c0cf50296..c18548a81d 100644
--- a/trytond/workflow/workflow.py
+++ b/trytond/workflow/workflow.py
@@ -6,6 +6,7 @@ from trytond.model import ModelView, ModelSQL, fields
from trytond.report import Report
from trytond.tools import exec_command_pipe
from trytond.backend import TableHandler
+from trytond.pyson import Eval, Equal, Not
import expr
import base64
@@ -67,14 +68,14 @@ class WorkflowActivity(ModelSQL, ModelView):
('stopall', 'Stop All'),
], 'Kind', required=True)
action = fields.Text('Action', states={
- 'readonly': "kind == 'dummy'",
- 'required': "kind == 'function'",
+ 'readonly': Equal(Eval('kind'), 'dummy'),
+ 'required': Equal(Eval('kind'), 'function'),
})
flow_start = fields.Boolean('Flow Start')
flow_stop = fields.Boolean('Flow Stop')
subflow = fields.Many2One('workflow', 'Subflow', states={
- 'readonly': "kind != 'subflow'",
- 'required': "kind == 'subflow'",
+ 'readonly': Not(Equal(Eval('kind'), 'subflow')),
+ 'required': Equal(Eval('kind'), 'subflow'),
})
signal_send = fields.Char('Signal (subflow.*)')
out_transitions = fields.One2Many('workflow.transition', 'act_from',