[MERGE] from trunk
bzr revid: chm@openerp.com-20130801124635-yj8xny76sl5ea0r2
This commit is contained in:
commit
f6172723d0
|
@ -14,6 +14,7 @@ OpenERP Server
|
||||||
02_architecture
|
02_architecture
|
||||||
03_module_dev
|
03_module_dev
|
||||||
04_security
|
04_security
|
||||||
|
workflows
|
||||||
05_test_framework
|
05_test_framework
|
||||||
06_misc
|
06_misc
|
||||||
deployment-gunicorn
|
deployment-gunicorn
|
||||||
|
|
|
@ -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.
|
|
@ -92,7 +92,8 @@ openerp.base = function(instance) {
|
||||||
|
|
||||||
start: function() {
|
start: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
return self.get_client().
|
// desactivated for now because apps does not work anyway due to changes in the framework
|
||||||
|
/*return self.get_client().
|
||||||
done(function(client) {
|
done(function(client) {
|
||||||
client.replace(self.$el).
|
client.replace(self.$el).
|
||||||
done(function() {
|
done(function() {
|
||||||
|
@ -100,13 +101,13 @@ openerp.base = function(instance) {
|
||||||
client.do_action(self.remote_action_id, {hide_breadcrumb: true});
|
client.do_action(self.remote_action_id, {hide_breadcrumb: true});
|
||||||
});
|
});
|
||||||
}).
|
}).
|
||||||
fail(function(client) {
|
fail(function(client) {*/
|
||||||
self.do_warn(_t('OpenERP Apps Unreachable'), _t('Showing locally available modules'));
|
self.do_warn(_t('OpenERP Apps Unreachable'), _t('Showing locally available modules'), true);
|
||||||
self.rpc('/web/action/load', {action_id: self.failback_action_id}).done(function(action) {
|
self.rpc('/web/action/load', {action_id: self.failback_action_id}).done(function(action) {
|
||||||
self.do_action(action);
|
self.do_action(action);
|
||||||
instance.webclient.menu.open_action(action.id);
|
instance.webclient.menu.open_action(action.id);
|
||||||
});
|
});
|
||||||
});
|
//});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import models
|
||||||
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -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:
|
|
@ -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>
|
|
@ -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:
|
|
@ -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:
|
|
@ -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])
|
|
@ -19,62 +19,92 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
Evaluate workflow code found in activity actions and transition conditions.
|
||||||
|
"""
|
||||||
|
|
||||||
import openerp
|
import openerp
|
||||||
from openerp.tools.safe_eval import safe_eval as eval
|
from openerp.tools.safe_eval import safe_eval as eval
|
||||||
|
|
||||||
class Env(dict):
|
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.cr = cr
|
||||||
self.uid = uid
|
self.uid = uid
|
||||||
self.model = model
|
self.model = model
|
||||||
self.ids = ids
|
self.id = id
|
||||||
|
self.ids = [id]
|
||||||
self.obj = openerp.registry(cr.dbname)[model]
|
self.obj = openerp.registry(cr.dbname)[model]
|
||||||
self.columns = self.obj._columns.keys() + self.obj._inherit_fields.keys()
|
self.columns = self.obj._columns.keys() + self.obj._inherit_fields.keys()
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
if (key in self.columns) or (key in dir(self.obj)):
|
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]
|
return res[key]
|
||||||
else:
|
else:
|
||||||
return super(Env, self).__getitem__(key)
|
return super(Env, self).__getitem__(key)
|
||||||
|
|
||||||
def _eval_expr(cr, ident, workitem, action):
|
def _eval_expr(cr, ident, workitem, lines):
|
||||||
ret=False
|
"""
|
||||||
assert action, 'You used a NULL action in a workflow, use dummy node instead.'
|
Evaluate each line of ``lines`` with the ``Env`` environment, returning
|
||||||
for line in action.split('\n'):
|
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()
|
line = line.strip()
|
||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
uid=ident[0]
|
if line == 'True':
|
||||||
model=ident[1]
|
result = True
|
||||||
ids=[ident[2]]
|
elif line == 'False':
|
||||||
if line =='True':
|
result = False
|
||||||
ret=True
|
|
||||||
elif line =='False':
|
|
||||||
ret=False
|
|
||||||
else:
|
else:
|
||||||
env = Env(cr, uid, model, ids)
|
env = Env(cr, uid, model, id)
|
||||||
ret = eval(line, env, nocopy=True)
|
result = eval(line, env, nocopy=True)
|
||||||
return ret
|
return result
|
||||||
|
|
||||||
def execute_action(cr, ident, workitem, activity):
|
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]]}
|
Evaluate the ir.actions.server action specified in the activity.
|
||||||
result = obj.run(cr, ident[0], [activity['action_id']], ctx)
|
"""
|
||||||
|
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
|
return result
|
||||||
|
|
||||||
def execute(cr, ident, workitem, activity):
|
def execute(cr, ident, workitem, activity):
|
||||||
|
"""
|
||||||
|
Evaluate the action specified in the activity.
|
||||||
|
"""
|
||||||
return _eval_expr(cr, ident, workitem, activity['action'])
|
return _eval_expr(cr, ident, workitem, activity['action'])
|
||||||
|
|
||||||
def check(cr, workitem, ident, transition, signal):
|
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']:
|
if transition['signal'] and signal != transition['signal']:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
uid = ident[0]
|
uid = ident[0]
|
||||||
if transition['group_id'] and uid != 1:
|
if uid != openerp.SUPERUSER_ID and transition['groups_id']:
|
||||||
registry = openerp.registry(cr.dbname)
|
registry = openerp.registry(cr.dbname)
|
||||||
user_groups = registry['res.users'].read(cr, uid, [uid], ['groups_id'])[0]['groups_id']
|
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 False
|
||||||
|
|
||||||
return _eval_expr(cr, ident, workitem, transition['condition'])
|
return _eval_expr(cr, ident, workitem, transition['condition'])
|
||||||
|
|
|
@ -19,9 +19,10 @@ from . import scaffold
|
||||||
from . import uninstall
|
from . import uninstall
|
||||||
from . import update
|
from . import update
|
||||||
from . import web
|
from . import web
|
||||||
|
from . import grunt_tests
|
||||||
|
|
||||||
command_list_server = (conf, cron, drop, initialize, model, module, read, run_tests,
|
command_list_server = (conf, cron, drop, initialize, model, module, read, run_tests,
|
||||||
scaffold, uninstall, update, web, )
|
scaffold, uninstall, update, web, grunt_tests, )
|
||||||
|
|
||||||
command_list_client = (Call, Open, Show, ConsumeNothing, ConsumeMemory,
|
command_list_client = (Call, Open, Show, ConsumeNothing, ConsumeMemory,
|
||||||
LeakMemory, ConsumeCPU, Bench, BenchRead,
|
LeakMemory, ConsumeCPU, Bench, BenchRead,
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
"""
|
||||||
|
Search for Gruntfile.js files in all the addons and launch them using the 'grunt test' command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import common
|
||||||
|
import fnmatch
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def grunt_tester(directories, log = sys.stdout):
|
||||||
|
result = 0
|
||||||
|
|
||||||
|
matches = []
|
||||||
|
for direc in directories:
|
||||||
|
for root, dirnames, filenames in os.walk(direc):
|
||||||
|
for filename in fnmatch.filter(filenames, 'Gruntfile.js'):
|
||||||
|
full = os.path.join(root, filename)
|
||||||
|
if re.match(r"(^.*?/node_modules/.*$)|(^.*?/lib/.*$)", full):
|
||||||
|
continue
|
||||||
|
matches.append(full)
|
||||||
|
|
||||||
|
for file_ in matches:
|
||||||
|
folder = os.path.dirname(file_)
|
||||||
|
p = subprocess.Popen(['npm', 'install'], cwd=folder)
|
||||||
|
if p.wait() != 0:
|
||||||
|
raise Exception("Failed to install dependencies for Gruntfile located in folder %s" % folder)
|
||||||
|
|
||||||
|
p = subprocess.Popen(['grunt', 'test', '--no-color'], cwd=folder, stdout=log, stderr=log)
|
||||||
|
if p.wait() != 0:
|
||||||
|
result = 1
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def run(args):
|
||||||
|
if args.addons:
|
||||||
|
args.addons = args.addons.split(':')
|
||||||
|
else:
|
||||||
|
args.addons = []
|
||||||
|
result = grunt_tester(args.addons)
|
||||||
|
if result != 0:
|
||||||
|
sys.exit(result)
|
||||||
|
|
||||||
|
def add_parser(subparsers):
|
||||||
|
parser = subparsers.add_parser('grunt-tests',
|
||||||
|
description='Run the tests contained in Gruntfile.js files.')
|
||||||
|
common.add_addons_argument(parser)
|
||||||
|
|
||||||
|
parser.set_defaults(run=run)
|
Loading…
Reference in New Issue