[MERGE] workflows: added some documentation and some tests.

bzr revid: vmt@openerp.com-20130731151636-05lgd5i08rie6yqp
This commit is contained in:
Vo Minh Thu 2013-07-31 17:16:36 +02:00
commit a507a9ea05
9 changed files with 1156 additions and 22 deletions

View File

@ -14,6 +14,7 @@ OpenERP Server
02_architecture
03_module_dev
04_security
workflows
05_test_framework
06_misc
deployment-gunicorn

306
doc/workflows.rst Normal file
View File

@ -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::
<record id="test_workflow" model="workflow">
<field name="name">test.workflow</field>
<field name="osv">test.workflow.model</field>
<field name="on_create">True</field>
</record>
<record id="activity_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow"/>
<field name="flow_start">True</field>
<field name="name">a</field>
<field name="kind">function</field>
<field name="action">print_a()</field>
</record>
<record id="activity_b" model="workflow.activity">
<field name="wkf_id" ref="test_workflow"/>
<field name="flow_stop">True</field>
<field name="name">b</field>
<field name="kind">function</field>
<field name="action">print_b()</field>
</record>
<record id="trans_a_b" model="workflow.transition">
<field name="act_from" ref="activity_a"/>
<field name="act_to" ref="activity_b"/>
</record>
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 ``<button/>`` 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.

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import models
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -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:

View File

