diff --git a/doc/index.rst b/doc/index.rst
index a5af574d09e..4b988e747b4 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -14,6 +14,7 @@ OpenERP Server
02_architecture
03_module_dev
04_security
+ workflows
05_test_framework
06_misc
deployment-gunicorn
diff --git a/doc/workflows.rst b/doc/workflows.rst
new file mode 100644
index 00000000000..d8ba89bd1ad
--- /dev/null
+++ b/doc/workflows.rst
@@ -0,0 +1,306 @@
+.. _workflows:
+
+Workflows
+=========
+
+In OpenERP, a workflow is a technical artefact to manage a set of "things to do"
+associated to the records of some data model. The workflow provides a higher-
+level way to organize the things to do on a record.
+
+More specifically, a workflow is a directed graph where the nodes are called
+"activities" and the arcs are called "transitions".
+
+- Activities define work that should be done within the OpenERP server, such as
+ changing the state of some records, or sending emails.
+
+- Transitions control how the workflow progresses from activity to activity.
+
+In the definition of a workflow, one can attach conditions, signals, and
+triggers to transitions, so that the behavior of the workflow depends on user
+actions (such as clicking on a button), changes to records, or arbitrary Python
+code.
+
+Basics
+------
+
+Defining a workflow with data files is straightforward: a record "workflow" is
+given together with records for the activities and the transitions. For
+instance, here is a simple sequence of two activities defined in XML::
+
+
+ test.workflow
+ test.workflow.model
+ True
+
+
+
+
+ True
+ a
+ function
+ print_a()
+
+
+
+ True
+ b
+ function
+ print_b()
+
+
+
+
+
+
+
+A worfklow is always defined with respect to a particular model (the model is
+given by the attribute ``osv`` on the model ``workflow``). Methods specified in
+the activities or transitions will be called on that model.
+
+In the example code above, a workflow called "test_workflow" is created. It is
+made up of two activies, named "a" and "b", and one transition, going from "a"
+to "b".
+
+The first activity has its attribute ``flow_start`` set to ``True`` so that
+OpenERP knows where to start the workflow traversal after it is instanciated.
+Because ``on_create`` is set to True on the workflow record, the workflow is
+instanciated for each newly created record. (Otherwise, the workflow should be
+instanciated by other means, such as from some module Python code.)
+
+When the workflow is instanciated, it begins with activity "a". That activity
+is of kind ``function``, which means that the action ``print_a()`` is a method
+call on the model ``test.workflow`` (the usual ``cr, uid, ids, context``
+arguments are passed for you).
+
+The transition between "a" and "b" does not specify any condition. This means
+that the workflow instance immediately goes from "a" to "b" after "a" has been
+processed, and thus also processes activity "b".
+
+Transitions
+-----------
+
+Transitions provide the control structures to orchestrate a workflow. When an
+activity is completed, the workflow engine tries to get across transitions
+departing from the completed activity, towards the next activities. In their
+simplest form (as in the example above), they link activities sequentially:
+activities are processed as soon as the activities preceding them are completed.
+
+Instead of running all activities in one fell swoop, it is also possible to wait
+on transitions, going through them only when some criteria are met. The criteria
+are the conditions, the signals, and the triggers. They are detailed in the
+following sections.
+
+Conditions
+''''''''''
+
+When an activity has been completed, its outgoing transitions are inspected to
+determine whether it is possible for the workflow instance to proceed through
+them and reach the next activities. When only a condition is defined (i.e., no
+signal or trigger is defined), the condition is evaluated by OpenERP, and if it
+evaluates to ``True``, the worklfow instance progresses through the transition.
+If the condition is not met, it will be reevaluated every time the associated
+record is modified, or by an explicit method call to do it.
+
+By default, the attribute ``condition`` (i.e., the expression to be evaluated)
+is just "True", which trivially evaluates to ``True``. Note that the condition
+may be several lines long; in that case, the value of the last one determines
+whether the transition can be taken.
+
+In the condition evaluation environment, several symbols are conveniently
+defined (in addition to the OpenERP ``safe_eval`` environment):
+
+- all the model column names, and
+- all the browse record's attributes.
+
+Signals
+'''''''
+
+In addition to a condition, a transition can specify a signal name. When such
+a signal name is present, the transition is not taken directly, even if the
+condition evaluates to ``True``. Instead the transition blocks, waiting to be
+woken up.
+
+In order to wake up a transition with a defined signal name, the signal must be
+sent to the workflow instance. A common way to send a signal is to use a button
+in the user interface, using the element ```` with the signal name as
+the attribute ``name`` of the button. Once the button is clicked, the signal is
+sent to the workflow instance of the current record.
+
+.. note:: The condition is still evaluated when the signal is sent to the
+ workflow instance.
+
+Triggers
+''''''''
+
+With conditions that evaluate to ``False``, transitions are not taken (and thus
+the activity it leads to is not processed immediately). Still, the workflow
+instance can get new chances to progress across that transition by providing
+so-called triggers. The idea is that when the condition is not satisfied,
+triggers are recorded in database. Later, it is possible to wake up
+specifically the workflow instances that installed those triggers, offering
+them to reevaluate their transition conditions. This mechanism makes it cheaper
+to wake up workflow instances by targetting just a few of them (those that have
+installed the triggers) instead of all of them.
+
+Triggers are recorded in database as record IDs (together with the model name)
+and refer to the workflow instance waiting for those records. The transition
+definition provides a model name (attribute ``trigger_model``) and a Python
+expression (attribute ``trigger_expression``) that evaluates to a list of record
+IDs in the given model. Any of those records can wake up the workflow instance
+they are associated with.
+
+.. note:: Note that triggers are not re-installed whenever the transition is
+ re-tried.
+
+Splitting and joining transitions
+'''''''''''''''''''''''''''''''''
+
+When multiple transitions leave the same activity, or lead to the same activity,
+OpenERP provides some control over which transitions are actually taken, or how
+the reached activity will be processed. The attributes ``split_mode`` and
+``join_mode`` on the activity are used for such control. The possible values of
+those attributes are explained below.
+
+Activities
+----------
+
+While the transitions can be seen as the control structures of the workflows,
+activities are the places where everything happens, from changing record states
+to sending email.
+
+Different kinds of activities exist: ``Dummy``, ``Function``, ``Subflow``, and
+``Stop all``, each doing different things when the activity is processed. In
+addition to their kind, activies have other properties, detailed in the next
+sections.
+
+Flow start and flow stop
+''''''''''''''''''''''''
+
+The attribute ``flow_start`` is a boolean value specifying whether the activity
+is processed when the workflow is instanciated. Multiple activities can have
+their attribute ``flow_start`` set to ``True``. When instanciating a workflow
+for a record, OpenERP simply processes all of them, and evaluate all their
+outgoing transitions afterwards.
+
+The attribute ``flow_stop`` is a boolean value specifying whether the activity
+stops the workflow instance. A workflow instance is considered completed when
+all its activities with the attribute ``flow_stop`` set to ``True`` are
+completed.
+
+It is important for OpenERP to know when a workflow instance is completed. A
+workflow can have an activity that is actually another workflow (called a
+subflow); that activity is completed when the subflow is completed.
+
+Subflow
+'''''''
+
+An activity can embed a complete workflow, called a subflow (the embedding
+workflow is called the parent workflow). The workflow to instanciate is
+specified by attribute ``subflow_id``.
+
+.. note:: In the GUI, that attribute can not be set unless the kind of the
+ activity is ``Subflow``.
+
+The activity is considered completed (and its outgoing transitions ready to be
+evaluated) when the subflow is completed (see attribute ``flow_stop`` above).
+
+Sending a signal from a subflow
+'''''''''''''''''''''''''''''''
+
+When a workflow is embedded in an activity (as a subflow) of a workflow, the
+sublow can send a signal from its own activities to the parent workflow by
+giving a signal name in the attribute ``signal_send``. OpenERP processes those
+activities by sending the value of ``signal_send`` prefixed by "subflow." to
+the parent workflow instance.
+
+In other words, it is possible to react and get transitions in the parent
+workflow as activities are executed in the sublow.
+
+Server actions
+''''''''''''''
+
+An activity can run a "Server Action" by specifying its ID in the attribute
+``action_id``.
+
+Python action
+'''''''''''''
+
+An activity can execute some Python code, given by the attribute ``action``.
+The evaluation environment is the same as the one explained in the section
+`Conditions`_.
+
+Split mode
+''''''''''
+
+After an activity has been processed, its outgoing transitions are evaluated.
+Normally, if a transition can be taken, OpenERP traverses it and proceed to the
+activity the transition leads to.
+
+Actually, when more than a single transition is leaving an activity, OpenERP may
+proceed or not, depending on the other transitions. That is, the conditions on
+the transitions can be combined together, and the combined result instructs
+OpenERP to traverse zero, one, or all the transitions. The way they are combined
+is controlled by the attribute ``split_mode``.
+
+There are three possible split modes: ``XOR``, ``OR`` and ``AND``.
+
+``XOR``
+ When the transitions are combined with a ``XOR`` split mode, as soon as a
+ transition has a satisfied condition, the transition is traversed and the
+ others are skipped.
+
+``OR``
+ With the ``OR`` mode, all the transitions with a satisfied condition are
+ traversed. The remaining transitions will not be evaluated later.
+
+``AND``
+ With the ``AND`` mode, OpenERP will wait for all outgoing transition
+ conditions to be satisfied, then traverse all of them at once.
+
+Join mode
+'''''''''
+
+Just like outgoing transition conditions can be combined together to decide
+whether they can be traversed or not, incoming transitions can be combined
+together to decide if and when an activity may be processed. The attribute
+``join_mode`` controls that behavior.
+
+There are two possible join modes: ``XOR`` and ``AND``.
+
+``XOR``
+ With the ``XOR`` mode, an incoming transition with a satisfied condition is
+ traversed immediately, and enables the processing of the activity.
+
+``AND``
+ With the ``AND`` mode, OpenERP will wait until all incoming transitions have
+ been traversed before enabling the processing of the activity.
+
+Kinds
+'''''
+
+Activities can be of different kinds: ``dummy``, ``function``, ``subflow``, or
+``stopall``. The kind defines what type of work an activity can do.
+
+Dummy
+ The ``dummy`` kind is for activities that do nothing, or for activities that
+ only call a server action. Activities that do nothing can be used as hubs to
+ gather/dispatch transitions.
+
+Function
+ The ``function`` kind is for activities that only need to run some Python
+ code, and possibly a server action.
+
+Stop all
+ The ``stopall`` kind is for activities that will completely stop the
+ workflow instance and mark it as completed. In addition they can also run
+ some Python code.
+
+Subflow
+ When the kind of the activity is ``subflow``, the activity embeds another
+ workflow instance. When the subflow is completed, the activity is also
+ considered completed.
+
+ By default, the subflow is instanciated for the same record as the parent
+ workflow. It is possible to change that behavior by providing Python code
+ that returns a record ID (of the same data model as the subflow). The
+ embedded subflow instance is then the one of the given record.
diff --git a/openerp/tests/addons/test_workflow/__init__.py b/openerp/tests/addons/test_workflow/__init__.py
new file mode 100644
index 00000000000..fe4487156b1
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+import models
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_workflow/__openerp__.py b/openerp/tests/addons/test_workflow/__openerp__.py
new file mode 100644
index 00000000000..1b846f9f227
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/__openerp__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': 'test-workflow',
+ 'version': '0.1',
+ 'category': 'Tests',
+ 'description': """A module to play with workflows.""",
+ 'author': 'OpenERP SA',
+ 'maintainer': 'OpenERP SA',
+ 'website': 'http://www.openerp.com',
+ 'depends': ['base'],
+ 'data': ['data.xml'],
+ 'installable': True,
+ 'auto_install': False,
+}
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_workflow/data.xml b/openerp/tests/addons/test_workflow/data.xml
new file mode 100644
index 00000000000..6299baa77ea
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/data.xml
@@ -0,0 +1,517 @@
+
+
+
+
+
+ Test workflow
+ test.workflow.model
+
+
+
+
+
+
+ Test workflow
+ ir.actions.act_window
+ test.workflow.model
+ form
+ tree,form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ test.workflow
+ test.workflow.model
+ True
+
+
+
+
+ True
+ a
+ function
+ print_a()
+
+
+
+ b
+ function
+ print_b()
+
+
+
+ True
+ c
+ function
+ print_c()
+
+
+
+
+
+ a-b
+
+
+
+
+ condition()
+ test.workflow.trigger
+ [1]
+
+
+
+
+ test.workflow.a
+ test.workflow.model.a
+ True
+
+
+
+
+ True
+ True
+ a
+ dummy
+
+
+
+
+ test.workflow.b
+ test.workflow.model.b
+ True
+
+
+
+
+ True
+ True
+ a
+ function
+ write({'value': 1})
+
+
+
+
+ test.workflow.c
+ test.workflow.model.c
+ True
+
+
+
+
+ True
+ True
+ a
+ dummy
+ write({'value': 1})
+
+
+
+
+ test.workflow.d
+ test.workflow.model.d
+ True
+
+
+
+
+ True
+ True
+ a
+ stopall
+ write({'value': 1})
+
+
+
+
+ test.workflow.e
+ test.workflow.model.e
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+
+
+
+
+
+
+ test.workflow.f
+ test.workflow.model.f
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+
+
+ a-b
+
+
+
+
+ test.workflow.g
+ test.workflow.model.g
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+
+
+ False
+
+
+
+
+ test.workflow.h
+ test.workflow.model.h
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+ OR
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+ True
+ c
+ function
+ write({'value': 2})
+
+
+
+
+
+
+
+
+
+
+
+
+
+ test.workflow.i
+ test.workflow.model.i
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+ OR
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+ True
+ c
+ function
+ write({'value': 3})
+
+
+
+
+
+
+
+
+
+ False
+
+
+
+
+ test.workflow.j
+ test.workflow.model.j
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+ AND
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+ True
+ c
+ function
+ write({'value': 3})
+
+
+
+
+
+
+
+
+
+ False
+
+
+
+
+ test.workflow.k
+ test.workflow.model.k
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+ XOR
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+ True
+ c
+ function
+ write({'value': 2})
+
+
+
+
+
+
+
+
+
+
+
+
+
+ test.workflow.l
+ test.workflow.model.l
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+ OR
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+ OR
+
+
+
+ True
+ c
+ function
+ write({'value': 3})
+ XOR
+
+
+
+ True
+ d
+ function
+ write({'value': 3})
+ AND
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openerp/tests/addons/test_workflow/models.py b/openerp/tests/addons/test_workflow/models.py
new file mode 100644
index 00000000000..f7a5ef23b1a
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/models.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+import openerp
+
+class m(openerp.osv.orm.Model):
+ """ A model for which we will define a workflow (see data.xml). """
+ _name = 'test.workflow.model'
+
+ def print_(self, cr, uid, ids, s, context=None):
+ print ' Running activity `%s` for record %s' % (s, ids)
+ return True
+
+ def print_a(self, cr, uid, ids, context=None):
+ return self.print_(cr, uid, ids, 'a', context)
+
+ def print_b(self, cr, uid, ids, context=None):
+ return self.print_(cr, uid, ids, 'b', context)
+
+ def print_c(self, cr, uid, ids, context=None):
+ return self.print_(cr, uid, ids, 'c', context)
+
+ def condition(self, cr, uid, ids, context=None):
+ m = self.pool['test.workflow.trigger']
+ for r in m.browse(cr, uid, [1], context=context):
+ if not r.value:
+ return False
+ return True
+
+ def trigger(self, cr, uid, context=None):
+ return openerp.workflow.trg_trigger(uid, 'test.workflow.trigger', 1, cr)
+
+class n(openerp.osv.orm.Model):
+ """ A model used for the trigger feature. """
+ _name = 'test.workflow.trigger'
+ _columns = { 'value': openerp.osv.fields.boolean('Value') }
+ _defaults = { 'value': False }
+
+class a(openerp.osv.orm.Model):
+ _name = 'test.workflow.model.a'
+ _columns = { 'value': openerp.osv.fields.integer('Value') }
+ _defaults = { 'value': 0 }
+
+class b(openerp.osv.orm.Model):
+ _name = 'test.workflow.model.b'
+ _inherit = 'test.workflow.model.a'
+
+class c(openerp.osv.orm.Model):
+ _name = 'test.workflow.model.c'
+ _inherit = 'test.workflow.model.a'
+
+class d(openerp.osv.orm.Model):
+ _name = 'test.workflow.model.d'
+ _inherit = 'test.workflow.model.a'
+
+class e(openerp.osv.orm.Model):
+ _name = 'test.workflow.model.e'
+ _inherit = 'test.workflow.model.a'
+
+for name in 'bcdefghijkl':
+ type(
+ name,
+ (openerp.osv.orm.Model,),
+ {
+ '_name': 'test.workflow.model.%s' % name,
+ '_inherit': 'test.workflow.model.a',
+ })
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_workflow/tests/__init__.py b/openerp/tests/addons/test_workflow/tests/__init__.py
new file mode 100644
index 00000000000..a09d09fe74a
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/tests/__init__.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+
+from . import test_workflow
+
+fast_suite = [
+]
+
+checks = [
+ test_workflow,
+]
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_workflow/tests/test_workflow.py b/openerp/tests/addons/test_workflow/tests/test_workflow.py
new file mode 100644
index 00000000000..e9ff7b9cd1e
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/tests/test_workflow.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+import openerp
+from openerp import SUPERUSER_ID
+from openerp.tests import common
+
+
+class test_workflows(common.TransactionCase):
+
+ def check_activities(self, model_name, i, names):
+ """ Check that the record i has workitems in the given activity names.
+ """
+ instance = self.registry('workflow.instance')
+ workitem = self.registry('workflow.workitem')
+
+ # Given the workflow instance associated to the record ...
+ instance_id = instance.search(
+ self.cr, SUPERUSER_ID,
+ [('res_type', '=', model_name), ('res_id', '=', i)])
+ self.assertTrue( instance_id, 'A workflow instance is expected.')
+
+ # ... get all its workitems ...
+ workitem_ids = workitem.search(
+ self.cr, SUPERUSER_ID,
+ [('inst_id', '=', instance_id[0])])
+ self.assertTrue(
+ workitem_ids,
+ 'The workflow instance should have workitems.')
+
+ # ... and check the activity the are in against the provided names.
+ workitem_records = workitem.browse(
+ self.cr, SUPERUSER_ID, workitem_ids)
+ self.assertEqual(
+ sorted([item.act_id.name for item in workitem_records]),
+ sorted(names))
+
+ def check_value(self, model_name, i, value):
+ """ Check that the record i has the given value.
+ """
+ model = self.registry(model_name)
+ record = model.read(self.cr, SUPERUSER_ID, [i], ['value'])[0]
+ self.assertEqual(record['value'], value)
+
+ def test_workflow(self):
+ model = self.registry('test.workflow.model')
+ trigger = self.registry('test.workflow.trigger')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+
+ # a -> b is just a signal.
+ model.signal_workflow(self.cr, SUPERUSER_ID, [i], 'a-b')
+ self.check_activities(model._name, i, ['b'])
+
+ # b -> c is a trigger (which is False),
+ # so we remain in the b activity.
+ model.trigger(self.cr, SUPERUSER_ID, [i])
+ self.check_activities(model._name, i, ['b'])
+
+ # b -> c is a trigger (which is set to True).
+ # so we go in c when the trigger is called.
+ trigger.write(self.cr, SUPERUSER_ID, [1], {'value': True})
+ model.trigger(self.cr, SUPERUSER_ID)
+ self.check_activities(model._name, i, ['c'])
+
+ self.assertEqual(
+ True,
+ True)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_a(self):
+ model = self.registry('test.workflow.model.a')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 0)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_b(self):
+ model = self.registry('test.workflow.model.b')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 1)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_c(self):
+ model = self.registry('test.workflow.model.c')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 0)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_d(self):
+ model = self.registry('test.workflow.model.d')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 1)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_e(self):
+ model = self.registry('test.workflow.model.e')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['b'])
+ self.check_value(model._name, i, 2)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_f(self):
+ model = self.registry('test.workflow.model.f')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 1)
+
+ model.signal_workflow(self.cr, SUPERUSER_ID, [i], 'a-b')
+ self.check_activities(model._name, i, ['b'])
+ self.check_value(model._name, i, 2)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_g(self):
+ model = self.registry('test.workflow.model.g')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 1)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_h(self):
+ model = self.registry('test.workflow.model.h')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['b', 'c'])
+ self.check_value(model._name, i, 2)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_i(self):
+ model = self.registry('test.workflow.model.i')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['b'])
+ self.check_value(model._name, i, 2)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_j(self):
+ model = self.registry('test.workflow.model.j')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 1)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_k(self):
+ model = self.registry('test.workflow.model.k')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ # Non-determinisitic: can be b or c
+ # self.check_activities(model._name, i, ['b'])
+ # self.check_activities(model._name, i, ['c'])
+ self.check_value(model._name, i, 2)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_l(self):
+ model = self.registry('test.workflow.model.l')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['c', 'c', 'd'])
+ self.check_value(model._name, i, 3)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
diff --git a/openerp/workflow/wkf_expr.py b/openerp/workflow/wkf_expr.py
index b43fa83e3de..0b83195aec2 100644
--- a/openerp/workflow/wkf_expr.py
+++ b/openerp/workflow/wkf_expr.py
@@ -19,62 +19,92 @@
#
##############################################################################
+"""
+Evaluate workflow code found in activity actions and transition conditions.
+"""
+
import openerp
from openerp.tools.safe_eval import safe_eval as eval
class Env(dict):
- def __init__(self, cr, uid, model, ids):
+ """
+ Dictionary class used as an environment to evaluate workflow code (such as
+ the condition on transitions).
+
+ This environment provides sybmols for cr, uid, id, model name, model
+ instance, column names, and all the record (the one obtained by browsing
+ the provided ID) attributes.
+ """
+ def __init__(self, cr, uid, model, id):
self.cr = cr
self.uid = uid
self.model = model
- self.ids = ids
+ self.id = id
+ self.ids = [id]
self.obj = openerp.registry(cr.dbname)[model]
self.columns = self.obj._columns.keys() + self.obj._inherit_fields.keys()
def __getitem__(self, key):
if (key in self.columns) or (key in dir(self.obj)):
- res = self.obj.browse(self.cr, self.uid, self.ids[0])
+ res = self.obj.browse(self.cr, self.uid, self.id)
return res[key]
else:
return super(Env, self).__getitem__(key)
-def _eval_expr(cr, ident, workitem, action):
- ret=False
- assert action, 'You used a NULL action in a workflow, use dummy node instead.'
- for line in action.split('\n'):
+def _eval_expr(cr, ident, workitem, lines):
+ """
+ Evaluate each line of ``lines`` with the ``Env`` environment, returning
+ the value of the last line.
+ """
+ assert lines, 'You used a NULL action in a workflow, use dummy node instead.'
+ uid, model, id = ident
+ result = False
+ for line in lines.split('\n'):
line = line.strip()
if not line:
continue
- uid=ident[0]
- model=ident[1]
- ids=[ident[2]]
- if line =='True':
- ret=True
- elif line =='False':
- ret=False
+ if line == 'True':
+ result = True
+ elif line == 'False':
+ result = False
else:
- env = Env(cr, uid, model, ids)
- ret = eval(line, env, nocopy=True)
- return ret
+ env = Env(cr, uid, model, id)
+ result = eval(line, env, nocopy=True)
+ return result
def execute_action(cr, ident, workitem, activity):
- obj = openerp.registry(cr.dbname)['ir.actions.server']
- ctx = {'active_model':ident[1], 'active_id':ident[2], 'active_ids':[ident[2]]}
- result = obj.run(cr, ident[0], [activity['action_id']], ctx)
+ """
+ Evaluate the ir.actions.server action specified in the activity.
+ """
+ uid, model, id = ident
+ ir_actions_server = openerp.registry(cr.dbname)['ir.actions.server']
+ context = { 'active_model': model, 'active_id': id, 'active_ids': [id] }
+ result = ir_actions_server.run(cr, uid, [activity['action_id']], context)
return result
def execute(cr, ident, workitem, activity):
+ """
+ Evaluate the action specified in the activity.
+ """
return _eval_expr(cr, ident, workitem, activity['action'])
def check(cr, workitem, ident, transition, signal):
+ """
+ Test if a transition can be taken. The transition can be taken if:
+
+ - the signal name matches,
+ - the uid is SUPERUSER_ID or the user groups contains the transition's
+ group,
+ - the condition evaluates to a truish value.
+ """
if transition['signal'] and signal != transition['signal']:
return False
uid = ident[0]
- if transition['group_id'] and uid != 1:
+ if uid != openerp.SUPERUSER_ID and transition['groups_id']:
registry = openerp.registry(cr.dbname)
user_groups = registry['res.users'].read(cr, uid, [uid], ['groups_id'])[0]['groups_id']
- if not transition['group_id'] in user_groups:
+ if transition['group_id'] not in user_groups:
return False
return _eval_expr(cr, ident, workitem, transition['condition'])