diff --git a/CHANGELOG b/CHANGELOG
index db94a58be5..e7cb59ece6 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Remove use of python code in workflow XML
* Remove ir.action.wizard_size
* Rename expand and fill attributes into yexpand and yfill
* Add xalign and yalign as field attributes
diff --git a/trytond/backend/mysql/init.sql b/trytond/backend/mysql/init.sql
index 3e37beb233..d2b3c86eef 100644
--- a/trytond/backend/mysql/init.sql
+++ b/trytond/backend/mysql/init.sql
@@ -105,12 +105,12 @@ CREATE TABLE wkf_activity (
subflow BIGINT,
split_mode VARCHAR(255),
join_mode VARCHAR(255),
- kind VARCHAR(255) NOT NULL,
name VARCHAR(255),
signal_send VARCHAR(255),
flow_start BOOLEAN NOT NULL,
flow_stop BOOLEAN NOT NULL,
- action TEXT,
+ stop_other BOOLEAN NOT NULL,
+ action VARCHAR(255),
PRIMARY KEY(id),
CONSTRAINT wkf_activity_workflow_fkey FOREIGN KEY (workflow) REFERENCES wkf (id) ON DELETE CASCADE,
CONSTRAINT wkf_activity_subflow_fkey FOREIGN KEY (subflow) REFERENCES wkf (id) ON DELETE SET NULL
diff --git a/trytond/backend/postgresql/init.sql b/trytond/backend/postgresql/init.sql
index 3e3c4229c5..bb83dbd803 100644
--- a/trytond/backend/postgresql/init.sql
+++ b/trytond/backend/postgresql/init.sql
@@ -127,12 +127,12 @@ CREATE TABLE wkf_activity (
subflow INTEGER,
split_mode VARCHAR NOT NULL,
join_mode VARCHAR NOT NULL,
- kind VARCHAR NOT NULL,
name VARCHAR NOT NULL,
signal_send VARCHAR,
flow_start BOOLEAN NOT NULL,
flow_stop BOOLEAN NOT NULL,
- action TEXT,
+ stop_other BOOLEAN NOT NULL,
+ action VARCHAR,
PRIMARY KEY(id),
FOREIGN KEY (workflow) REFERENCES wkf (id) ON DELETE CASCADE,
FOREIGN KEY (subflow) REFERENCES wkf (id) ON DELETE SET NULL
diff --git a/trytond/backend/sqlite/init.sql b/trytond/backend/sqlite/init.sql
index 9d1879c227..e20292c445 100644
--- a/trytond/backend/sqlite/init.sql
+++ b/trytond/backend/sqlite/init.sql
@@ -90,12 +90,12 @@ CREATE TABLE wkf_activity (
subflow INTEGER,
split_mode VARCHAR,
join_mode VARCHAR,
- kind VARCHAR,
name VARCHAR,
signal_send VARCHAR,
flow_start BOOLEAN,
flow_stop BOOLEAN,
- action TEXT
+ stop_other BOOLEAN,
+ action VARCHAR
);
CREATE TABLE wkf_transition (
diff --git a/trytond/test/__init__.py b/trytond/test/__init__.py
index e457e73eb7..3b5643eca3 100644
--- a/trytond/test/__init__.py
+++ b/trytond/test/__init__.py
@@ -9,3 +9,4 @@ from export_data import *
from trigger import *
from access import *
from wizard import *
+from workflow import *
diff --git a/trytond/test/__tryton__.py b/trytond/test/__tryton__.py
index d854433951..90de2fc51e 100644
--- a/trytond/test/__tryton__.py
+++ b/trytond/test/__tryton__.py
@@ -5,10 +5,11 @@
'description': '''A module for unittest.
''',
'active': True,
- 'depends': ['ir', 'res'],
+ 'depends': ['ir', 'res', 'workflow'],
'xml': [
'import_data.xml',
'sequence.xml',
+ 'workflow.xml',
],
'translation': [
],
diff --git a/trytond/test/workflow.py b/trytond/test/workflow.py
new file mode 100644
index 0000000000..4df177e727
--- /dev/null
+++ b/trytond/test/workflow.py
@@ -0,0 +1,35 @@
+# 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.model import ModelSQL, ModelWorkflow, fields
+
+
+class WorkflowedModel(ModelWorkflow, ModelSQL):
+ _name = 'test.workflowed'
+
+ name = fields.Char('Name')
+ state = fields.Char('State')
+ value = fields.Integer('Value')
+
+ def default_transitions(self):
+ return 0
+
+ def default_value(self):
+ return 0
+
+ def workflow_start(self, workflowed):
+ self.write([workflowed.id], {'state': 'Start'})
+
+ def workflow_middle(self, workflowed):
+ self.write([workflowed.id], {'state': 'Middle'})
+
+ def workflow_end(self, workflowed):
+ self.write([workflowed.id], {'state': 'End'})
+
+ def start_middle_ok(self, workflowed):
+ return workflowed.value > 4
+
+ def middle_end_ok(self, workflowed):
+ return workflowed.value < 8
+
+WorkflowedModel()
diff --git a/trytond/test/workflow.xml b/trytond/test/workflow.xml
new file mode 100644
index 0000000000..a19f47af94
--- /dev/null
+++ b/trytond/test/workflow.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+ Test workflow
+ test.workflowed
+
+
+
+ Start
+ workflow_start
+
+
+
+
+ Middle
+ workflow_middle
+
+
+
+ End
+ workflow_end
+
+
+
+
+
+
+ start_middle_ok
+
+
+
+
+ middle_end_ok
+
+
+
diff --git a/trytond/tests/test_workflow.py b/trytond/tests/test_workflow.py
new file mode 100644
index 0000000000..c7bcb91058
--- /dev/null
+++ b/trytond/tests/test_workflow.py
@@ -0,0 +1,66 @@
+# 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 unittest
+
+from trytond.transaction import Transaction
+from trytond.tests.test_tryton import (POOL, DB_NAME, USER, CONTEXT,
+ install_module)
+
+
+class WorkflowTestCase(unittest.TestCase):
+
+ def setUp(self):
+ install_module('test')
+ self.workflow_obj = POOL.get('test.workflowed')
+
+ def test0010object_creation(self):
+ 'Test workflow object creation'
+ with Transaction().start(DB_NAME, USER, CONTEXT) as transaction:
+ wkf_id = self.workflow_obj.create({
+ 'name': 'value = 3',
+ 'value': 3,
+ })
+ self.assertEqual(self.workflow_obj.read(wkf_id)['state'], 'Start')
+
+ wkf_id = self.workflow_obj.create({
+ 'name': 'value = 10',
+ 'value': 10,
+ })
+ self.assertEqual(self.workflow_obj.read(wkf_id)['state'], 'Middle')
+
+ wkf_id = self.workflow_obj.create({
+ 'name': 'value = 6',
+ 'value': 6,
+ })
+ self.assertEqual(self.workflow_obj.read(wkf_id)['state'], 'End')
+
+ transaction.cursor.rollback()
+
+ def test0020object_modification(self):
+ 'Test workflow object modification'
+ with Transaction().start(DB_NAME, USER, CONTEXT):
+ wkf_id = self.workflow_obj.create({
+ 'name': 'value = 3',
+ 'value': 3,
+ })
+ wkf_id = self.workflow_obj.create({
+ 'name': 'value = 10',
+ 'value': 10,
+ })
+
+ wkf_id, = self.workflow_obj.search([('name', '=', 'value = 3')])
+ self.workflow_obj.write(wkf_id, {'value': 11})
+ self.assertEqual(self.workflow_obj.read(wkf_id)['state'], 'Middle')
+
+ wkf_id, = self.workflow_obj.search([('name', '=', 'value = 10')])
+ self.workflow_obj.write(wkf_id, {'value': 6})
+ self.assertEqual(self.workflow_obj.read(wkf_id)['state'], 'End')
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(WorkflowTestCase)
+
+
+if __name__ == '__main__':
+ unittest.TextTestRunner(verbosity=2).run(suite())
diff --git a/trytond/workflow/expr.py b/trytond/workflow/expr.py
deleted file mode 100644
index 268d1b0986..0000000000
--- a/trytond/workflow/expr.py
+++ /dev/null
@@ -1,80 +0,0 @@
-#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.pool import Pool
-from trytond.tools import safe_eval
-from trytond.transaction import Transaction
-
-
-class EnvCall(object):
-
- def __init__(self, obj, method, obj_id):
- self.obj = obj
- self.method = method
- self.obj_id = obj_id
-
- def __call__(self, *args, **kargs):
- return getattr(self.obj, self.method)(self.obj_id, *args, **kargs)
-
-
-class Env(dict):
-
- def __init__(self, model, obj_id):
- super(Env, self).__init__()
- self.model = model
- self.obj_id = obj_id
- self.obj = Pool(Transaction().cursor.dbname).get(model)
- self.browse = self.obj.browse(obj_id)
- self.columns = self.obj._columns.keys() + \
- self.obj._inherit_fields.keys()
-
- def __getitem__(self, key):
- if key == 'context':
- return Transaction().context
- elif key in self.columns:
- res = self.browse[key]
- return res
- elif key in dir(self.obj):
- return EnvCall(self.obj, key, self.obj_id)
- else:
- return super(Env, self).__getitem__(key)
-
-def eval_expr(model, obj_id, action):
- res = False
- env = Env(model, obj_id)
- for line in action.split('\n'):
- if line == 'True':
- res = True
- elif line =='False':
- res = False
- else:
- res = safe_eval(line, env)
- return res
-
-def execute(model, obj_id, activity):
- '''
- Execute
-
- :param model: the model name
- :param obj_id: the record id
- :param activity: a BrowseRecord of workflow.activity
- '''
- return eval_expr(model, obj_id, activity.action)
-
-def check(model, obj_id, transition, signal):
- '''
- Check
-
- :param model: the model name
- :param obj_id: the record id
- :param transition: a BrowseRecord of workflow.transition
- '''
- if transition.signal:
- if signal != transition.signal:
- return False
-
- if transition.group and Transaction().user != 0:
- user_obj = Pool(Transaction().cursor.dbname).get('res.user')
- user_groups = user_obj.get_groups()
- if transition.group.id not in user_groups:
- return False
- return eval_expr(model, obj_id, transition.condition)
diff --git a/trytond/workflow/workflow.py b/trytond/workflow/workflow.py
index c8969d93f9..ad4592a896 100644
--- a/trytond/workflow/workflow.py
+++ b/trytond/workflow/workflow.py
@@ -1,13 +1,12 @@
#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 os
-import expr
import base64
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
+from trytond.pyson import Eval, Equal, Not, Bool
from trytond.transaction import Transaction
from trytond.pool import Pool
@@ -63,30 +62,27 @@ class WorkflowActivity(ModelSQL, ModelView):
('XOR', 'Xor'),
('AND', 'And'),
], 'Join Mode', required=True)
- kind = fields.Selection([
- ('dummy', 'Dummy'),
- ('function', 'Function'),
- ('subflow', 'Subflow'),
- ('stopall', 'Stop All'),
- ], 'Kind', required=True)
- action = fields.Text('Action', states={
- 'readonly': Equal(Eval('kind'), 'dummy'),
- 'required': Equal(Eval('kind'), 'function'),
- })
+ method = fields.Char('Method')
flow_start = fields.Boolean('Flow Start')
flow_stop = fields.Boolean('Flow Stop')
- subflow = fields.Many2One('workflow', 'Subflow', states={
- 'readonly': Not(Equal(Eval('kind'), 'subflow')),
- 'required': Equal(Eval('kind'), 'subflow'),
- })
+ stop_other = fields.Boolean('Stop Other')
+ subflow = fields.Many2One('workflow', 'Subflow')
signal_send = fields.Char('Signal (subflow.*)')
out_transitions = fields.One2Many('workflow.transition', 'act_from',
'Outgoing transitions')
in_transitions = fields.One2Many('workflow.transition', 'act_to',
'Incoming transitions')
- #TODO add a _constraint on subflow without action
- #to have the same model than the workflow
+ def init(self, module_name):
+ super(WorkflowActivity, self).init(module_name)
+ cursor = Transaction().cursor
+ table = TableHandler(cursor, self, module_name)
+
+ # Migration from 2.0
+ if table.column_exist('kind'):
+ cursor.execute('UPDATE "%s" SET stop_other = %%s '
+ "WHERE kind='stopall'" % self._table, (True,))
+ table.drop_column('kind', exception=True)
def default_kind(self):
return 'dummy'
@@ -103,6 +99,9 @@ class WorkflowActivity(ModelSQL, ModelView):
def default_flow_stop(self):
return False
+ def default_stop_other(self):
+ return False
+
WorkflowActivity()
@@ -113,7 +112,9 @@ class WorkflowTransition(ModelSQL, ModelView):
_rec_name = 'signal'
_description = __doc__
trigger_model = fields.Char('Trigger Type')
- trigger_expr_id = fields.Char('Trigger Expr ID')
+ trigger_ids = fields.Char('Trigger Expr ID', states={
+ 'required': Bool(Eval('trigger_model')),
+ })
signal = fields.Char('Signal (button Name)')
group = fields.Many2One('res.group', 'Group Required')
condition = fields.Char('Condition', required=True)
@@ -125,7 +126,7 @@ class WorkflowTransition(ModelSQL, ModelView):
'trans_id', 'inst_id', 'Instances')
def default_condition(self):
- return 'True'
+ return ''
WorkflowTransition()
@@ -327,19 +328,20 @@ class WorkflowWorkitem(ModelSQL, ModelView):
triggers = triggers and not res
if triggers:
- for transition in activity.out_transitions:
- if transition.trigger_model:
- ids = expr.eval_expr( workitem.instance.res_type,
- workitem.instance.res_id,
- transition.trigger_expr_id)
- with Transaction().set_user(0):
- for res_id in ids:
- trigger_obj.create({
- 'model': transition.trigger_model,
- 'res_id': res_id,
- 'instance': workitem.instance.id,
- 'workitem': workitem.id,
- })
+ trigger_transitions = (t for t in activity.out_transitions
+ if t.trigger_model)
+ for transition in trigger_transitions:
+ model_obj = pool.get(workitem.instance.res_type)
+ model_ids_fct = getattr(model_obj, transition.trigger_ids)
+ ids = model_ids_fct(model_obj.browse(workitem.instance.res_id))
+ with Transaction().set_user(0):
+ for res_id in ids:
+ trigger_obj.create({
+ 'model': transition.trigger_model,
+ 'res_id': res_id,
+ 'instance': workitem.instance.id,
+ 'workitem': workitem.id,
+ })
return True
def _state_set(self, workitem, state):
@@ -358,18 +360,9 @@ class WorkflowWorkitem(ModelSQL, ModelView):
instance_obj.validate(overflow.instance, activity.signal_send,
force_running=True)
- if activity.kind == 'dummy':
- if workitem.state == 'active':
- self._state_set(workitem, 'complete')
- elif activity.kind == 'function':
- if workitem.state == 'active':
- self._state_set(workitem, 'running')
- expr.execute(workitem.instance.res_type,
- workitem.instance.res_id, activity)
- self._state_set(workitem, 'complete')
- elif activity.kind == 'stopall':
- if workitem.state == 'active':
- self._state_set(workitem, 'running')
+ if workitem.state == 'active':
+ self._state_set(workitem, 'running')
+ if activity.stop_other:
ids = self.search([
('instance', '=', workitem.instance.id),
('id', '!=', workitem.id),
@@ -377,41 +370,64 @@ class WorkflowWorkitem(ModelSQL, ModelView):
#XXX check if delete must not be replace by _state_set 'complete'
with Transaction().set_user(0):
self.delete(ids)
- if activity.action:
- expr.execute(workitem.instance.res_type,
- workitem.instance.res_id, activity)
- self._state_set(workitem, 'complete')
- elif activity.kind == 'subflow':
- if workitem.state == 'active':
- self._state_set(workitem, 'running')
- if activity.action:
- id_new = expr.execute(workitem.instance.res_type,
- workitem.instance.res_id, activity)
- if not id_new:
- with Transaction().set_user(0):
- self.delete(workitem.id)
- return False
+ if activity.method:
+ id_new = self._execute_action(workitem, activity)
+ else:
+ id_new = None
+ if activity.subflow:
+ if activity.method and id_new:
with Transaction().set_user(0):
instance_id = instance_obj.search([
('res_id', '=', id_new),
('workflow', '=', activity.subflow.id),
], limit=1)[0]
- else:
+ elif not activity.method:
instance_id = instance_obj.create({
'res_type': workitem.instance.res_type,
'res_id': workitem.instance.res_id,
'workflow': activity.subflow.id,
})
+ else:
+ with Transaction().set_user(0):
+ self.delete(workitem.id)
+ return False
self.write(workitem.id, {
'subflow': instance_id,
})
#XXX must be changed with a cache reset on BrowseRecord
workitem._data[workitem.id]['subflow'] = instance_id
- elif workitem.state == 'running':
- if workitem.subflow.state == 'complete':
- self._state_set(workitem, 'complete')
+ else:
+ self._state_set(workitem, 'complete')
+ elif workitem.state == 'running' and activity.subflow:
+ if workitem.subflow.state == 'complete':
+ self._state_set(workitem, 'complete')
+
return True
+ def _execute_action(self, workitem, activity):
+ model_obj = Pool().get(workitem.instance.res_type)
+ wkf_action = getattr(model_obj, activity.method)
+ return wkf_action(model_obj.browse(workitem.instance.res_id))
+
+ def _activate_transition(self, workitem, transition, signal):
+ pool = Pool()
+ user_obj = pool.get('res.user')
+ if transition.signal:
+ if signal != transition.signal:
+ return False
+
+ if transition.group and Transaction().user != 0:
+ user_groups = user_obj.get_groups()
+ if transition.group.id not in user_groups:
+ return False
+
+ model_obj = pool.get(workitem.instance.res_type)
+ if transition.condition:
+ test_fct = getattr(model_obj, transition.condition)
+ return test_fct(model_obj.browse(workitem.instance.res_id))
+ else:
+ return True
+
def _split_test(self, workitem, split_mode, signal=None):
pool = Pool()
instance_obj = pool.get('workflow.instance')
@@ -419,8 +435,7 @@ class WorkflowWorkitem(ModelSQL, ModelView):
transitions = []
if split_mode == 'XOR' or split_mode == 'OR':
for transition in workitem.activity.out_transitions:
- if expr.check(workitem.instance.res_type,
- workitem.instance.res_id, transition, signal):
+ if self._activate_transition(workitem, transition, signal):
test = True
transitions.append(transition)
if split_mode == 'XOR':
@@ -428,8 +443,7 @@ class WorkflowWorkitem(ModelSQL, ModelView):
else:
test = True
for transition in workitem.activity.out_transitions:
- if not expr.check(workitem.instance.res_type,
- workitem.instance.res_id, transition, signal):
+ if not self._activate_transition(workitem, transition, signal):
test = False
break
if transition.id not in \
diff --git a/trytond/workflow/workflow.xml b/trytond/workflow/workflow.xml
index 31087c0115..2fc046f836 100644
--- a/trytond/workflow/workflow.xml
+++ b/trytond/workflow/workflow.xml
@@ -88,10 +88,8 @@ this repository contains the full copyright notices and license terms. -->
-
-
-
-
+
+
@@ -112,7 +110,7 @@ this repository contains the full copyright notices and license terms. -->
-
+
@@ -137,7 +135,7 @@ this repository contains the full copyright notices and license terms. -->
-
+
@@ -167,7 +165,7 @@ this repository contains the full copyright notices and license terms. -->
-
+
@@ -230,8 +228,8 @@ this repository contains the full copyright notices and license terms. -->
-
-
+
+
]]>
@@ -248,7 +246,7 @@ this repository contains the full copyright notices and license terms. -->
-
+
]]>