@ -0,0 +1,517 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_test_workflow_model" model="ir.ui.view">
<field name="name">Test workflow</field>
<field name="model">test.workflow.model</field>
<field name="arch" type="xml">
<form string="Test workflow">
<button name="a-b" string="a-b" type="workflow" icon="gtk-ok" colspan="1"/>
<label string="a-b"/>
<button name="trigger" string="trigger" type="object" icon="gtk-ok" colspan="1"/>
<label string="trigger"/>
</form>
</field>
</record>
<record id="action_test_workflow" model="ir.actions.act_window">
<field name="name">Test workflow</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">test.workflow.model</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem icon="STOCK_PREFERENCES" id="base.menu_tests" name="Tests" sequence="1000000"/>
<menuitem id="menu_test_workflow" parent="base.menu_tests" name="Test workflow"/>
<menuitem id="menu_test_workflow_leaf"
name="Test workflow"
action="action_test_workflow"
parent="menu_test_workflow"/>
<record id="test_workflow_trigger_1" model="test.workflow.trigger">
<!-- A single trigger record, with known ID 1 -->
</record>
<!-- A simple workflow:
a -signal-> b -trigger-> c
-->
<record id="test_workflow" model="workflow">
<field name="name">test.workflow</field>
<field name="osv">test.workflow.model</field>
<field name="on_create">True</field>
</record>
<record id="activity_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow"/>
<field name="flow_start">True</field>
<field name="name">a</field>
<field name="kind">function</field>
<field name="action">print_a()</field>
</record>
<record id="activity_b" model="workflow.activity">
<field name="wkf_id" ref="test_workflow"/>
<field name="name">b</field>
<field name="kind">function</field>
<field name="action">print_b()</field>
</record>
<record id="activity_c" model="workflow.activity">
<field name="wkf_id" ref="test_workflow"/>
<field name="flow_stop">True</field>
<field name="name">c</field>
<field name="kind">function</field>
<field name="action">print_c()</field>
</record>
<record id="trans_a_b" model="workflow.transition">
<field name="act_from" ref="activity_a"/>
<field name="act_to" ref="activity_b"/>
<field name="signal">a-b</field>
</record>
<record id="trans_b_c" model="workflow.transition">
<field name="act_from" ref="activity_b"/>
<field name="act_to" ref="activity_c"/>
<field name="condition">condition()</field>
<field name="trigger_model">test.workflow.trigger</field>
<field name="trigger_expr_id">[1]</field>
</record>
<!-- Workflow A (a single activity):
a
-->
<record id="test_workflow_a" model="workflow">
<field name="name">test.workflow.a</field>
<field name="osv">test.workflow.model.a</field>
<field name="on_create">True</field>
</record>
<record id="activity_a_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_a"/>
<field name="flow_start">True</field>
<field name="flow_stop">True</field>
<field name="name">a</field>
<field name="kind">dummy</field>
</record>
<!-- Workflow B (a single activity):
a
The function is run when the record is created.
-->
<record id="test_workflow_b" model="workflow">
<field name="name">test.workflow.b</field>
<field name="osv">test.workflow.model.b</field>
<field name="on_create">True</field>
</record>
<record id="activity_b_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_b"/>
<field name="flow_start">True</field>
<field name="flow_stop">True</field>
<field name="name">a</field>
<field name="kind">function</field>
<field name="action">write({'value': 1})</field>
</record>
<!-- Workflow C (a single activity):
a
The function is not run when the kind is dummy and no action_id is provided.
-->
<record id="test_workflow_c" model="workflow">
<field name="name">test.workflow.c</field>
<field name="osv">test.workflow.model.c</field>
<field name="on_create">True</field>
</record>
<record id="activity_c_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_c"/>
<field name="flow_start">True</field>
<field name="flow_stop">True</field>
<field name="name">a</field>
<field name="kind">dummy</field>
<field name="action">write({'value': 1})</field>
</record>
<!-- Workflow D (a single activity):
a
The function is run when the kind is stopall and no action_id is provided.
-->
<record id="test_workflow_d" model="workflow">
<field name="name">test.workflow.d</field>
<field name="osv">test.workflow.model.d</field>
<field name="on_create">True</field>
</record>
<record id="activity_d_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_d"/>
<field name="flow_start">True</field>
<field name="flow_stop">True</field>
<field name="name">a</field>
<field name="kind">stopall</field>
<field name="action">write({'value': 1})</field>
</record>
<!-- Workflow E:
a -True-> b
Both activities are run when the workflow is instanciated.
-->
<record id="test_workflow_e" model="workflow">
<field name="name">test.workflow.e</field>
<field name="osv">test.workflow.model.e</field>
<field name="on_create">True</field>
</record>
<record id="activity_e_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_e"/>
<field name="flow_start">True</field>
<field name="name">a</field>
<field name="kind">function</field>
<field name="action">write({'value': 1})</field>
</record>
<record id="activity_e_b" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_e"/>
<field name="flow_stop">True</field>
<field name="name">b</field>
<field name="kind">function</field>
<field name="action">write({'value': 2})</field>
</record>
<record id="trans_e_a_b" model="workflow.transition">
<field name="act_from" ref="activity_e_a"/>
<field name="act_to" ref="activity_e_b"/>
</record>
<!-- Workflow F:
a -signal-> b
Same as E but with a signal on the transition.
-->
<record id="test_workflow_f" model="workflow">
<field name="name">test.workflow.f</field>
<field name="osv">test.workflow.model.f</field>
<field name="on_create">True</field>
</record>
<record id="activity_f_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_f"/>
<field name="flow_start">True</field>
<field name="name">a</field>
<field name="kind">function</field>
<field name="action">write({'value': 1})</field>
</record>
<record id="activity_f_b" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_f"/>
<field name="flow_stop">True</field>
<field name="name">b</field>
<field name="kind">function</field>
<field name="action">write({'value': 2})</field>
</record>
<record id="trans_f_a_b" model="workflow.transition">
<field name="act_from" ref="activity_f_a"/>
<field name="act_to" ref="activity_f_b"/>
<field name="signal">a-b</field>
</record>
<!-- Workflow G:
a -False-> b
-->
<record id="test_workflow_g" model="workflow">
<field name="name">test.workflow.g</field>
<field name="osv">test.workflow.model.g</field>
<field name="on_create">True</field>
</record>
<record id="activity_g_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_g"/>
<field name="flow_start">True</field>
<field name="name">a</field>
<field name="kind">function</field>
<field name="action">write({'value': 1})</field>
</record>
<record id="activity_g_b" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_g"/>
<field name="flow_stop">True</field>
<field name="name">b</field>
<field name="kind">function</field>
<field name="action">write({'value': 2})</field>
</record>
<record id="trans_g_a_b" model="workflow.transition">
<field name="act_from" ref="activity_g_a"/>
<field name="act_to" ref="activity_g_b"/>
<field name="condition">False</field>
</record>
<!-- Workflow H:
a or -> b { value: 2 }
`-> c { value: 2 }
Whether the action of b or c is exectued last is non-deterministic.
-->
<record id="test_workflow_h" model="workflow">
<field name="name">test.workflow.h</field>
<field name="osv">test.workflow.model.h</field>
<field name="on_create">True</field>
</record>
<record id="activity_h_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_h"/>
<field name="flow_start">True</field>
<field name="name">a</field>
<field name="kind">function</field>
<field name="action">write({'value': 1})</field>
<field name="split_mode">OR</field>
</record>
<record id="activity_h_b" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_h"/>
<field name="flow_stop">True</field>
<field name="name">b</field>
<field name="kind">function</field>
<field name="action">write({'value': 2})</field>
</record>
<record id="activity_h_c" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_h"/>
<field name="flow_stop">True</field>
<field name="name">c</field>
<field name="kind">function</field>
<field name="action">write({'value': 2})</field>
</record>
<record id="trans_h_a_b" model="workflow.transition">
<field name="act_from" ref="activity_h_a"/>
<field name="act_to" ref="activity_h_b"/>
</record>
<record id="trans_h_a_c" model="workflow.transition">
<field name="act_from" ref="activity_h_a"/>
<field name="act_to" ref="activity_h_c"/>
</record>
<!-- Workflow I:
a or -> b { value: 2 }
`false> c { value: 3 }
-->
<record id="test_workflow_i" model="workflow">
<field name="name">test.workflow.i</field>
<field name="osv">test.workflow.model.i</field>
<field name="on_create">True</field>
</record>
<record id="activity_i_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_i"/>
<field name="flow_start">True</field>
<field name="name">a</field>
<field name="kind">function</field>
<field name="action">write({'value': 1})</field>
<field name="split_mode">OR</field>
</record>
<record id="activity_i_b" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_i"/>
<field name="flow_stop">True</field>
<field name="name">b</field>
<field name="kind">function</field>
<field name="action">write({'value': 2})</field>
</record>
<record id="activity_i_c" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_i"/>
<field name="flow_stop">True</field>
<field name="name">c</field>
<field name="kind">function</field>
<field name="action">write({'value': 3})</field>
</record>
<record id="trans_i_a_b" model="workflow.transition">
<field name="act_from" ref="activity_i_a"/>
<field name="act_to" ref="activity_i_b"/>
</record>
<record id="trans_i_a_c" model="workflow.transition">
<field name="act_from" ref="activity_i_a"/>
<field name="act_to" ref="activity_i_c"/>
<field name="condition">False</field>
</record>
<!-- Workflow J:
a and -> b { value: 2 }
`False> c { value: 3 }
This will stay in a because all transitions should be True at the same time.
-->
<record id="test_workflow_j" model="workflow">
<field name="name">test.workflow.j</field>
<field name="osv">test.workflow.model.j</field>
<field name="on_create">True</field>
</record>
<record id="activity_j_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_j"/>
<field name="flow_start">True</field>
<field name="name">a</field>
<field name="kind">function</field>
<field name="action">write({'value': 1})</field>
<field name="split_mode">AND</field>
</record>
<record id="activity_j_b" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_j"/>
<field name="flow_stop">True</field>
<field name="name">b</field>
<field name="kind">function</field>
<field name="action">write({'value': 2})</field>
</record>
<record id="activity_j_c" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_j"/>
<field name="flow_stop">True</field>
<field name="name">c</field>
<field name="kind">function</field>
<field name="action">write({'value': 3})</field>
</record>
<record id="trans_j_a_b" model="workflow.transition">
<field name="act_from" ref="activity_j_a"/>
<field name="act_to" ref="activity_j_b"/>
</record>
<record id="trans_j_a_c" model="workflow.transition">
<field name="act_from" ref="activity_j_a"/>
<field name="act_to" ref="activity_j_c"/>
<field name="condition">False</field>
</record>
<!-- Workflow K:
a xor -> b { value: 2 }
`> c { value: 2 }
Only one (truish) transition is taken with a XOR.
-->
<record id="test_workflow_k" model="workflow">
<field name="name">test.workflow.k</field>
<field name="osv">test.workflow.model.k</field>
<field name="on_create">True</field>
</record>
<record id="activity_k_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_k"/>
<field name="flow_start">True</field>
<field name="name">a</field>
<field name="kind">function</field>
<field name="action">write({'value': 1})</field>
<field name="split_mode">XOR</field>
</record>
<record id="activity_k_b" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_k"/>
<field name="flow_stop">True</field>
<field name="name">b</field>
<field name="kind">function</field>
<field name="action">write({'value': 2})</field>
</record>
<record id="activity_k_c" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_k"/>
<field name="flow_stop">True</field>
<field name="name">c</field>
<field name="kind">function</field>
<field name="action">write({'value': 2})</field>
</record>
<record id="trans_k_a_b" model="workflow.transition">
<field name="act_from" ref="activity_k_a"/>
<field name="act_to" ref="activity_k_b"/>
</record>
<record id="trans_k_a_c" model="workflow.transition">
<field name="act_from" ref="activity_k_a"/>
<field name="act_to" ref="activity_k_c"/>
</record>
<!-- Workflow L:
a -> xor c { value: 3 }
b ´
a -> and d { value: 3 }
b ´
c is run for each incoming (and taken) transition.
d is run once when all its incoming transitions are taken at the same time.
-->
<record id="test_workflow_l" model="workflow">
<field name="name">test.workflow.l</field>
<field name="osv">test.workflow.model.l</field>
<field name="on_create">True</field>
</record>
<record id="activity_l_a" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_l"/>
<field name="flow_start">True</field>
<field name="name">a</field>
<field name="kind">function</field>
<field name="action">write({'value': 1})</field>
<field name="split_mode">OR</field>
</record>
<record id="activity_l_b" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_l"/>
<field name="flow_start">True</field>
<field name="name">b</field>
<field name="kind">function</field>
<field name="action">write({'value': 2})</field>
<field name="split_mode">OR</field>
</record>
<record id="activity_l_c" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_l"/>
<field name="flow_stop">True</field>
<field name="name">c</field>
<field name="kind">function</field>
<field name="action">write({'value': 3})</field>
<field name="join_mode">XOR</field>
</record>
<record id="activity_l_d" model="workflow.activity">
<field name="wkf_id" ref="test_workflow_l"/>
<field name="flow_stop">True</field>
<field name="name">d</field>
<field name="kind">function</field>
<field name="action">write({'value': 3})</field>
<field name="join_mode">AND</field>
</record>
<record id="trans_l_a_c" model="workflow.transition">
<field name="act_from" ref="activity_l_a"/>
<field name="act_to" ref="activity_l_c"/>
</record>
<record id="trans_l_b_c" model="workflow.transition">
<field name="act_from" ref="activity_l_b"/>
<field name="act_to" ref="activity_l_c"/>
</record>
<record id="trans_l_a_d" model="workflow.transition">
<field name="act_from" ref="activity_l_a"/>
<field name="act_to" ref="activity_l_d"/>
</record>
<record id="trans_l_b_d" model="workflow.transition">
<field name="act_from" ref="activity_l_b"/>
<field name="act_to" ref="activity_l_d"/>
</record>
</data>
</openerp>

View File

@ -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:

View File

@ -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:

View File

@ -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])

View File

@ -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'])