[REM] workflow: Remove workflow engine, documentation and tests

This commit is contained in:
Yannick Tivisse
2016-11-23 11:52:39 +01:00
parent 94d2b97da4
commit 98cb4719db
69 changed files with 7 additions and 3569 deletions
-3
View File
@@ -463,9 +463,6 @@ class PosOrder(models.Model):
new_invoice.with_context(local_context).sudo().compute_taxes()
order.sudo().write({'state': 'invoiced'})
# this workflow signal didn't exist on account.invoice -> should it have been 'invoice_open' ? (and now method .action_invoice_open())
# shouldn't the created invoice be marked as paid, seing the customer paid in the POS?
# new_invoice.sudo().signal_workflow('validate')
if not Invoice:
return {}
+1 -1
View File
@@ -14,5 +14,5 @@ class MailMail(models.Model):
if mail_sent and mail.model == 'purchase.order':
purchase_order = self.env['purchase.order'].browse(mail.res_id)
if purchase_order.state == 'draft':
self.env['purchase.order'].signal_workflow([mail.res_id], 'send_rfq')
purchase_order.state = 'sent'
return super(MailMail, self)._postprocess_sent_message(mail_sent=mail_sent)
-1
View File
@@ -304,7 +304,6 @@ class PurchaseOrder(models.Model):
@api.multi
def print_quotation(self):
self.write({'state': "sent"})
return self.env['report'].get_action(self, 'purchase.report_purchasequotation')
@api.multi
-5
View File
@@ -868,11 +868,6 @@ class DataSet(http.Controller):
return clean_action(action)
return False
@http.route('/web/dataset/exec_workflow', type='json', auth="user")
def exec_workflow(self, model, id, signal):
request.session.check_security()
return request.env[model].browse(id).signal_workflow(signal)[id]
@http.route('/web/dataset/resequence', type='json', auth="user")
def resequence(self, model, ids, field='sequence', offset=0):
""" Re-sequences a number of records in the model, by their ids
@@ -548,9 +548,6 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
name_create: function(name, context) {
return this._model.call('name_create', [name], {context: this.get_context(context)});
},
exec_workflow: function (id, signal) {
return this._model.exec_workflow(id, signal);
},
get_context: function(request_context) {
return this._model.context(request_context);
},
@@ -940,10 +937,6 @@ var BufferedDataSet = DataSetStatic.extend({
this.evict_record(args[0][0]);
return this._super(method, args);
},
exec_workflow: function (id, signal) {
this.evict_record(id);
return this._super(id, signal);
},
alter_ids: function(n_ids, options) {
var dirty = !_.isEqual(this.ids, n_ids);
this._super(n_ids, options);
@@ -54,19 +54,6 @@ var Model = Class.extend({
kwargs: kwargs
}, options);
},
/**
* Executes a signal on the designated workflow, on the bound OpenERP model
*
* @param {Number} id workflow identifier
* @param {String} signal signal to trigger on the workflow
*/
exec_workflow: function (id, signal) {
return session.rpc('/web/dataset/exec_workflow', {
model: this.name,
id: id,
signal: signal
});
},
});
return Model;
@@ -127,8 +127,6 @@ var View = Widget.extend({
active_ids: dataset.ids,
active_id: record_id
})).then(handler);
} else {
return dataset.exec_workflow(record_id, action_data.name).then(handler);
}
},
do_show: function () {
@@ -217,7 +217,6 @@ class CrmLead(models.Model):
@api.multi
def update_lead_portal(self, values):
# YTI FIXME : Use the standard workflow defined in crm_activity
self.check_access_rights('write')
for lead in self:
if values['date_action'] == '':
-122
View File
@@ -1295,128 +1295,6 @@ activated as actual fields on the model.
}
]
Workflow manipulations
----------------------
:ref:`reference/workflows` can be moved along by sending them *signals*.
Instead of using the top-level ``execute_kw``, signals are sent using
``exec_workflow``.
Signals are sent to a specific record, and possibly trigger a transition on
the workflow instance associated with the record.
.. container:: doc-aside
.. warning:: this example needs ``account`` module installed
.. switcher::
.. code-block:: python
client = models.execute_kw(
db, uid, password,
'res.partner', 'search_read',
[[('customer', '=', True)]],
{'limit': 1, 'fields': [
'property_account_receivable_id',
'property_payment_term_id',
'property_account_position_id']
})[0]
invoice_id = models.execute_kw(
db, uid, password,
'account.invoice', 'create', [{
'partner_id': client['id'],
'account_id': client['property_account_receivable_id'][0],
'invoice_line_ids': [(0, False, {'name': "AAA"})]
}])
models.exec_workflow(
db, uid, password, 'account.invoice', 'invoice_open', invoice_id)
.. code-block:: php
$client = $models->execute_kw(
$db, $uid, $password,
'res.partner', 'search_read',
array(array(array('customer', '=', true))),
array(
'limit' => 1,
'fields' => array(
'property_account_receivable_id',
'property_payment_term_id',
'property_account_position_id'
)))[0];
$invoice_id = $models->execute_kw(
$db, $uid, $password,
'account.invoice', 'create', array(array(
'partner_id' => $client['id'],
'account_id' => $client['property_account_receivable_id'][0],
'invoice_line_ids' => array(array(0, false, array('name' => "AAA")))
)));
$models->exec_workflow(
$db, $uid, $password,
'account.invoice', 'invoice_open',
$invoice_id);
.. code-block:: ruby
client = models.execute_kw(
db, uid, password,
'res.partner', 'search_read',
[[['customer', '=', true]]],
{limit: 1, fields: %w(property_account_receivable_id property_payment_term_id property_account_position_id)}
)[0]
invoice_id = models.execute_kw(
db, uid, password,
'account.invoice', 'create', [{
partner_id: client['id'],
account_id: client['property_account_receivable_id'][0],
invoice_line_ids: [[0, false, {name: "AAA"}]]
}])
models.exec_workflow(
db, uid, password,
'account.invoice', 'invoice_open', invoice_id)
.. code-block:: java
final Map<String, Object> c = (Map<String, Object>)
((Object[])models.execute("execute_kw", asList(
db, uid, password,
"res.partner", "search_read",
asList(
asList(
asList("customer", "=", true))),
new HashMap<String, Object>() {{
put("limit", 1);
put("fields", asList(
"property_account_receivable_id",
"property_payment_term_id",
"property_account_position_id"
));
}}
)))[0];
final Integer invoice_id = (Integer)models.execute(
"execute_kw", asList(
db, uid, password,
"account.invoice", "create",
asList(new HashMap<String, Object>() {{
put("partner_id", c.get("id"));
put("account_id", ((Object[])c.get("property_account_receivable_id"))[0]);
put("invoice_line_ids", asList(
asList(0, false, new HashMap<String, Object>() {{
put("name", "AAA");
}})
));
}})
));
models.execute(
"exec_workflow", asList(
db, uid, password,
"account.invoice", "invoice_open", invoice_id));
Report printing
---------------
-84
View File
@@ -1230,90 +1230,6 @@ Kanban views define the structure of each card as a mix of form elements
.. patch::
Workflows
=========
Workflows are models associated to business objects describing their dynamics.
Workflows are also used to track processes that evolve over time.
.. exercise:: Almost a workflow
Add a ``state`` field to the *Session* model. It will be used to define
a workflow-ish.
A sesion can have three possible states: Draft (default), Confirmed and
Done.
In the session form, add a (read-only) field to
visualize the state, and buttons to change it. The valid transitions are:
* Draft -> Confirmed
* Confirmed -> Draft
* Confirmed -> Done
* Done -> Draft
.. only:: solutions
#. Add a new ``state`` field
#. Add state-transitioning methods, those can be called from view
buttons to change the record's state
#. And add the relevant buttons to the session's form view
.. patch::
Workflows may be associated with any object in Odoo, and are entirely
customizable. Workflows are used to structure and manage the lifecycles of
business objects and documents, and define transitions, triggers, etc. with
graphical tools. Workflows, activities (nodes or actions) and transitions
(conditions) are declared as XML records, as usual. The tokens that navigate
in workflows are called workitems.
.. warning::
A workflow associated with a model is only created when the
model's records are created. Thus there is no workflow instance
associated with session instances created before the workflow's
definition
.. exercise:: Workflow
Replace the ad-hoc *Session* workflow by a real workflow. Transform the
*Session* form view so its buttons call the workflow instead of the
model's methods.
.. only:: solutions
.. patch::
.. tip::
In order to check if instances of the workflow are correctly
created alongside sessions, go to :menuselection:`Settings -->
Technical --> Workflows --> Instances`
.. exercise:: Automatic transitions
Automatically transition sessions from *Draft* to *Confirmed* when more
than half the session's seats are reserved.
.. only:: solutions
.. patch::
.. exercise:: Server actions
Replace the Python methods for synchronizing session state by
server actions.
Both the workflow and the server actions could have been created entirely
from the UI.
.. only:: solutions
.. patch::
Security
========
-102
View File
@@ -1,102 +0,0 @@
# HG changeset patch
# Parent c72382bf0c5794135fa318f6ba59899b2277d8c5
# Parent 82f902dbb7aab4d3ddd9caaa9170536afa9d274d
diff --git a/openacademy/__manifest__.py b/openacademy/__manifest__.py
--- a/openacademy/__manifest__.py
+++ b/openacademy/__manifest__.py
@@ -29,6 +29,7 @@
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
+ 'views/session_workflow.xml',
],
# only loaded in demonstration mode
'demo': [
diff --git a/openacademy/models.py b/openacademy/models.py
--- a/openacademy/models.py
+++ b/openacademy/models.py
@@ -70,7 +70,7 @@ class Session(models.Model):
('draft', "Draft"),
('confirmed', "Confirmed"),
('done', "Done"),
- ], default='draft')
+ ])
@api.multi
def action_draft(self):
diff --git a/openacademy/views/openacademy.xml b/openacademy/views/openacademy.xml
--- a/openacademy/views/openacademy.xml
+++ b/openacademy/views/openacademy.xml
@@ -97,13 +97,13 @@
<field name="arch" type="xml">
<form string="Session Form">
<header>
- <button name="action_draft" type="object"
+ <button name="draft" type="workflow"
string="Reset to draft"
states="confirmed,done"/>
- <button name="action_confirm" type="object"
+ <button name="confirm" type="workflow"
string="Confirm" states="draft"
class="oe_highlight"/>
- <button name="action_done" type="object"
+ <button name="done" type="workflow"
string="Mark as done" states="confirmed"
class="oe_highlight"/>
<field name="state" widget="statusbar"/>
diff --git a/openacademy/views/session_workflow.xml b/openacademy/views/session_workflow.xml
new file mode 100644
--- /dev/null
+++ b/openacademy/views/session_workflow.xml
@@ -0,0 +1,50 @@
+<odoo>
+ <data>
+ <record model="workflow" id="wkf_session">
+ <field name="name">OpenAcademy sessions workflow</field>
+ <field name="osv">openacademy.session</field>
+ <field name="on_create">True</field>
+ </record>
+
+ <record model="workflow.activity" id="draft">
+ <field name="name">Draft</field>
+ <field name="wkf_id" ref="wkf_session"/>
+ <field name="flow_start" eval="True"/>
+ <field name="kind">function</field>
+ <field name="action">action_draft()</field>
+ </record>
+ <record model="workflow.activity" id="confirmed">
+ <field name="name">Confirmed</field>
+ <field name="wkf_id" ref="wkf_session"/>
+ <field name="kind">function</field>
+ <field name="action">action_confirm()</field>
+ </record>
+ <record model="workflow.activity" id="done">
+ <field name="name">Done</field>
+ <field name="wkf_id" ref="wkf_session"/>
+ <field name="kind">function</field>
+ <field name="action">action_done()</field>
+ </record>
+
+ <record model="workflow.transition" id="session_draft_to_confirmed">
+ <field name="act_from" ref="draft"/>
+ <field name="act_to" ref="confirmed"/>
+ <field name="signal">confirm</field>
+ </record>
+ <record model="workflow.transition" id="session_confirmed_to_draft">
+ <field name="act_from" ref="confirmed"/>
+ <field name="act_to" ref="draft"/>
+ <field name="signal">draft</field>
+ </record>
+ <record model="workflow.transition" id="session_done_to_draft">
+ <field name="act_from" ref="done"/>
+ <field name="act_to" ref="draft"/>
+ <field name="signal">draft</field>
+ </record>
+ <record model="workflow.transition" id="session_confirmed_to_done">
+ <field name="act_from" ref="confirmed"/>
+ <field name="act_to" ref="done"/>
+ <field name="signal">done</field>
+ </record>
+ </data>
+</odoo>
@@ -1,64 +0,0 @@
# HG changeset patch
# Parent 0764cf061d021a0a5b2811f5ec5d835f799630fc
Index: addons/openacademy/views/session_workflow.xml
===================================================================
--- addons.orig/openacademy/views/session_workflow.xml 2014-08-26 17:26:17.339783114 +0200
+++ addons/openacademy/views/session_workflow.xml 2014-08-26 17:26:17.331783114 +0200
@@ -6,24 +6,50 @@
<field name="on_create">True</field>
</record>
+ <record model="ir.actions.server" id="set_session_to_draft">
+ <field name="name">Set session to Draft</field>
+ <field name="model_id" ref="model_openacademy_session"/>
+ <field name="code">
+model.search([('id', 'in', context['active_ids'])]).action_draft()
+ </field>
+ </record>
<record model="workflow.activity" id="draft">
<field name="name">Draft</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="flow_start" eval="True"/>
- <field name="kind">function</field>
- <field name="action">action_draft()</field>
+ <field name="kind">dummy</field>
+ <field name="action"></field>
+ <field name="action_id" ref="set_session_to_draft"/>
+ </record>
+
+ <record model="ir.actions.server" id="set_session_to_confirmed">
+ <field name="name">Set session to Confirmed</field>
+ <field name="model_id" ref="model_openacademy_session"/>
+ <field name="code">
+model.search([('id', 'in', context['active_ids'])]).action_confirm()
+ </field>
</record>
<record model="workflow.activity" id="confirmed">
<field name="name">Confirmed</field>
<field name="wkf_id" ref="wkf_session"/>
- <field name="kind">function</field>
- <field name="action">action_confirm()</field>
+ <field name="kind">dummy</field>
+ <field name="action"></field>
+ <field name="action_id" ref="set_session_to_confirmed"/>
+ </record>
+
+ <record model="ir.actions.server" id="set_session_to_done">
+ <field name="name">Set session to Done</field>
+ <field name="model_id" ref="model_openacademy_session"/>
+ <field name="code">
+model.search([('id', 'in', context['active_ids'])]).action_done()
+ </field>
</record>
<record model="workflow.activity" id="done">
<field name="name">Done</field>
<field name="wkf_id" ref="wkf_session"/>
- <field name="kind">function</field>
- <field name="action">action_done()</field>
+ <field name="kind">dummy</field>
+ <field name="action"></field>
+ <field name="action_id" ref="set_session_to_done"/>
</record>
<record model="workflow.transition" id="session_draft_to_confirmed">
@@ -1,19 +0,0 @@
# HG changeset patch
# Parent 3b19489a47d41e72f88490eb389a05977761c8d2
Index: addons/openacademy/views/session_workflow.xml
===================================================================
--- addons.orig/openacademy/views/session_workflow.xml 2014-08-26 17:26:16.583783126 +0200
+++ addons/openacademy/views/session_workflow.xml 2014-08-26 17:26:16.579783126 +0200
@@ -46,5 +46,11 @@
<field name="act_to" ref="done"/>
<field name="signal">done</field>
</record>
+
+ <record model="workflow.transition" id="session_auto_confirm_half_filled">
+ <field name="act_from" ref="draft"/>
+ <field name="act_to" ref="confirmed"/>
+ <field name="condition">taken_seats &gt; 50</field>
+ </record>
</data>
</odoo>
-3
View File
@@ -25,9 +25,6 @@ exercise-gantt
exercise-graph
exercise-kanban
exercise-state-basic
exercise-state-workflow
exercise-state-workflow-automatic
exercise-state-workflow-actions
exercise-access-rights
exercise-access-rules
exercise-wizard
-1
View File
@@ -19,7 +19,6 @@ Reference
reference/javascript
reference/translations
reference/reports
reference/workflows
reference/mixins
reference/guidelines
reference/upgrade_api
-15
View File
@@ -324,18 +324,6 @@ Executes multiple actions one after the other. Actions to execute are defined
via the ``child_ids`` m2m. If sub-actions themselves return actions, the last
one will be returned to the client as the multi's own next action
``trigger``
-----------
Sends a signal to a workflow.
``wkf_transition_id``
:class:`~odoo.fields.Many2one` to a ``workflow.transition`` to trigger
``use_relational_model``
if ``base`` (the default), trigger the signal on behalf of the current
record. If ``relational``, trigger the signal on behalf of a field of the
current record selected through ``wkf_model_id`` and ``wkf_field_id``
``client_action``
-----------------
@@ -368,9 +356,6 @@ server actions:
``Warning``
constructor for the ``Warning`` exception
.. ignored uid (available through ``user``), workflow (available through
workflow methods on models)
.. _reference/actions/report:
Report Actions (``ir.actions.report.xml``)
-10
View File
@@ -152,16 +152,6 @@ Parameters can be provided using ``eval`` (should evaluate to a sequence of
parameters to call the method with) or ``value`` elements (see ``list``
values).
``workflow``
------------
The ``workflow`` tag sends a signal to an existing workflow. The workflow
can be specified via a ``ref`` attribute (the :term:`external id` of
an existing workflow) or a ``value`` tag returning the id of a workflow.
The tag also has two mandatory attributes ``model`` (the model linked to the
workflow) and ``action`` (the name of the signal to send to the workflow).
.. ignored assert
Shortcuts
-1
View File
@@ -182,7 +182,6 @@ To declare a record in XML, the **record** notation (using *<record>*) is recomm
Odoo supports custom tags acting as syntactic sugar:
- menuitem: use it as a shortcut to declare a ``ir.ui.menu``
- workflow: the <workflow> tag sends a signal to an existing workflow.
- template: use it to declare a QWeb View requiring only the ``arch`` section of the view.
- report: use to declare a :ref:`report action <reference/actions/report>`
- act_window: use it if the record notation can't do what you want
-3
View File
@@ -428,9 +428,6 @@ The urls in the actions list can be generated automatically by calling the
self-explanatory
``unfollow``
self-explanatory
``workflow``
trigger a workflow signal; the signal's name should be
provided as the kwarg ``signal``
``method``
call a method on the record; the method's name should be
provided as the kwarg ``method``
-6
View File
@@ -98,11 +98,5 @@ access to the field:
field access groups apply to administrator in fields_get but not in
read/write...
Workflow transition rules
=========================
Workflow transitions can be restricted to a specific group. Users outside the
group can not trigger the transition.
.. _foo: http://google.com
.. _time module: https://docs.python.org/2/library/time.html
-4
View File
@@ -201,10 +201,6 @@ Possible children elements of the list view are:
``type``
type of button, indicates how it clicking it affects Odoo:
``workflow`` (default)
sends a signal to a workflow. The button's ``name`` is the
workflow signal, the row's record is passed as argument to the
signal
``object``
call a method on the list's model. The button's ``name`` is the
method, which is called with the current row's record id and the
-20
View File
@@ -1,20 +0,0 @@
DOTFILES:=$(wildcard *.dot)
SVGFILES:=$(patsubst %.dot,%.svg,$(DOTFILES))
PNGFILES:=$(patsubst %.dot,%.png,$(DOTFILES))
# try to disable implicit rules
.SUFFIXES:
.PHONY: all clean
all: $(SVGFILES) $(PNGFILES)
# must -f to ignore errors when running clean multiple times in a row
clean:
rm -f *.png *.svg
%.svg: %.dot
dot -Tsvg $< > $@
%.png: %.dot
dot -Tpng $< > $@
-10
View File
@@ -1,10 +0,0 @@
digraph join {
// dummy sources as support for edges, make invisible and height 0
a [style=invis height=0 fontsize=0]
b [style=invis height=0 fontsize=0]
c [style=invis height=0 fontsize=0]
a -> Activity
b -> Activity
c -> Activity
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

-36
View File
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
-->
<!-- Title: join Pages: 1 -->
<svg width="206pt" height="93pt"
viewBox="0.00 0.00 206.00 92.73" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 88.7279)">
<title>join</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-88.7279 202,-88.7279 202,4 -4,4"/>
<!-- a -->
<!-- Activity -->
<g id="node4" class="node"><title>Activity</title>
<ellipse fill="none" stroke="black" cx="99" cy="-18" rx="40.0939" ry="18"/>
<text text-anchor="middle" x="99" y="-14.3" font-family="Times,serif" font-size="14.00">Activity</text>
</g>
<!-- a&#45;&gt;Activity -->
<g id="edge1" class="edge"><title>a&#45;&gt;Activity</title>
<path fill="none" stroke="black" d="M33.6445,-71.9778C42.4332,-64.8537 58.4442,-51.875 72.4186,-40.5472"/>
<polygon fill="black" stroke="black" points="74.7768,-43.1411 80.3411,-34.1251 70.3688,-37.7033 74.7768,-43.1411"/>
</g>
<!-- b -->
<!-- b&#45;&gt;Activity -->
<g id="edge2" class="edge"><title>b&#45;&gt;Activity</title>
<path fill="none" stroke="black" d="M99,-71.9778C99,-66.0508 99,-56.0715 99,-46.3619"/>
<polygon fill="black" stroke="black" points="102.5,-46.1364 99,-36.1364 95.5001,-46.1365 102.5,-46.1364"/>
</g>
<!-- c -->
<!-- c&#45;&gt;Activity -->
<g id="edge3" class="edge"><title>c&#45;&gt;Activity</title>
<path fill="none" stroke="black" d="M164.355,-71.9778C155.567,-64.8537 139.556,-51.875 125.581,-40.5472"/>
<polygon fill="black" stroke="black" points="127.631,-37.7033 117.659,-34.1251 123.223,-43.1411 127.631,-37.7033"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

-9
View File
@@ -1,9 +0,0 @@
digraph order {
Draft [style=filled fillcolor="#73fa79"]
Closed [style=filled fillcolor="#98c7df"]
Canceled [style=filled fillcolor="#98c7df"]
Draft -> Confirmed
Confirmed -> Closed
Confirmed -> Canceled
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

-48
View File
@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
-->
<!-- Title: order Pages: 1 -->
<svg width="188pt" height="188pt"
viewBox="0.00 0.00 188.24 188.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 184)">
<title>order</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-184 184.243,-184 184.243,4 -4,4"/>
<!-- Draft -->
<g id="node1" class="node"><title>Draft</title>
<ellipse fill="#73fa79" stroke="black" cx="85.3968" cy="-162" rx="30.5947" ry="18"/>
<text text-anchor="middle" x="85.3968" y="-158.3" font-family="Times,serif" font-size="14.00">Draft</text>
</g>
<!-- Confirmed -->
<g id="node4" class="node"><title>Confirmed</title>
<ellipse fill="none" stroke="black" cx="85.3968" cy="-90" rx="51.9908" ry="18"/>
<text text-anchor="middle" x="85.3968" y="-86.3" font-family="Times,serif" font-size="14.00">Confirmed</text>
</g>
<!-- Draft&#45;&gt;Confirmed -->
<g id="edge1" class="edge"><title>Draft&#45;&gt;Confirmed</title>
<path fill="none" stroke="black" d="M85.3968,-143.697C85.3968,-135.983 85.3968,-126.712 85.3968,-118.112"/>
<polygon fill="black" stroke="black" points="88.8969,-118.104 85.3968,-108.104 81.8969,-118.104 88.8969,-118.104"/>
</g>
<!-- Closed -->
<g id="node2" class="node"><title>Closed</title>
<ellipse fill="#98c7df" stroke="black" cx="36.3968" cy="-18" rx="36.2938" ry="18"/>
<text text-anchor="middle" x="36.3968" y="-14.3" font-family="Times,serif" font-size="14.00">Closed</text>
</g>
<!-- Canceled -->
<g id="node3" class="node"><title>Canceled</title>
<ellipse fill="#98c7df" stroke="black" cx="135.397" cy="-18" rx="44.6926" ry="18"/>
<text text-anchor="middle" x="135.397" y="-14.3" font-family="Times,serif" font-size="14.00">Canceled</text>
</g>
<!-- Confirmed&#45;&gt;Closed -->
<g id="edge2" class="edge"><title>Confirmed&#45;&gt;Closed</title>
<path fill="none" stroke="black" d="M73.7845,-72.411C67.8042,-63.8677 60.3917,-53.2785 53.7479,-43.7874"/>
<polygon fill="black" stroke="black" points="56.5277,-41.6552 47.9257,-35.4699 50.7931,-45.6694 56.5277,-41.6552"/>
</g>
<!-- Confirmed&#45;&gt;Canceled -->
<g id="edge3" class="edge"><title>Confirmed&#45;&gt;Canceled</title>
<path fill="none" stroke="black" d="M97.2461,-72.411C103.348,-63.8677 110.912,-53.2785 117.692,-43.7874"/>
<polygon fill="black" stroke="black" points="120.668,-45.6416 123.633,-35.4699 114.972,-41.573 120.668,-45.6416"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

-11
View File
@@ -1,11 +0,0 @@
digraph order {
Draft [style=filled fillcolor="#73fa79"]
Closed [style=filled fillcolor="#98c7df"]
Canceled [style=filled fillcolor="#98c7df"]
Draft -> Confirmed [label="discount <= 15%"]
Draft -> Validation [label="discount > 15%"]
Validation -> Confirmed [label="Accept"]
Confirmed -> Closed
Confirmed -> Canceled
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

-66
View File
@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
-->
<!-- Title: order Pages: 1 -->
<svg width="260pt" height="291pt"
viewBox="0.00 0.00 259.79 291.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 287)">
<title>order</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-287 255.792,-287 255.792,4 -4,4"/>
<!-- Draft -->
<g id="node1" class="node"><title>Draft</title>
<ellipse fill="#73fa79" stroke="black" cx="85.3968" cy="-265" rx="30.5947" ry="18"/>
<text text-anchor="middle" x="85.3968" y="-261.3" font-family="Times,serif" font-size="14.00">Draft</text>
</g>
<!-- Confirmed -->
<g id="node4" class="node"><title>Confirmed</title>
<ellipse fill="none" stroke="black" cx="85.3968" cy="-91" rx="51.9908" ry="18"/>
<text text-anchor="middle" x="85.3968" y="-87.3" font-family="Times,serif" font-size="14.00">Confirmed</text>
</g>
<!-- Draft&#45;&gt;Confirmed -->
<g id="edge1" class="edge"><title>Draft&#45;&gt;Confirmed</title>
<path fill="none" stroke="black" d="M72.5816,-248.399C62.7132,-235.312 49.8641,-215.656 44.3968,-196 40.1092,-180.585 40.1092,-175.415 44.3968,-160 48.7071,-144.503 57.6054,-129.007 66.0142,-116.73"/>
<polygon fill="black" stroke="black" points="68.9995,-118.572 71.9779,-108.405 63.3089,-114.496 68.9995,-118.572"/>
<text text-anchor="middle" x="94.3968" y="-174.3" font-family="Times,serif" font-size="14.00">discount &lt;= 15%</text>
</g>
<!-- Validation -->
<g id="node5" class="node"><title>Validation</title>
<ellipse fill="none" stroke="black" cx="202.397" cy="-178" rx="49.2915" ry="18"/>
<text text-anchor="middle" x="202.397" y="-174.3" font-family="Times,serif" font-size="14.00">Validation</text>
</g>
<!-- Draft&#45;&gt;Validation -->
<g id="edge2" class="edge"><title>Draft&#45;&gt;Validation</title>
<path fill="none" stroke="black" d="M103.936,-250.531C122.48,-237.059 151.286,-216.131 172.959,-200.386"/>
<polygon fill="black" stroke="black" points="175.139,-203.129 181.172,-194.42 171.024,-197.466 175.139,-203.129"/>
<text text-anchor="middle" x="195.897" y="-217.8" font-family="Times,serif" font-size="14.00">discount &gt; 15%</text>
</g>
<!-- Closed -->
<g id="node2" class="node"><title>Closed</title>
<ellipse fill="#98c7df" stroke="black" cx="36.3968" cy="-18" rx="36.2938" ry="18"/>
<text text-anchor="middle" x="36.3968" y="-14.3" font-family="Times,serif" font-size="14.00">Closed</text>
</g>
<!-- Canceled -->
<g id="node3" class="node"><title>Canceled</title>
<ellipse fill="#98c7df" stroke="black" cx="135.397" cy="-18" rx="44.6926" ry="18"/>
<text text-anchor="middle" x="135.397" y="-14.3" font-family="Times,serif" font-size="14.00">Canceled</text>
</g>
<!-- Confirmed&#45;&gt;Closed -->
<g id="edge4" class="edge"><title>Confirmed&#45;&gt;Closed</title>
<path fill="none" stroke="black" d="M73.7845,-73.174C67.7175,-64.3831 60.1766,-53.4564 53.4596,-43.7236"/>
<polygon fill="black" stroke="black" points="56.1491,-41.4588 47.5884,-35.2165 50.3879,-45.4348 56.1491,-41.4588"/>
</g>
<!-- Confirmed&#45;&gt;Canceled -->
<g id="edge5" class="edge"><title>Confirmed&#45;&gt;Canceled</title>
<path fill="none" stroke="black" d="M97.2461,-73.174C103.348,-64.5087 110.912,-53.7682 117.692,-44.1415"/>
<polygon fill="black" stroke="black" points="120.736,-45.8965 123.633,-35.7052 115.013,-41.8661 120.736,-45.8965"/>
</g>
<!-- Validation&#45;&gt;Confirmed -->
<g id="edge3" class="edge"><title>Validation&#45;&gt;Confirmed</title>
<path fill="none" stroke="black" d="M181.209,-161.607C162.688,-148.152 135.604,-128.475 114.953,-113.472"/>
<polygon fill="black" stroke="black" points="116.97,-110.612 106.822,-107.566 112.856,-116.275 116.97,-110.612"/>
<text text-anchor="middle" x="170.397" y="-130.8" font-family="Times,serif" font-size="14.00">Accept</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

-10
View File
@@ -1,10 +0,0 @@
digraph split {
// dummy destinations as support for edges, make invisible and height 0
a [style=invis height=0 fontsize=0]
b [style=invis height=0 fontsize=0]
c [style=invis height=0 fontsize=0]
Activity -> a
Activity -> b
Activity -> c
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

-36
View File
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
-->
<!-- Title: split Pages: 1 -->
<svg width="206pt" height="93pt"
viewBox="0.00 0.00 206.00 92.73" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 88.7279)">
<title>split</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-88.7279 202,-88.7279 202,4 -4,4"/>
<!-- a -->
<!-- b -->
<!-- c -->
<!-- Activity -->
<g id="node4" class="node"><title>Activity</title>
<ellipse fill="none" stroke="black" cx="99" cy="-66.7279" rx="40.0939" ry="18"/>
<text text-anchor="middle" x="99" y="-63.0279" font-family="Times,serif" font-size="14.00">Activity</text>
</g>
<!-- Activity&#45;&gt;a -->
<g id="edge1" class="edge"><title>Activity&#45;&gt;a</title>
<path fill="none" stroke="black" d="M80.4582,-50.6978C68.3594,-40.8903 52.7894,-28.2691 41.577,-19.1802"/>
<polygon fill="black" stroke="black" points="43.641,-16.3478 33.6687,-12.7696 39.233,-21.7857 43.641,-16.3478"/>
</g>
<!-- Activity&#45;&gt;b -->
<g id="edge2" class="edge"><title>Activity&#45;&gt;b</title>
<path fill="none" stroke="black" d="M99,-48.5325C99,-40.494 99,-30.9869 99,-23.1351"/>
<polygon fill="black" stroke="black" points="102.5,-22.8941 99,-12.8941 95.5001,-22.8942 102.5,-22.8941"/>
</g>
<!-- Activity&#45;&gt;c -->
<g id="edge3" class="edge"><title>Activity&#45;&gt;c</title>
<path fill="none" stroke="black" d="M117.542,-50.6978C129.641,-40.8903 145.211,-28.2691 156.423,-19.1802"/>
<polygon fill="black" stroke="black" points="158.767,-21.7857 164.331,-12.7696 154.359,-16.3478 158.767,-21.7857"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

-328
View File
@@ -1,328 +0,0 @@
.. _reference/workflows:
Workflows
=========
In Odoo, a workflow is a technical artefact to manage a set of "things to
do" associated to the records of a model. The workflow provides a higher-level
way to organize tasks to perform with or 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 Odoo 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.
All in all, Odoo's workflow system provides:
* a description of the evolution of a record (document) over time
* automatic actions based on various and flexible conditions
* management of company roles and validation steps
* management of interactions between objects
* a visual representation of document flows through their lifecycle
For instance, a basic order could have the following flow:
.. sphinx.ext.graphviz would be nice, but it requires ``dot`` on any machine
.. where the doc is compiled... otoh this is a pain in the ass because you
.. need 2 compilation steps (dot -> image and rst -> html) every time
.. image:: workflow/order_0.*
:align: center
Orders start in the *Draft* state, can be *Confirmed* by a user, and then
either shipped (*Closed*) or *Canceled*.
A company using Odoo may want to add discount support to orders, where sales
staff has discretionary discounting powers up to 15%, but manager validation
is required for discounts beyond 15%. The workflow can be altered online to
add the relevant steps without editing Python or XML files:
.. image:: workflow/order_1.*
:align: center
Because Activities can perform arbitrary actions, the *Validation* can
automatically send a validation request to the relevant employee.
.. note:: the order view needs to be modified to add an *Accept Discount*
button for managers
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
.. code-block:: 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"/>
<field name="signal">signal_goto_b</signal>
</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
Odoo 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" specify a signal but not any condition. This
means the workflow instance will immediately transition from "a" to "b" when the
signal ``signal_goto_b`` is recieved and thus processes activity "b"'s action
``print_b()``.
Activities
----------
While the transitions can be seen as the control structures of the workflows,
activities are 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, Odoo 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 Odoo 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``. Odoo 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, Odoo evaluates its transition to reach
the next activity in the flow.
However if an activity has more than one transition, Odoo must decide which
activity or activities to follow.
.. image:: workflow/split.*
:align: center
This choice is controlled by the ``split_mode`` attribute:
``XOR`` (default)
By default, Odoo will use the first transition (in ``sequence`` order)
whose condition is satisfied. All other transitions are ignored.
``OR``
In ``OR`` mode, all transitions with a satisfied condition are traversed
simultanously. Transitions not yet valid will be ignored, even if they
become valid later.
``AND``
In ``AND`` mode, Odoo will wait until *all* transitions are satisfied, and
will traverse all of them (much like the ``OR`` mode).
Both ``OR`` and ``AND`` mode will lead to activities being active in the same
workflow.
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.
.. image:: workflow/join.*
:align: center
The ``join_mode`` attribute controls that behavior:
``XOR`` (default)
Any incoming transition enables the activity and starts its processing.
``AND``
The activity is enabled and processed only once *all* incoming transitions
have been traversed.
Kinds
'''''
An activity's kind defines the type of work an activity can perform.
Dummy (``dummy``, default)
Do nothing at all, or call a server action. Often used as dispatch or
gather "hubs" for transitions.
Function (``function``)
Run some python code, execute a server action.
Stop all (``stopall``)
Completely stops the workflow instance and marks it as completed.
Subflow (``subflow``)
Starts executing an other workflow, once that workflow is completed the
activity is done processing.
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.
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 Odoo, 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 Odoo ``safe_eval`` environment):
- all the model column names, and
- all the browse record's attributes.
.. _reference/workflows/signals:
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:: triggers are not re-installed whenever the transition is re-tried.
-1
View File
@@ -68,7 +68,6 @@ import report
import service
import sql_db
import tools
import workflow
#----------------------------------------------------------
# Model classes, fields, api decorators, and translations
-1
View File
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import ir
import workflow
import module
import res
import report
-1
View File
@@ -38,7 +38,6 @@ The kernel of Odoo, needed for all installation.
'ir/report_ir_model.xml',
'ir/ir_logging_view.xml',
'ir/ir_qweb.xml',
'workflow/workflow_view.xml',
'module/module_view.xml',
'module/module_data.xml',
'module/module_report.xml',
+2 -55
View File
@@ -428,7 +428,6 @@ class IrActionsServer(models.Model):
The available actions are :
- 'Execute Python Code': a block of python code that will be executed
- 'Trigger a Workflow Signal': send a signal to a workflow
- 'Run a Client Action': choose a client action to launch
- 'Create or Copy a new Record': create a new record with new values, or
copy an existing record in your database
@@ -463,7 +462,6 @@ class IrActionsServer(models.Model):
note that the added key length should not be higher than already-existing
ones. """
return [('code', 'Execute Python Code'),
('trigger', 'Trigger a Workflow Signal'),
('client_action', 'Run a Client Action'),
('object_create', 'Create or Copy a new Record'),
('object_write', 'Write on a Record'),
@@ -482,7 +480,6 @@ class IrActionsServer(models.Model):
string='Action To Do', default='code', required=True,
help="Type of server action. The following values are available:\n"
"- 'Execute Python Code': a block of python code that will be executed\n"
"- 'Trigger a Workflow Signal': send a signal to a workflow\n"
"- 'Run a Client Action': choose a client action to launch\n"
"- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n"
"- 'Write on a Record': update the values of a record\n"
@@ -505,17 +502,6 @@ class IrActionsServer(models.Model):
default=DEFAULT_PYTHON_CODE,
help="Write Python code that the action will execute. Some variables are "
"available for use; help about pyhon expression is given in the help tab.")
# Workflow signal
use_relational_model = fields.Selection([('base', 'Use the base model of the action'),
('relational', 'Use a relation field on the base model')],
string='Relational Target Model', default='base', required=True)
wkf_transition_id = fields.Many2one('workflow.transition', string='Signal to Trigger',
help="Select the workflow signal to trigger.")
wkf_model_id = fields.Many2one('ir.model', string='Target Model',
help="The model that will receive the workflow signal. Note that it should have a workflow associated with it.")
wkf_model_name = fields.Char(string='Target Model Name', related='wkf_model_id.model', store=True, readonly=True)
wkf_field_id = fields.Many2one('ir.model.fields', string='Relation Field',
oldname='trigger_obj_id', help="The field on the current object that links to the target object record (must be a many2one, or an integer field with the record ID)")
# Multi
child_ids = fields.Many2many('ir.actions.server', 'rel_server_actions', 'server_id', 'action_id',
string='Child Actions', help='Child server actions that will be executed. Note that the last return returned action value will be used as global return value.')
@@ -548,7 +534,7 @@ class IrActionsServer(models.Model):
"If it is a relationship field you will be able to select "
"a target field at the destination of the relationship.")
sub_object = fields.Many2one('ir.model', string='Sub-model', readonly=True,
help="When a relationship field is selected as first field, "
help="fWhen a relationship field is selected as first field, "
"this field shows the document model the relationship goes to.")
sub_model_object_field = fields.Many2one('ir.model.fields', string='Sub-field',
help="When a relationship field is selected as first field, "
@@ -615,34 +601,12 @@ class IrActionsServer(models.Model):
@api.onchange('model_id')
def _onchange_model_id(self):
""" When changing the action base model, reset workflow and crud config
""" When changing the action base model, reset crud config
to ease value coherence. """
self.use_create = 'new'
self.use_write = 'current'
self.use_relational_model = 'base'
self.wkf_model_id = self.model_id
self.wkf_field_id = False
self.crud_model_id = self.model_id
@api.onchange('use_relational_model', 'wkf_field_id')
def _onchange_wkf_config(self):
""" Update workflow type configuration
- update the workflow model (for base (model_id) /relational (field.relation))
- update wkf_transition_id to False if workflow model changes, to force
the user to choose a new one
"""
if self.use_relational_model == 'relational' and self.wkf_field_id:
field = self.wkf_field_id
self.wkf_model_id = self.env['ir.model'].search([('model', '=', field.relation)])
else:
self.wkf_model_id = self.model_id
@api.onchange('wkf_model_id')
def _onchange_wkf_model_id(self):
""" When changing the workflow model, update its stored name also """
self.wkf_transition_id = False
@api.onchange('use_create', 'use_write', 'ref_object')
def _onchange_crud_config(self):
""" Wrapper on CRUD-type (create or write) on_change """
@@ -796,23 +760,6 @@ class IrActionsServer(models.Model):
if 'action' in eval_context:
return eval_context['action']
@api.model
def run_action_trigger(self, action, eval_context=None):
""" Trigger a workflow signal, depending on the use_relational_model:
- `base`: base_model_pool.signal_workflow(cr, uid, context.get('active_id'), <TRIGGER_NAME>)
- `relational`: find the related model and object, using the relational
field, then target_model_pool.signal_workflow(cr, uid, target_id, <TRIGGER_NAME>)
"""
# weird signature and calling -> no self.env, use action param's
record = action.env[action.model_id.model].browse(self._context['active_id'])
if action.use_relational_model == 'relational':
record = getattr(record, action.wkf_field_id.name)
if not isinstance(record, models.BaseModel):
record = action.env[action.wkf_model_id.model].browse(record)
record.signal_workflow(action.wkf_transition_id.signal)
@api.model
def run_action_multi(self, action, eval_context=None):
res = False
-19
View File
@@ -295,25 +295,6 @@
<field name="code" widget="ace" options="{'mode': 'python'}" placeholder="Enter Python code here. Help about Python expression is available in the help tab of this document."/>
</page>
<page string="Worflow Signal" autofocus="autofocus"
attrs="{'invisible': [('state', '!=', 'trigger')]}">
<p attrs="{'invisible': [('model_id', '!=', False)]}">
Please set the Base Model before setting the action details.
</p>
<group attrs="{'invisible': [('model_id', '=', False)]}">
<field name="use_relational_model" widget="radio"
attrs="{'readonly': [('model_id', '=', False)]}"/>
<field name="wkf_field_id" context="{'key': ''}"
attrs="{'required': [('state', '=', 'trigger'), ('use_relational_model', '=', 'relational')],
'invisible': [('use_relational_model', '=', 'base')]}"
domain="[('model_id', '=', model_id), ('ttype', 'in', ['many2one'])]"/>
<field name="wkf_model_id" invisible="1"/>
<field name="wkf_model_name" invisible="1"/>
<field name="wkf_transition_id" attrs="{'required': [('state', '=', 'trigger')]}"
domain="[('wkf_id.osv', '=', wkf_model_name)]"/>
</group>
</page>
<page string="Client" autofocus="autofocus"
attrs="{'invisible': [('state', '!=', 'client_action')]}">
<group>
-18
View File
@@ -1227,32 +1227,14 @@ class IrModelData(models.Model):
self = self.with_context(**{MODULE_UNINSTALL_FLAG: True})
datas = self.search([('module', 'in', modules_to_remove)])
wkf_todo = []
to_unlink = tools.OrderedSet()
undeletable = self.browse([])
for data in datas.sorted(key='id', reverse=True):
model = data.model
res_id = data.res_id
to_unlink.add((model, res_id))
if model == 'workflow.activity':
# Special treatment for workflow activities: temporarily revert their
# incoming transition and trigger an update to force all workflow items
# to move out before deleting them
self._cr.execute('SELECT res_type, res_id FROM wkf_instance WHERE id IN (SELECT inst_id FROM wkf_workitem WHERE act_id=%s)', (res_id,))
wkf_todo.extend(self._cr.fetchall())
self._cr.execute("UPDATE wkf_transition SET condition='True', group_id=NULL, signal=NULL, act_to=act_from, act_from=%s WHERE act_to=%s", (res_id, res_id))
self.invalidate_cache()
for model, res_id in wkf_todo:
try:
record = self.env[model].browse(res_id)
record.step_workflow()
except Exception:
_logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model, exc_info=True)
def unlink_if_refcount(to_unlink):
undeletable = self.browse()
for model, res_id in to_unlink:
-26
View File
@@ -803,31 +803,6 @@ actual arch.
return arch
@api.model
def _disable_workflow_buttons(self, model, node):
""" Set the buttons in node to readonly if the user can't activate them. """
if model is None or self.env.user.id == SUPERUSER_ID:
# admin user can always activate workflow buttons
return node
# TODO handle the case of more than one workflow for a model or multiple
# transitions with different groups and same signal
user_group_ids = set(self.env.user.groups_id.ids)
buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
for button in buttons:
query = """SELECT DISTINCT t.group_id
FROM wkf
INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
INNER JOIN wkf_transition t ON (t.act_to = a.id)
WHERE wkf.osv = %s
AND t.signal = %s
AND t.group_id is NOT NULL"""
self._cr.execute(query, (model, button.get('name')))
group_ids = set(row[0] for row in self._cr.fetchall() if row[0])
can_click = not group_ids or bool(user_group_ids & group_ids)
button.set('readonly', str(int(not can_click)))
return node
@api.model
def postprocess_and_fields(self, model, node, view_id):
""" Return an architecture and a description of all the fields.
@@ -864,7 +839,6 @@ actual arch.
node = self.add_on_change(model, node)
fields_def = self.postprocess(model, node, view_id, False, fields)
node = self._disable_workflow_buttons(model, node)
if node.tag in ('kanban', 'tree', 'form', 'gantt'):
for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
if (not node.get(action) and
@@ -76,16 +76,6 @@
"access_ir_actions_report_xml_all","ir_actions_report_xml","model_ir_actions_report_xml",,1,0,0,0
"access_ir_actions_report_xml_group_system","ir_actions_report_xml_group_system","model_ir_actions_report_xml","group_system",1,1,1,1
"access_ir_actions_todo_group_system","ir_actions_todo group system","model_ir_actions_todo","group_system",1,1,1,1
"access_workflow_all","workflow_all","model_workflow",,1,0,0,0
"access_workflow_group_system","workflow_group_system","model_workflow","group_system",1,1,1,1
"access_workflow_activity_all","workflow_activity_all","model_workflow_activity",,1,0,0,0
"access_workflow_activity_group_system","workflow_activity_group_system","model_workflow_activity","group_system",1,1,1,1
"access_workflow_instance_all","workflow_instance_all","model_workflow_instance",,1,0,0,0
"access_workflow_transition_all","workflow_transition_all","model_workflow_transition",,1,0,0,0
"access_workflow_transition_group_system","workflow_transition_group_system","model_workflow_transition","group_system",1,1,1,1
"access_workflow_triggers_all","workflow_triggers_all","model_workflow_triggers",,1,0,0,0
"access_workflow_triggers_group_system","workflow_triggers_group_system","model_workflow_triggers","group_system",1,1,1,1
"access_workflow_workitem_all","workflow_workitem_all","model_workflow_workitem",,1,1,1,1
"access_ir_actions_act_window_view_all","ir_actions_act_window_view_all","model_ir_actions_act_window_view",,1,0,0,0
"access_ir_actions_act_window_view_group_system","ir_actions_act_window_view_group_system","model_ir_actions_act_window_view","group_system",1,1,1,1
"access_ir_actions_act_url_all","ir_actions_act_url_all","model_ir_actions_act_url",,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
76 access_ir_actions_report_xml_all ir_actions_report_xml model_ir_actions_report_xml 1 0 0 0
77 access_ir_actions_report_xml_group_system ir_actions_report_xml_group_system model_ir_actions_report_xml group_system 1 1 1 1
78 access_ir_actions_todo_group_system ir_actions_todo group system model_ir_actions_todo group_system 1 1 1 1
access_workflow_all workflow_all model_workflow 1 0 0 0
access_workflow_group_system workflow_group_system model_workflow group_system 1 1 1 1
access_workflow_activity_all workflow_activity_all model_workflow_activity 1 0 0 0
access_workflow_activity_group_system workflow_activity_group_system model_workflow_activity group_system 1 1 1 1
access_workflow_instance_all workflow_instance_all model_workflow_instance 1 0 0 0
access_workflow_transition_all workflow_transition_all model_workflow_transition 1 0 0 0
access_workflow_transition_group_system workflow_transition_group_system model_workflow_transition group_system 1 1 1 1
access_workflow_triggers_all workflow_triggers_all model_workflow_triggers 1 0 0 0
access_workflow_triggers_group_system workflow_triggers_group_system model_workflow_triggers group_system 1 1 1 1
access_workflow_workitem_all workflow_workitem_all model_workflow_workitem 1 1 1 1
79 access_ir_actions_act_window_view_all ir_actions_act_window_view_all model_ir_actions_act_window_view 1 0 0 0
80 access_ir_actions_act_window_view_group_system ir_actions_act_window_view_group_system model_ir_actions_act_window_view group_system 1 1 1 1
81 access_ir_actions_act_url_all ir_actions_act_url_all model_ir_actions_act_url 1 0 0 0
-104
View File
@@ -4,7 +4,6 @@
from odoo.exceptions import ValidationError
from odoo.tools import mute_logger
import odoo.tests.common as common
import odoo.workflow
class TestServerActionsBase(common.TransactionCase):
@@ -91,109 +90,6 @@ class TestServerActions(TestServerActionsBase):
partners = self.test_partner.search([('name', 'ilike', 'TestingPartner_code')])
self.assertEqual(len(partners), 1, 'ir_actions_server: 1 new partner should have been created')
def test_20_trigger(self):
Workflow = self.env['workflow']
WorkflowActivity = self.env['workflow.activity']
WorkflowTransition = self.env['workflow.transition']
# Data: code server action (at this point code-based actions should work)
action2 = self.action.create({
'name': 'TestAction2',
'type': 'ir.actions.server',
'condition': 'True',
'model_id': self.res_partner_model.id,
'state': 'code',
'code': 'record.write({"comment": "MyComment"})',
})
action3 = self.action.create({
'name': 'TestAction3',
'type': 'ir.actions.server',
'condition': 'True',
'model_id': self.res_country_model.id,
'state': 'code',
'code': 'record.write({"code": "ZZ"})',
})
# Data: create workflows
partner_workflow = Workflow.create({
'name': 'TestWorkflow',
'osv': 'res.partner',
'on_create': True,
})
partner_activity1 = WorkflowActivity.create({
'name': 'PartnerStart',
'wkf_id': partner_workflow.id,
'flow_start': True,
})
partner_activity2 = WorkflowActivity.create({
'name': 'PartnerTwo',
'wkf_id': partner_workflow.id,
'kind': 'function',
'action': 'True',
'action_id': action2.id,
})
partner_transition1 = WorkflowTransition.create({
'signal': 'partner_trans',
'act_from': partner_activity1.id,
'act_to': partner_activity2.id,
})
country_workflow = Workflow.create({
'name': 'TestWorkflow',
'osv': 'res.country',
'on_create': True,
})
country_activity1 = WorkflowActivity.create({
'name': 'CountryStart',
'wkf_id': country_workflow.id,
'flow_start': True,
})
country_activity2 = WorkflowActivity.create({
'name': 'CountryTwo',
'wkf_id': country_workflow.id,
'kind': 'function',
'action': 'True',
'action_id': action3.id,
})
country_transition1 = WorkflowTransition.create({
'signal': 'country_trans',
'act_from': country_activity1.id,
'act_to': country_activity2.id,
})
# Data: re-create country and partner to benefit from the workflows
country = self.test_country.create({
'name': 'TestingCountry2',
'code': 'T2',
})
partner = self.test_partner.create({
'name': 'TestingPartner2',
'country_id': country.id,
})
context = dict(self.context, active_id=partner.id)
# Run the action on partner object itself ('base')
self.action.write({
'state': 'trigger',
'use_relational_model': 'base',
'wkf_model_id': self.res_partner_model.id,
'wkf_transition_id': partner_transition1.id,
})
self.action.with_context(context).run()
self.assertEqual(partner.comment, 'MyComment', 'ir_actions_server: incorrect signal trigger')
# Run the action on related country object ('relational')
self.action.write({
'use_relational_model': 'relational',
'wkf_model_id': self.res_country_model.id,
'wkf_field_id': self.res_partner_country_field.id,
'wkf_transition_id': country_transition1.id,
})
self.action.with_context(context).run()
self.assertEqual(country.code, 'ZZ', 'ir_actions_server: incorrect signal trigger')
# Clear workflow cache, otherwise odoo will try to create workflows even if it has been deleted
odoo.workflow.clear_cache(self.cr, self.uid)
def test_30_client(self):
client_action = self.env['ir.actions.client'].create({
'name': 'TestAction2',
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
import workflow
import workflow_report
-185
View File
@@ -1,185 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
import odoo.workflow
class Workflow(models.Model):
_name = "workflow"
_table = "wkf"
_order = "name"
name = fields.Char('Name', required=True)
osv = fields.Char('Resource Object', required=True, index=True)
on_create = fields.Boolean('On Create', default=True, index=True)
activities = fields.One2many('workflow.activity', 'wkf_id', string='Activities')
@api.multi
def copy(self, values):
raise UserError(_("Duplicating workflows is not possible, please create a new workflow"))
@api.multi
def write(self, vals):
odoo.workflow.clear_cache(self._cr, self._uid)
return super(Workflow, self).write(vals)
@api.model
def get_active_workitems(self, res_model, res_id):
cr = self._cr
cr.execute("SELECT * FROM wkf WHERE osv=%s LIMIT 1", (res_model,))
wkfinfo = cr.dictfetchone()
workitems = []
if wkfinfo:
query = """ SELECT id FROM wkf_instance
WHERE res_id=%s AND wkf_id=%s
ORDER BY state LIMIT 1 """
cr.execute(query, (res_id, wkfinfo['id']))
inst_id = cr.fetchone()
query = """ SELECT act_id, COUNT(*) FROM wkf_workitem
WHERE inst_id=%s GROUP BY act_id """
cr.execute(query, (inst_id,))
workitems = dict(cr.fetchall())
return {'wkf': wkfinfo, 'workitems': workitems}
@api.model
def create(self, vals):
odoo.workflow.clear_cache(self._cr, self._uid)
return super(Workflow, self).create(vals)
class WorkflowActivity(models.Model):
_name = "workflow.activity"
_table = "wkf_activity"
_order = "name"
name = fields.Char('Name', required=True)
wkf_id = fields.Many2one('workflow', string='Workflow', ondelete='cascade',
required=True, index=True)
split_mode = fields.Selection([('XOR', 'Xor'), ('OR','Or'), ('AND','And')],
string='Split Mode', size=3, required=True, default='XOR')
join_mode = fields.Selection([('XOR', 'Xor'), ('AND', 'And')],
string='Join Mode', size=3, required=True, default='XOR')
kind = fields.Selection([('dummy', 'Dummy'), ('function', 'Function'),
('subflow', 'Subflow'), ('stopall', 'Stop All')],
string='Kind', required=True, default='dummy')
action = fields.Text('Python Action')
action_id = fields.Many2one('ir.actions.server', string='Server Action', ondelete='set null')
flow_start = fields.Boolean('Flow Start')
flow_stop = fields.Boolean('Flow Stop')
subflow_id = fields.Many2one('workflow', string='Subflow')
signal_send = fields.Char('Signal (subflow.*)')
out_transitions = fields.One2many('workflow.transition', 'act_from', string='Outgoing Transitions')
in_transitions = fields.One2many('workflow.transition', 'act_to', string='Incoming Transitions')
@api.multi
def unlink(self):
if not self._context.get('_force_unlink') and \
self.env['workflow.workitem'].search([('act_id', 'in', self.ids)]):
raise UserError(_('Please make sure no workitems refer to an activity before deleting it!'))
super(WorkflowActivity, self).unlink()
class WorkflowTransition(models.Model):
_name = "workflow.transition"
_table = "wkf_transition"
_rec_name = 'signal'
_order = 'sequence,id'
trigger_model = fields.Char('Trigger Object')
trigger_expr_id = fields.Char('Trigger Expression')
sequence = fields.Integer('Sequence', default=10)
signal = fields.Char('Signal (Button Name)',
help="When the operation of transition comes from a button pressed in the client form, "
"signal tests the name of the pressed button. If signal is NULL, no button is necessary to validate this transition.")
group_id = fields.Many2one('res.groups', string='Group Required',
help="The group that a user must have to be authorized to validate this transition.")
condition = fields.Char('Condition', required=True, default='True',
help="Expression to be satisfied if we want the transition done.")
act_from = fields.Many2one('workflow.activity', string='Source Activity',
ondelete='cascade', required=True, index=True,
help="Source activity. When this activity is over, the condition is tested to determine if we can start the ACT_TO activity.")
act_to = fields.Many2one('workflow.activity', string='Destination Activity',
ondelete='cascade', required=True, index=True,
help="The destination activity.")
wkf_id = fields.Many2one('workflow', related='act_from.wkf_id', string='Workflow')
@api.multi
def name_get(self):
return [
(line.id, line.signal or "%s+%s" % (line.act_from.name, line.act_to.name))
for line in self
]
@api.model
def name_search(self, name, args=None, operator='ilike', limit=100):
if name:
domain = ['|', ('act_from', operator, name), ('act_to', operator, name)] + (args or [])
return self.search(domain, limit=limit).name_get()
return super(WorkflowTransition, self).name_search(name, args, operator, limit=limit)
class WorkflowInstance(models.Model):
_name = "workflow.instance"
_table = "wkf_instance"
_rec_name = 'res_type'
_log_access = False
uid = fields.Integer('User') # FIXME no constraint??
wkf_id = fields.Many2one('workflow', string='Workflow', ondelete='cascade', index=True)
res_id = fields.Integer('Resource ID')
res_type = fields.Char('Resource Object')
state = fields.Char('Status')
transition_ids = fields.Many2many('workflow.transition', 'wkf_witm_trans', 'inst_id', 'trans_id')
@api.model_cr_context
def _auto_init(self):
res = super(WorkflowInstance, self)._auto_init()
cr = self._cr
cr.execute('SELECT indexname FROM pg_indexes WHERE indexname=%s', ['wkf_instance_res_type_res_id_state_index'])
if not cr.fetchone():
cr.execute('CREATE INDEX wkf_instance_res_type_res_id_state_index ON wkf_instance (res_type, res_id, state)')
cr.execute('SELECT indexname FROM pg_indexes WHERE indexname=%s', ['wkf_instance_res_id_wkf_id_index'])
if not cr.fetchone():
cr.execute('CREATE INDEX wkf_instance_res_id_wkf_id_index ON wkf_instance (res_id, wkf_id)')
return res
class WorkflowWorkitem(models.Model):
_name = "workflow.workitem"
_table = "wkf_workitem"
_rec_name = 'state'
_log_access = False
act_id = fields.Many2one('workflow.activity', string='Activity',
ondelete="cascade", required=True, index=True)
wkf_id = fields.Many2one('workflow', related='act_id.wkf_id', string='Workflow')
subflow_id = fields.Many2one('workflow.instance', string='Subflow',
ondelete="set null", index=True)
inst_id = fields.Many2one('workflow.instance', string='Instance',
ondelete="cascade", required=True, index=True)
state = fields.Char('Status', index=True)
class WorkflowTriggers(models.Model):
_name = "workflow.triggers"
_table = "wkf_triggers"
_log_access = False
res_id = fields.Integer('Resource ID', size=128)
model = fields.Char('Object')
instance_id = fields.Many2one('workflow.instance', string='Destination Instance',
ondelete="cascade")
workitem_id = fields.Many2one('workflow.workitem', string='Workitem',
ondelete="cascade", required=True)
@api.model_cr_context
def _auto_init(self):
res = super(WorkflowTriggers, self)._auto_init()
cr = self._cr
cr.execute('SELECT indexname FROM pg_indexes WHERE indexname=%s', ['wkf_triggers_res_id_model_index'])
if not cr.fetchone():
cr.execute('CREATE INDEX wkf_triggers_res_id_model_index ON wkf_triggers (res_id, model)')
return res
@@ -1,204 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from operator import itemgetter
import os
from odoo import report, tools
_logger = logging.getLogger(__name__)
def graph_get(cr, graph, wkf_ids, nested, workitem, witm_trans, processed_subflows):
import pydot
cr.execute('select * from wkf_activity where wkf_id in ('+','.join(['%s']*len(wkf_ids))+')', wkf_ids)
nodes = cr.dictfetchall()
activities = {}
actfrom = {}
actto = {}
for n in nodes:
activities[n['id']] = n
if n['subflow_id'] and nested and n['subflow_id'] not in processed_subflows:
processed_subflows.add(n['subflow_id']) # don't create multiple times the same cluster.
cr.execute('select * from wkf where id=%s', (n['subflow_id'],))
wkfinfo = cr.dictfetchone()
graph2 = pydot.Cluster('subflow'+str(n['subflow_id']), fontsize='12', label = "\"Subflow: %s\\nOSV: %s\"" % ( n['name'], wkfinfo['osv']) )
(s1,s2) = graph_get(cr, graph2, [n['subflow_id']], True, workitem, witm_trans, processed_subflows)
graph.add_subgraph(graph2)
actfrom[n['id']] = s2
actto[n['id']] = s1
else:
args = {}
if n['flow_start'] or n['flow_stop']:
args['style']='filled'
args['color']='lightgrey'
args['label']=n['name']
workitems = ''
if n['id'] in workitem:
workitems = '\\nx ' + str(workitem[n['id']])
args['label'] += workitems
args['color'] = "red"
args['style']='filled'
if n['subflow_id']:
args['shape'] = 'box'
if nested and n['subflow_id'] in processed_subflows:
cr.execute('select * from wkf where id=%s', (n['subflow_id'],))
wkfinfo = cr.dictfetchone()
args['label'] = \
'\"Subflow: %s\\nOSV: %s\\n(already expanded)%s\"' % \
(n['name'], wkfinfo['osv'], workitems)
args['color'] = 'green'
args['style'] ='filled'
graph.add_node(pydot.Node(n['id'], **args))
actfrom[n['id']] = (n['id'],{})
actto[n['id']] = (n['id'],{})
node_ids = tuple(map(itemgetter('id'), nodes))
cr.execute('select * from wkf_transition where act_from IN %s ORDER BY sequence,id', (node_ids,))
transitions = cr.dictfetchall()
for t in transitions:
if not t['act_to'] in activities:
continue
args = {
'label': str(t['condition']).replace(' or ', '\\nor ')
.replace(' and ','\\nand ')
}
if t['signal']:
args['label'] += '\\n'+str(t['signal'])
args['style'] = 'bold'
if activities[t['act_from']]['split_mode']=='AND':
args['arrowtail']='box'
elif str(activities[t['act_from']]['split_mode'])=='OR ':
args['arrowtail']='inv'
if activities[t['act_to']]['join_mode']=='AND':
args['arrowhead']='crow'
if t['id'] in witm_trans:
args['color'] = 'red'
activity_from = actfrom[t['act_from']][1].get(t['signal'], actfrom[t['act_from']][0])
activity_to = actto[t['act_to']][1].get(t['signal'], actto[t['act_to']][0])
graph.add_edge(pydot.Edge( str(activity_from) ,str(activity_to), fontsize='10', **args))
cr.execute('select * from wkf_activity where flow_start=True and wkf_id in ('+','.join(['%s']*len(wkf_ids))+')', wkf_ids)
start = cr.fetchone()[0]
cr.execute("select 'subflow.'||name,id from wkf_activity where flow_stop=True and wkf_id in ("+','.join(['%s']*len(wkf_ids))+')', wkf_ids)
stop = cr.fetchall()
if stop:
stop = (stop[0][1], dict(stop))
else:
stop = ("stop",{})
return (start, {}), stop
def graph_instance_get(cr, graph, inst_id, nested=False):
cr.execute('select wkf_id from wkf_instance where id=%s', (inst_id,))
inst = cr.fetchall()
def workitem_get(instance):
cr.execute('select act_id,count(*) from wkf_workitem where inst_id=%s group by act_id', (instance,))
workitems = dict(cr.fetchall())
cr.execute('select subflow_id from wkf_workitem where inst_id=%s', (instance,))
for (subflow_id,) in cr.fetchall():
workitems.update(workitem_get(subflow_id))
return workitems
def witm_get(instance):
cr.execute("select trans_id from wkf_witm_trans where inst_id=%s", (instance,))
return set(t[0] for t in cr.fetchall())
processed_subflows = set()
graph_get(cr, graph, [x[0] for x in inst], nested, workitem_get(inst_id), witm_get(inst_id), processed_subflows)
#
# TODO: pas clean: concurrent !!!
#
class report_graph_instance(object):
def __init__(self, cr, uid, ids, data):
try:
import pydot
except Exception,e:
_logger.warning(
'Import Error for pydot, you will not be able to render workflows.\n'
'Consider Installing PyDot or dependencies: http://dkbza.org/pydot.html.')
raise e
self.done = False
try:
cr.execute('select * from wkf where osv=%s limit 1',
(data['model'],))
wkfinfo = cr.dictfetchone()
if not wkfinfo:
ps_string = '''%PS-Adobe-3.0
/inch {72 mul} def
/Times-Roman findfont 50 scalefont setfont
1.5 inch 15 inch moveto
(No workflow defined) show
showpage'''
else:
cr.execute('select i.id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where res_id=%s and osv=%s',(data['id'],data['model']))
inst_ids = cr.fetchall()
if not inst_ids:
ps_string = '''%PS-Adobe-3.0
/inch {72 mul} def
/Times-Roman findfont 50 scalefont setfont
1.5 inch 15 inch moveto
(No workflow instance defined) show
showpage'''
else:
graph = pydot.Dot(graph_name=data['model'].replace('.','_'),
fontsize='16',
label="""\\\n\\nWorkflow: %s\\n OSV: %s""" % (wkfinfo['name'],wkfinfo['osv']),
size='7.3, 10.1', center='1', ratio='auto', rotate='0', rankdir='TB',
)
for inst_id in inst_ids:
inst_id = inst_id[0]
graph_instance_get(cr, graph, inst_id, data.get('nested', False))
ps_string = graph.create(prog='dot', format='ps')
except Exception:
_logger.exception('Exception in call:')
# string is in PS, like the success message would have been
ps_string = '''%PS-Adobe-3.0
/inch {72 mul} def
/Times-Roman findfont 50 scalefont setfont
1.5 inch 15 inch moveto
(No workflow available) show
showpage'''
if os.name == "nt":
prog = 'ps2pdf.bat'
else:
prog = 'ps2pdf'
args = (prog, '-', '-')
input, output = tools.exec_command_pipe(*args)
input.write(ps_string)
input.close()
self.result = output.read()
output.close()
self.done = True
def is_done(self):
return self.done
def get(self):
if self.done:
return self.result
else:
return None
class report_graph(report.interface.report_int):
def __init__(self, name, table):
report.interface.report_int.__init__(self, name)
self.table = table
def result(self):
if self.obj.is_done():
return True, self.obj.get(), 'pdf'
else:
return False, False, False
def create(self, cr, uid, ids, data, context=None):
self.obj = report_graph_instance(cr, uid, ids, data)
return self.obj.get(), 'pdf'
report_graph('report.workflow.instance.graph', 'ir.workflow')
-370
View File
@@ -1,370 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<menuitem id="menu_workflow_root" name="Workflows" parent="base.menu_custom" groups="base.group_no_one"/>
<!--
================================
Workflows
================================
-->
<record id="view_workflow_form" model="ir.ui.view">
<field name="name">workflow.form</field>
<field name="model">workflow</field>
<field name="arch" type="xml">
<form string="Workflow">
<group col="4">
<field name="name"/>
<field name="osv"/>
<field name="on_create"/>
</group>
<field name="activities"/>
</form>
</field>
</record>
<record id="view_workflow_search" model="ir.ui.view">
<field name="name">workflow.search</field>
<field name="model">workflow</field>
<field name="arch" type="xml">
<search string="Workflow">
<field name="name" filter_domain="['|', ('name','ilike',self), ('osv','ilike',self)]" string="Workflow"/>
</search>
</field>
</record>
<record id="view_workflow_diagram" model="ir.ui.view">
<field name="name">workflow.diagram</field>
<field name="model">workflow</field>
<field name="arch" type="xml">
<diagram string="Workflow Editor">
<node object="workflow.activity" shape="rectangle:subflow_id!=False" bgcolor="gray:flow_start==True;grey:flow_stop==True">
<field name="name"/>
<field name="kind"/>
<field name="action"/>
<field name="flow_start" invisible="1"/>
<field name="flow_stop" invisible="1"/>
<field name="subflow_id" invisible="1"/>
</node>
<arrow object="workflow.transition" source="act_from" destination="act_to" label="['signal','condition']">
<field name="act_from"/>
<field name="act_to"/>
<field name="signal"/>
</arrow>
<label string="When customizing a workflow, be sure you do not modify an existing node or arrow, but rather add new nodes or arrows. If you absolutly need to modify a node or arrow, you can only change fields that are empty or set to the default value. If you don't do that, your customization will be overwrited at the next update or upgrade to a future version of Odoo."/>
</diagram>
</field>
</record>
<record id="view_workflow_tree" model="ir.ui.view">
<field name="name">workflow.tree</field>
<field name="model">workflow</field>
<field name="arch" type="xml">
<tree string="Workflow">
<field name="name"/>
<field name="osv"/>
<field name="on_create"/>
</tree>
</field>
</record>
<record id="action_workflow_form" model="ir.actions.act_window">
<field name="name">Workflows</field>
<field name="res_model">workflow</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_workflow_tree"/>
<field name="view_mode">tree,form,diagram</field>
</record>
<menuitem action="action_workflow_form" id="menu_workflow" parent="base.menu_workflow_root"/>
<!--
================================
Activities
================================
-->
<record id="view_workflow_activity_form" model="ir.ui.view">
<field name="name">workflow.activity.form</field>
<field name="model">workflow.activity</field>
<field name="arch" type="xml">
<form string="Activity">
<sheet>
<group>
<group>
<field name="name"/>
<field name="wkf_id"/>
<field name="kind"/>
</group>
<group>
<field name="flow_start"/>
<field name="flow_stop"/>
</group>
</group>
<notebook>
<page string="Properties">
<group>
<group string="Subflow">
<field name="subflow_id" attrs="{'readonly':[('kind','&lt;&gt;','subflow')]}"/>
<field name="signal_send"/>
</group>
<group string="Conditions">
<field name="split_mode"/>
<field name="join_mode"/>
</group>
<group string="Actions">
<field name="action_id"/>
<field name="action" attrs="{'readonly':[('kind','=','dummy')]}"/>
</group>
</group>
</page>
<page string="Transitions">
<field name="in_transitions">
<tree string="Incoming Transitions">
<field name="sequence" widget="handle" />
<field name="act_from"/>
<field name="signal"/>
<field name="condition"/>
</tree>
</field>
<field name="out_transitions">
<tree string="Outgoing Transitions">
<field name="sequence" widget="handle" />
<field name="act_to"/>
<field name="signal"/>
<field name="condition"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_workflow_activity_tree" model="ir.ui.view">
<field name="name">workflow.activity.tree</field>
<field name="model">workflow.activity</field>
<field name="arch" type="xml">
<tree string="Activity">
<field name="name"/>
<field name="wkf_id"/>
<field name="kind"/>
<field name="flow_start"/>
<field name="flow_stop"/>
</tree>
</field>
</record>
<record id="view_workflow_activity_search" model="ir.ui.view">
<field name="name">workflow.activity.search</field>
<field name="model">workflow.activity</field>
<field name="arch" type="xml">
<search string="Workflow Activity">
<field name="name" string="Workflow Activity"/>
<filter string="Flow Start" domain="[('flow_start', '=',True)]"/>
<filter string="Flow Stop" domain="[('flow_stop', '=',True)]"/>
<field name="wkf_id"/>
<field name="action_id"/>
<field name="kind"/>
<group expand="0" string="Group By">
<filter string="Workflow" domain="[]" context="{'group_by':'wkf_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_workflow_activity_form" model="ir.actions.act_window">
<field name="name">Activities</field>
<field name="res_model">workflow.activity</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_workflow_activity_tree"/>
<field name="search_view_id" ref="view_workflow_activity_search"/>
</record>
<menuitem action="action_workflow_activity_form" id="menu_workflow_activity" parent="base.menu_workflow_root"/>
<!--
================================
Transitions
================================
-->
<record id="view_workflow_transition_form" model="ir.ui.view">
<field name="name">workflow.transition.form</field>
<field name="model">workflow.transition</field>
<field name="arch" type="xml">
<form string="Transition">
<sheet>
<group>
<group>
<field name="sequence" />
<field name="act_from"/>
<field name="act_to"/>
<field name="signal"/>
<field name="condition"/>
</group>
<group>
<field name="group_id"/>
<field name="trigger_model"/>
<field name="trigger_expr_id"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_workflow_transition_tree" model="ir.ui.view">
<field name="name">workflow.transition.tree</field>
<field name="model">workflow.transition</field>
<field name="arch" type="xml">
<tree string="Transition">
<field name="sequence" widget="handle" />
<field name="act_from"/>
<field name="act_to"/>
<field name="signal"/>
<field name="condition"/>
</tree>
</field>
</record>
<record id="view_workflow_transition_search" model="ir.ui.view">
<field name="name">workflow.transition.search</field>
<field name="model">workflow.transition</field>
<field name="arch" type="xml">
<search string="Transition">
<field name="signal" filter_domain="['|', ('signal','ilike',self), ('condition','ilike',self)]" string="Workflow Transition"/>
<field name="act_from"/>
<field name="act_to"/>
</search>
</field>
</record>
<record id="action_workflow_transition_form" model="ir.actions.act_window">
<field name="name">Transitions</field>
<field name="res_model">workflow.transition</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_workflow_transition_tree"/>
<field name="search_view_id" ref="view_workflow_transition_search"/>
</record>
<menuitem action="action_workflow_transition_form" id="menu_workflow_transition" parent="base.menu_workflow_root"/>
<!--
================================
Instances
================================
-->
<record id="view_workflow_instance_form" model="ir.ui.view">
<field name="name">workflow.instance.form</field>
<field name="model">workflow.instance</field>
<field name="arch" type="xml">
<form string="Workflow Instances">
<sheet>
<group col="4">
<field name="wkf_id"/>
<field name="res_id"/>
<field name="res_type"/>
<field name="state"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_workflow_instance_tree" model="ir.ui.view">
<field name="name">workflow.instance.tree</field>
<field name="model">workflow.instance</field>
<field name="arch" type="xml">
<tree string="Workflow Instances">
<field name="wkf_id"/>
<field name="res_id"/>
<field name="res_type"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="view_workflow_instance_search" model="ir.ui.view">
<field name="name">workflow.instance.search</field>
<field name="model">workflow.instance</field>
<field name="arch" type="xml">
<search string="Workflow Instances">
<field name="res_type" string="Resource Object"/>
<filter string="Active" domain="[('state','=','active')]" name="active"/>
<field name="wkf_id"/>
<field name="res_id"/>
<field name="state"/>
</search>
</field>
</record>
<record id="action_workflow_instance_form" model="ir.actions.act_window">
<field name="name">Instances</field>
<field name="res_model">workflow.instance</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_workflow_instance_tree"/>
<field name="context">{'search_default_active':1}</field>
<field name="search_view_id" ref="view_workflow_instance_search"/>
</record>
<menuitem action="action_workflow_instance_form" id="menu_workflow_instance" parent="base.menu_workflow_root"/>
<!--
================================
Workitems
================================
-->
<record id="view_workflow_workitem_form" model="ir.ui.view">
<field name="name">workflow.workitem.form</field>
<field name="model">workflow.workitem</field>
<field name="arch" type="xml">
<form string="Workflow Workitems">
<sheet>
<group col="4">
<field name="wkf_id"/>
<field name="act_id"/>
<field name="subflow_id"/>
<field name="inst_id"/>
<field name="state"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_workflow_workitem_tree" model="ir.ui.view">
<field name="name">workflow.workitem.tree</field>
<field name="model">workflow.workitem</field>
<field name="arch" type="xml">
<tree string="Workflow Workitems">
<field name="wkf_id"/>
<field name="act_id"/>
<field name="subflow_id"/>
<field name="inst_id"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="view_workflow_workitem_search" model="ir.ui.view">
<field name="name">workflow.workitem.search</field>
<field name="model">workflow.workitem</field>
<field name="arch" type="xml">
<search string="Workflow Workitems">
<field name="state" string="Status"/>
<filter string="Active" name="active" domain="[('state','=','active')]"/>
<field name="wkf_id"/>
<field name="act_id"/>
<field name="subflow_id"/>
<field name="inst_id"/>
</search>
</field>
</record>
<record id="action_workflow_workitem_form" model="ir.actions.act_window">
<field name="name">Workitems</field>
<field name="res_model">workflow.workitem</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_workflow_workitem_tree"/>
<field name="context">{'search_default_active':1}</field>
<field name="search_view_id" ref="view_workflow_workitem_search"/>
</record>
<menuitem action="action_workflow_workitem_form" id="menu_workflow_workitem" parent="base.menu_workflow_root"/>
</data>
</odoo>
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
-12
View File
@@ -1,12 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'test-workflow',
'version': '0.1',
'category': 'Tests',
'description': """A module to play with workflows.""",
'depends': ['base'],
'data': ['data.xml', 'ir.model.access.csv'],
'installable': True,
'auto_install': False,
}
-517
View File
@@ -1,517 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<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">
<group col="4">
<button name="a-b" string="a-b" type="workflow" icon="fa-check text-success" colspan="1"/>
<label string="a-b"/>
<button name="trigger" string="trigger" type="object" icon="fa-refresh text-primary" colspan="1"/>
<label string="trigger"/>
</group>
</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 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>
</odoo>
@@ -1,15 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_test_workflow_model,access_test_workflow_model,model_test_workflow_model,,1,1,1,1
access_test_workflow_trigger,access_test_workflow_trigger,model_test_workflow_trigger,,1,1,1,1
access_test_workflow_model_a,access_test_workflow_model_a,model_test_workflow_model_a,,1,1,1,1
access_test_workflow_model_b,access_test_workflow_model_b,model_test_workflow_model_b,,1,1,1,1
access_test_workflow_model_c,access_test_workflow_model_c,model_test_workflow_model_c,,1,1,1,1
access_test_workflow_model_d,access_test_workflow_model_d,model_test_workflow_model_d,,1,1,1,1
access_test_workflow_model_e,access_test_workflow_model_e,model_test_workflow_model_e,,1,1,1,1
access_test_workflow_model_f,access_test_workflow_model_f,model_test_workflow_model_f,,1,1,1,1
access_test_workflow_model_g,access_test_workflow_model_g,model_test_workflow_model_g,,1,1,1,1
access_test_workflow_model_h,access_test_workflow_model_h,model_test_workflow_model_h,,1,1,1,1
access_test_workflow_model_i,access_test_workflow_model_i,model_test_workflow_model_i,,1,1,1,1
access_test_workflow_model_j,access_test_workflow_model_j,model_test_workflow_model_j,,1,1,1,1
access_test_workflow_model_k,access_test_workflow_model_k,model_test_workflow_model_k,,1,1,1,1
access_test_workflow_model_l,access_test_workflow_model_l,model_test_workflow_model_l,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_test_workflow_model access_test_workflow_model model_test_workflow_model 1 1 1 1
3 access_test_workflow_trigger access_test_workflow_trigger model_test_workflow_trigger 1 1 1 1
4 access_test_workflow_model_a access_test_workflow_model_a model_test_workflow_model_a 1 1 1 1
5 access_test_workflow_model_b access_test_workflow_model_b model_test_workflow_model_b 1 1 1 1
6 access_test_workflow_model_c access_test_workflow_model_c model_test_workflow_model_c 1 1 1 1
7 access_test_workflow_model_d access_test_workflow_model_d model_test_workflow_model_d 1 1 1 1
8 access_test_workflow_model_e access_test_workflow_model_e model_test_workflow_model_e 1 1 1 1
9 access_test_workflow_model_f access_test_workflow_model_f model_test_workflow_model_f 1 1 1 1
10 access_test_workflow_model_g access_test_workflow_model_g model_test_workflow_model_g 1 1 1 1
11 access_test_workflow_model_h access_test_workflow_model_h model_test_workflow_model_h 1 1 1 1
12 access_test_workflow_model_i access_test_workflow_model_i model_test_workflow_model_i 1 1 1 1
13 access_test_workflow_model_j access_test_workflow_model_j model_test_workflow_model_j 1 1 1 1
14 access_test_workflow_model_k access_test_workflow_model_k model_test_workflow_model_k 1 1 1 1
15 access_test_workflow_model_l access_test_workflow_model_l model_test_workflow_model_l 1 1 1 1
-81
View File
@@ -1,81 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import models, fields, api
from odoo.workflow import trg_trigger
_logger = logging.getLogger(__name__)
class m(models.Model):
""" A model for which we will define a workflow (see data.xml). """
_name = 'test.workflow.model'
@api.multi
def _print(self, s):
_logger.info('Running activity `%s` for record %s', s, self.ids)
return True
@api.multi
def print_a(self):
return self._print('a')
@api.multi
def print_b(self):
return self._print('b')
@api.multi
def print_c(self):
return self._print('c')
@api.multi
def condition(self):
record = self.env['test.workflow.trigger'].browse(1)
return bool(record.value)
@api.model
def trigger(self):
return trg_trigger(self._uid, 'test.workflow.trigger', 1, self._cr)
class n(models.Model):
""" A model used for the trigger feature. """
_name = 'test.workflow.trigger'
value = fields.Boolean(default=False)
class a(models.Model):
_name = 'test.workflow.model.a'
value = fields.Integer(default=0)
class b(models.Model):
_name = 'test.workflow.model.b'
_inherit = 'test.workflow.model.a'
class c(models.Model):
_name = 'test.workflow.model.c'
_inherit = 'test.workflow.model.a'
class d(models.Model):
_name = 'test.workflow.model.d'
_inherit = 'test.workflow.model.a'
class e(models.Model):
_name = 'test.workflow.model.e'
_inherit = 'test.workflow.model.a'
for name in 'bcdefghijkl':
#
# Do not use type() to create the class here, but use the class construct.
# This is because the __module__ of the new class would be the one of the
# metaclass that provides method __new__!
#
class NewModel(models.Model):
_name = 'test.workflow.model.%s' % name
_inherit = 'test.workflow.model.a'
@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_workflow
@@ -1,121 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common
class test_workflows(common.TransactionCase):
def check_activities(self, record, names):
""" Check that the record has workitems in the given activity names.
"""
Instance = self.env['workflow.instance']
Workitem = self.env['workflow.workitem']
# Given the workflow instance associated to the record ...
instance = Instance.search([('res_type', '=', record._name), ('res_id', '=', record.id)])
self.assertTrue(instance, 'A workflow instance is expected.')
# ... get all its workitems ...
workitems = Workitem.search([('inst_id', '=', instance.id)])
self.assertTrue(workitems, 'The workflow instance should have workitems.')
# ... and check the activity the are in against the provided names.
self.assertEqual(
sorted([item.act_id.name for item in workitems]),
sorted(names))
def check_value(self, record, value):
""" Check that the record has the given value.
"""
self.assertEqual(record.value, value)
def test_workflow(self):
model = self.env['test.workflow.model']
trigger = self.env['test.workflow.trigger']
record = model.create({})
self.check_activities(record, ['a'])
# a -> b is just a signal.
record.signal_workflow('a-b')
self.check_activities(record, ['b'])
# b -> c is a trigger (which is False),
# so we remain in the b activity.
record.trigger()
self.check_activities(record, ['b'])
# b -> c is a trigger (which is set to True).
# so we go in c when the trigger is called.
trigger.browse(1).write({'value': True})
record.trigger()
self.check_activities(record, ['c'])
self.assertEqual(True, True)
def test_workflow_a(self):
record = self.env['test.workflow.model.a'].create({})
self.check_activities(record, ['a'])
self.check_value(record, 0)
def test_workflow_b(self):
record = self.env['test.workflow.model.b'].create({})
self.check_activities(record, ['a'])
self.check_value(record, 1)
def test_workflow_c(self):
record = self.env['test.workflow.model.c'].create({})
self.check_activities(record, ['a'])
self.check_value(record, 0)
def test_workflow_d(self):
record = self.env['test.workflow.model.d'].create({})
self.check_activities(record, ['a'])
self.check_value(record, 1)
def test_workflow_e(self):
record = self.env['test.workflow.model.e'].create({})
self.check_activities(record, ['b'])
self.check_value(record, 2)
def test_workflow_f(self):
record = self.env['test.workflow.model.f'].create({})
self.check_activities(record, ['a'])
self.check_value(record, 1)
record.signal_workflow('a-b')
self.check_activities(record, ['b'])
self.check_value(record, 2)
def test_workflow_g(self):
record = self.env['test.workflow.model.g'].create({})
self.check_activities(record, ['a'])
self.check_value(record, 1)
def test_workflow_h(self):
record = self.env['test.workflow.model.h'].create({})
self.check_activities(record, ['b', 'c'])
self.check_value(record, 2)
def test_workflow_i(self):
record = self.env['test.workflow.model.i'].create({})
self.check_activities(record, ['b'])
self.check_value(record, 2)
def test_workflow_j(self):
record = self.env['test.workflow.model.j'].create({})
self.check_activities(record, ['a'])
self.check_value(record, 1)
def test_workflow_k(self):
record = self.env['test.workflow.model.k'].create({})
# Non-determinisitic: can be b or c
# self.check_activities(record, ['b'])
# self.check_activities(record, ['c'])
self.check_value(record, 2)
def test_workflow_l(self):
record = self.env['test.workflow.model.l'].create({})
self.check_activities(record, ['c', 'c', 'd'])
self.check_value(record, 3)
-1
View File
@@ -20,7 +20,6 @@ code removed, and thus these variables made unnecessary.
open_openerp_namespace = False
# If True, openerp.netsvc.LocalService() can be used to lookup reports or to
# access openerp.workflow.
# Introduced around 2013.03.
# Among the related code:
# - The openerp.netsvc.LocalService() function.
-49
View File
@@ -3302,51 +3302,6 @@ class BaseModel(object):
returned_ids = [x[0] for x in self._cr.fetchall()]
self.browse(sub_ids)._check_record_rules_result_count(returned_ids, operation)
@api.multi
def create_workflow(self):
""" Create a workflow instance for the given records. """
from odoo import workflow
for res_id in self.ids:
workflow.trg_create(self._uid, self._name, res_id, self._cr)
return True
@api.multi
def delete_workflow(self):
""" Delete the workflow instances bound to the given records. """
from odoo import workflow
for res_id in self.ids:
workflow.trg_delete(self._uid, self._name, res_id, self._cr)
self.invalidate_cache()
return True
@api.multi
def step_workflow(self):
""" Reevaluate the workflow instances of the given records. """
from odoo import workflow
for res_id in self.ids:
workflow.trg_write(self._uid, self._name, res_id, self._cr)
return True
@api.multi
def signal_workflow(self, signal):
""" Send the workflow signal, and return a dict mapping ids to workflow results. """
from odoo import workflow
result = {}
for res_id in self.ids:
result[res_id] = workflow.trg_validate(self._uid, self._name, res_id, signal, self._cr)
return result
@api.model
def redirect_workflow(self, old_new_ids):
""" Rebind the workflow instance bound to the given 'old' record IDs to
the given 'new' IDs. (``old_new_ids`` is a list of pairs ``(old, new)``.
"""
from odoo import workflow
for old_id, new_id in old_new_ids:
workflow.trg_redirect(self._uid, self._name, old_id, new_id, self._cr)
self.invalidate_cache()
return True
@api.multi
def unlink(self):
""" unlink()
@@ -3376,8 +3331,6 @@ class BaseModel(object):
# Delete the records' properties.
self.env['ir.property'].search([('res_id', 'in', refs)]).unlink()
self.delete_workflow()
self.check_access_rule('unlink')
cr = self._cr
@@ -3750,7 +3703,6 @@ class BaseModel(object):
if self.env.recompute and self._context.get('recompute', True):
self.recompute()
self.step_workflow()
return True
#
@@ -3964,7 +3916,6 @@ class BaseModel(object):
self.recompute()
self.check_access_rule('create')
self.create_workflow()
return id_new
# TODO: ameliorer avec NULL
+2 -9
View File
@@ -28,18 +28,12 @@ def log(logger, level, prefix, msg, depth=None):
def LocalService(name):
"""
The odoo.netsvc.LocalService() function is deprecated. It still works
in two cases: workflows and reports. For workflows, instead of using
LocalService('workflow'), odoo.workflow should be used (better yet,
methods on odoo.osv.orm.Model should be used). For reports,
odoo.report.render_report() should be used (methods on the Model should
be provided too in the future).
in one case: reports. For reports, odoo.report.render_report() should
be used (methods on the Model should be provided too in the future).
"""
assert odoo.conf.deprecation.allow_local_service
_logger.warning("LocalService() is deprecated since march 2013 (it was called with '%s')." % name)
if name == 'workflow':
return odoo.workflow
if name.startswith('report.'):
report = odoo.report.interface.report_int._reports.get(name)
if report:
@@ -195,7 +189,6 @@ def init_logger():
_logger.debug('logger level set: "%s"', logconfig_item)
DEFAULT_LOG_CONFIGURATION = [
'odoo.workflow.workitem:WARNING',
'odoo.http.rpc.request:INFO',
'odoo.http.rpc.response:INFO',
'odoo.addons.web.http:INFO',
+1 -1
View File
@@ -21,7 +21,7 @@ description = 'Odoo Server'
long_desc = '''Odoo is a complete ERP and CRM. The main features are accounting (analytic
and financial), stock management, sales and purchases management, tasks
automation, marketing campaigns, help desk, POS, etc. Technical features include
a distributed server, flexible workflows, an object database, a dynamic GUI,
a distributed server, an object database, a dynamic GUI,
customizable reports, and XML-RPC interfaces.
'''
classifiers = """Development Status :: 5 - Production/Stable
+1 -11
View File
@@ -30,7 +30,7 @@ def dispatch(method, params):
params = params[3:]
if method == 'obj_list':
raise NameError("obj_list has been discontinued via RPC as of 6.0, please query ir.model directly!")
if method not in ['execute', 'execute_kw', 'exec_workflow']:
if method not in ['execute', 'execute_kw']:
raise NameError("Method not available %s" % method)
security.check(db,uid,passwd)
registry = odoo.registry(db).check_signaling()
@@ -183,13 +183,3 @@ def execute(db, uid, obj, method, *args, **kw):
if res is None:
_logger.info('The method %s of the object %s can not return `None` !', method, obj)
return res
def exec_workflow_cr(cr, uid, obj, signal, *args):
res_id = args[0]
return execute_cr(cr, uid, obj, 'signal_workflow', [res_id], signal)[res_id]
@check
def exec_workflow(db, uid, obj, signal, *args):
with odoo.registry(db).cursor() as cr:
return exec_workflow_cr(cr, uid, obj, signal, *args)
-20
View File
@@ -475,25 +475,6 @@ form: module.record_id""" % (xml_id,)
elif res['key'] == 'action':
ir_values.set_action(res['name'], action_slot=res['key2'], model=model, action=res['value'], res_id=res_id)
def _tag_workflow(self, rec, data_node=None, mode=None):
if self.isnoupdate(data_node) and self.mode != 'init':
return
model = rec.get('model').encode('ascii')
w_ref = rec.get('ref')
if w_ref:
id = self.id_get(w_ref)
else:
number_children = len(rec)
assert number_children > 0,\
'You must define a child node if you dont give a ref'
assert number_children == 1,\
'Only one child node is accepted (%d given)' % number_children
id = _eval_xml(self, rec[0], self.env)
uid = self.get_uid(data_node, rec)
record = self.env(user=uid)[model].browse(id)
record.signal_workflow(rec.get('action').encode('ascii'))
def _tag_menuitem(self, rec, data_node=None, mode=None):
rec_id = rec.get("id",'').encode('ascii')
self._test_xml_id(rec_id)
@@ -824,7 +805,6 @@ form: module.record_id""" % (xml_id,)
'function': self._tag_function,
'menuitem': self._tag_menuitem,
'template': self._tag_template,
'workflow': self._tag_workflow,
'report': self._tag_report,
'ir_set': self._tag_ir_set, # deprecated:: 9.0
'act_window': self._tag_act_window,
-1
View File
@@ -726,7 +726,6 @@ def in_modules(object_name, modules):
module_dict = {
'ir': 'base',
'res': 'base',
'workflow': 'base',
}
module = object_name.split('.')[0]
module = module_dict.get(module, module)
-38
View File
@@ -64,9 +64,6 @@ def is_function(node):
def is_report(node):
return isinstance(node, yaml_tag.Report)
def is_workflow(node):
return isinstance(node, yaml_tag.Workflow)
def is_act_window(node):
return isinstance(node, yaml_tag.ActWindow)
@@ -618,36 +615,6 @@ class YamlInterpreter(object):
else:
self.assertion_report.record_success()
def process_workflow(self, node):
workflow, values = node.items()[0]
if self.isnoupdate(workflow) and self.mode != 'init':
return
if workflow.ref:
id = self.get_id(workflow.ref)
else:
if not values:
raise YamlImportException('You must define a child node if you do not give a ref.')
if not len(values) == 1:
raise YamlImportException('Only one child node is accepted (%d given).' % len(values))
value = values[0]
if not 'model' in value and (not 'eval' in value or not 'search' in value):
raise YamlImportException('You must provide a "model" and an "eval" or "search" to evaluate.')
value_model = self.env[value['model']]
local_context = {'obj': value_model.browse}
local_context.update(self.id_map)
id = safe_eval(value['eval'], self.eval_context, local_context)
if workflow.uid is not None:
uid = workflow.uid
else:
uid = self.uid
self.cr.execute('select distinct signal, sequence, id from wkf_transition ORDER BY sequence,id')
signals=[x['signal'] for x in self.cr.dictfetchall()]
if workflow.action not in signals:
raise YamlImportException('Incorrect action %s. No such action defined' % workflow.action)
record = self.env(user=uid)[workflow.model].browse(id)
record.signal_workflow(workflow.action)
def _eval_params(self, model, params):
args = []
for i, param in enumerate(params):
@@ -907,11 +874,6 @@ class YamlInterpreter(object):
self.process_act_window(node)
elif is_report(node):
self.process_report(node)
elif is_workflow(node):
if isinstance(node, types.DictionaryType):
self.process_workflow(node)
else:
self.process_workflow({node: []})
elif is_function(node):
if isinstance(node, types.DictionaryType):
self.process_function(node)
-14
View File
@@ -47,15 +47,6 @@ class Menuitem(YamlTag):
self.name = name
super(Menuitem, self).__init__(**kwargs)
class Workflow(YamlTag):
def __init__(self, model, action, ref=None, **kwargs):
self.model = model
self.action = action
self.ref = ref
super(Workflow, self).__init__(**kwargs)
def __str__(self):
return '!workflow {model: %s, action: %s, ref: %s}' % (str(self.model,), str(self.action,), str(self.ref,))
class ActWindow(YamlTag):
def __init__(self, **kwargs):
super(ActWindow, self).__init__(**kwargs)
@@ -122,10 +113,6 @@ def menuitem_constructor(loader, node):
kwargs = loader.construct_mapping(node)
return Menuitem(**kwargs)
def workflow_constructor(loader, node):
kwargs = loader.construct_mapping(node)
return Workflow(**kwargs)
def act_window_constructor(loader, node):
kwargs = loader.construct_mapping(node)
return ActWindow(**kwargs)
@@ -173,7 +160,6 @@ def add_constructors():
yaml.add_constructor(u"!record", record_constructor)
yaml.add_constructor(u"!python", python_constructor)
yaml.add_constructor(u"!menuitem", menuitem_constructor)
yaml.add_constructor(u"!workflow", workflow_constructor)
yaml.add_constructor(u"!act_window", act_window_constructor)
yaml.add_constructor(u"!function", function_constructor)
yaml.add_constructor(u"!report", report_constructor)
-83
View File
@@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.workflow.service import WorkflowService
# The new API is in odoo.workflow.workflow_service
# OLD API of the Workflow
def clear_cache(cr, uid):
WorkflowService.clear_cache(cr.dbname)
def trg_write(uid, res_type, res_id, cr):
"""
Reevaluates the specified workflow instance. Thus if any condition for
a transition have been changed in the backend, then running ``trg_write``
will move the workflow over that transition.
:param res_type: the model name
:param res_id: the model instance id the workflow belongs to
:param cr: a database cursor
"""
return WorkflowService.new(cr, uid, res_type, res_id).write()
def trg_trigger(uid, res_type, res_id, cr):
"""
Activate a trigger.
If a workflow instance is waiting for a trigger from another model, then this
trigger can be activated if its conditions are met.
:param res_type: the model name
:param res_id: the model instance id the workflow belongs to
:param cr: a database cursor
"""
return WorkflowService.new(cr, uid, res_type, res_id).trigger()
def trg_delete(uid, res_type, res_id, cr):
"""
Delete a workflow instance
:param res_type: the model name
:param res_id: the model instance id the workflow belongs to
:param cr: a database cursor
"""
return WorkflowService.new(cr, uid, res_type, res_id).delete()
def trg_create(uid, res_type, res_id, cr):
"""
Create a new workflow instance
:param res_type: the model name
:param res_id: the model instance id to own the created worfklow instance
:param cr: a database cursor
"""
return WorkflowService.new(cr, uid, res_type, res_id).create()
def trg_validate(uid, res_type, res_id, signal, cr):
"""
Fire a signal on a given workflow instance
:param res_type: the model name
:param res_id: the model instance id the workflow belongs to
:signal: the signal name to be fired
:param cr: a database cursor
"""
assert isinstance(signal, basestring)
return WorkflowService.new(cr, uid, res_type, res_id).validate(signal)
def trg_redirect(uid, res_type, res_id, new_rid, cr):
"""
Re-bind a workflow instance to another instance of the same model.
Make all workitems which are waiting for a (subflow) workflow instance
for the old resource point to the (first active) workflow instance for
the new resource.
:param res_type: the model name
:param res_id: the model instance id the workflow belongs to
:param new_rid: the model instance id to own the worfklow instance
:param cr: a database cursor
"""
assert isinstance(new_rid, (long, int))
return WorkflowService.new(cr, uid, res_type, res_id).redirect(new_rid)
-21
View File
@@ -1,21 +0,0 @@
import odoo.sql_db
class Session(object):
def __init__(self, cr, uid):
assert isinstance(cr, odoo.sql_db.Cursor)
assert isinstance(uid, (int, long))
self.cr = cr
self.uid = uid
class Record(object):
def __init__(self, model, record_id):
assert isinstance(model, basestring)
assert isinstance(record_id, (int, long))
self.model = model
self.id = record_id
class WorkflowActivity(object):
KIND_FUNCTION = 'function'
KIND_DUMMY = 'dummy'
KIND_STOPALL = 'stopall'
KIND_SUBFLOW = 'subflow'
-117
View File
@@ -1,117 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import workitem
from odoo.workflow.helpers import Session
from odoo.workflow.helpers import Record
from odoo.workflow.workitem import WorkflowItem
class WorkflowInstance(object):
def __init__(self, session, record, values):
assert isinstance(session, Session)
assert isinstance(record, Record)
self.session = session
self.record = record
if not values:
values = {}
assert isinstance(values, dict)
self.instance = values
@classmethod
def create(cls, session, record, workflow_id):
assert isinstance(session, Session)
assert isinstance(record, Record)
assert isinstance(workflow_id, (int, long))
cr = session.cr
cr.execute('insert into wkf_instance (res_type,res_id,uid,wkf_id,state) values (%s,%s,%s,%s,%s) RETURNING id', (record.model, record.id, session.uid, workflow_id, 'active'))
instance_id = cr.fetchone()[0]
cr.execute('select * from wkf_activity where flow_start=True and wkf_id=%s', (workflow_id,))
stack = []
activities = cr.dictfetchall()
for activity in activities:
WorkflowItem.create(session, record, activity, instance_id, stack)
cr.execute('SELECT * FROM wkf_instance WHERE id = %s', (instance_id,))
values = cr.dictfetchone()
wi = WorkflowInstance(session, record, values)
wi.update()
return wi
def delete(self):
self.session.cr.execute('delete from wkf_instance where res_id=%s and res_type=%s', (self.record.id, self.record.model))
def validate(self, signal, force_running=False):
assert isinstance(signal, basestring)
assert isinstance(force_running, bool)
cr = self.session.cr
cr.execute("select * from wkf_workitem where inst_id=%s", (self.instance['id'],))
stack = []
for i, work_item_values in enumerate(cr.dictfetchall()):
if i > 0:
# test if previous workitem has already processed this one
cr.execute("select id from wkf_workitem where id=%s", (work_item_values['id'],))
if not cr.fetchone():
continue
wi = WorkflowItem(self.session, self.record, work_item_values)
wi.process(signal=signal, force_running=force_running, stack=stack)
# An action is returned
self._update_end()
return stack and stack[0] or False
def update(self):
cr = self.session.cr
cr.execute("select * from wkf_workitem where inst_id=%s", (self.instance['id'],))
for work_item_values in cr.dictfetchall():
stack = []
WorkflowItem(self.session, self.record, work_item_values).process(stack=stack)
return self._update_end()
def _update_end(self):
cr = self.session.cr
instance_id = self.instance['id']
cr.execute('select wkf_id from wkf_instance where id=%s', (instance_id,))
wkf_id = cr.fetchone()[0]
cr.execute('select state,flow_stop from wkf_workitem w left join wkf_activity a on (a.id=w.act_id) where w.inst_id=%s', (instance_id,))
ok=True
for r in cr.fetchall():
if (r[0]<>'complete') or not r[1]:
ok=False
break
if ok:
cr.execute('select distinct a.name from wkf_activity a left join wkf_workitem w on (a.id=w.act_id) where w.inst_id=%s', (instance_id,))
act_names = cr.fetchall()
cr.execute("update wkf_instance set state='complete' where id=%s", (instance_id,))
cr.execute("update wkf_workitem set state='complete' where subflow_id=%s", (instance_id,))
cr.execute("select i.id,w.osv,i.res_id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where i.id IN (select inst_id from wkf_workitem where subflow_id=%s)", (instance_id,))
for cur_instance_id, cur_model_name, cur_record_id in cr.fetchall():
cur_record = Record(cur_model_name, cur_record_id)
for act_name in act_names:
WorkflowInstance(self.session, cur_record, {'id':cur_instance_id}).validate('subflow.%s' % act_name[0])
return ok
def create(session, record, workflow_id):
return WorkflowInstance(session, record).create(workflow_id)
def delete(session, record):
return WorkflowInstance(session, record).delete()
def validate(session, record, instance_id, signal, force_running=False):
return WorkflowInstance(session, record).validate(instance_id, signal, force_running)
def update(session, record, instance_id):
return WorkflowInstance(session, record).update(instance_id)
def _update_end(session, record, instance_id):
return WorkflowInstance(session, record)._update_end(instance_id)
-96
View File
@@ -1,96 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from helpers import Session
from helpers import Record
from odoo.workflow.instance import WorkflowInstance
# import instance
class WorkflowService(object):
CACHE = {}
@classmethod
def clear_cache(cls, dbname):
cls.CACHE[dbname] = {}
@classmethod
def new(cls, cr, uid, model_name, record_id):
return cls(Session(cr, uid), Record(model_name, record_id))
def __init__(self, session, record):
assert isinstance(session, Session)
assert isinstance(record, Record)
self.session = session
self.record = record
self.cr = self.session.cr
def write(self):
self.cr.execute('select id from wkf_instance where res_id=%s and res_type=%s and state=%s',
(self.record.id or None, self.record.model or None, 'active')
)
for (instance_id,) in self.cr.fetchall():
WorkflowInstance(self.session, self.record, {'id': instance_id}).update()
def trigger(self):
self.cr.execute('select instance_id from wkf_triggers where res_id=%s and model=%s', (self.record.id, self.record.model))
res = self.cr.fetchall()
for (instance_id,) in res:
self.cr.execute('select %s,res_type,res_id from wkf_instance where id=%s', (self.session.uid, instance_id,))
current_uid, current_model_name, current_record_id = self.cr.fetchone()
current_session = Session(self.session.cr, current_uid)
current_record = Record(current_model_name, current_record_id)
WorkflowInstance(current_session, current_record, {'id': instance_id}).update()
def delete(self):
WorkflowInstance(self.session, self.record, {}).delete()
def create(self):
WorkflowService.CACHE.setdefault(self.cr.dbname, {})
wkf_ids = WorkflowService.CACHE[self.cr.dbname].get(self.record.model, None)
if not wkf_ids:
self.cr.execute('select id from wkf where osv=%s and on_create=True', (self.record.model,))
wkf_ids = self.cr.fetchall()
WorkflowService.CACHE[self.cr.dbname][self.record.model] = wkf_ids
for (wkf_id, ) in wkf_ids:
WorkflowInstance.create(self.session, self.record, wkf_id)
def validate(self, signal):
result = False
# ids of all active workflow instances for a corresponding resource (id, model_nam)
self.cr.execute('select id from wkf_instance where res_id=%s and res_type=%s and state=%s', (self.record.id, self.record.model, 'active'))
# TODO: Refactor the workflow instance object
for (instance_id,) in self.cr.fetchall():
wi = WorkflowInstance(self.session, self.record, {'id': instance_id})
res2 = wi.validate(signal)
result = result or res2
return result
def redirect(self, new_rid):
# get ids of wkf instances for the old resource (res_id)
# CHECKME: shouldn't we get only active instances?
self.cr.execute('select id, wkf_id from wkf_instance where res_id=%s and res_type=%s', (self.record.id, self.record.model))
for old_inst_id, workflow_id in self.cr.fetchall():
# first active instance for new resource (new_rid), using same wkf
self.cr.execute(
'SELECT id '\
'FROM wkf_instance '\
'WHERE res_id=%s AND res_type=%s AND wkf_id=%s AND state=%s',
(new_rid, self.record.model, workflow_id, 'active'))
new_id = self.cr.fetchone()
if new_id:
# select all workitems which "wait" for the old instance
self.cr.execute('select id from wkf_workitem where subflow_id=%s', (old_inst_id,))
for (item_id,) in self.cr.fetchall():
# redirect all those workitems to the wkf instance of the new resource
self.cr.execute('update wkf_workitem set subflow_id=%s where id=%s', (new_id[0], item_id))
-314
View File
@@ -1,314 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
#
# TODO:
# cr.execute('delete from wkf_triggers where model=%s and res_id=%s', (res_type,res_id))
#
import logging
import instance
from odoo.workflow.helpers import Session
from odoo.workflow.helpers import Record
from odoo.workflow.helpers import WorkflowActivity
logger = logging.getLogger(__name__)
import odoo
from odoo.tools.safe_eval import safe_eval
class Environment(dict):
"""
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, session, record):
self.cr = session.cr
self.uid = session.uid
self.model = record.model
self.id = record.id
self.ids = [record.id]
def __getitem__(self, key):
env = odoo.api.Environment(self.cr, self.uid, {})
records = env[self.model].browse(self.ids)
if hasattr(records, key):
return getattr(records, key)
else:
return super(Environment, self).__getitem__(key)
class WorkflowItem(object):
def __init__(self, session, record, work_item_values):
assert isinstance(session, Session)
assert isinstance(record, Record)
self.session = session
self.record = record
if not work_item_values:
work_item_values = {}
assert isinstance(work_item_values, dict)
self.workitem = work_item_values
@classmethod
def create(cls, session, record, activity, instance_id, stack):
assert isinstance(session, Session)
assert isinstance(record, Record)
assert isinstance(activity, dict)
assert isinstance(instance_id, (long, int))
assert isinstance(stack, list)
cr = session.cr
cr.execute("select nextval('wkf_workitem_id_seq')")
id_new = cr.fetchone()[0]
cr.execute("insert into wkf_workitem (id,act_id,inst_id,state) values (%s,%s,%s,'active')", (id_new, activity['id'], instance_id))
cr.execute('select * from wkf_workitem where id=%s',(id_new,))
work_item_values = cr.dictfetchone()
logger.info('Created workflow item in activity %s',
activity['id'],
extra={'ident': (session.uid, record.model, record.id)})
workflow_item = WorkflowItem(session, record, work_item_values)
workflow_item.process(stack=stack)
@classmethod
def create_all(cls, session, record, activities, instance_id, stack):
assert isinstance(activities, list)
for activity in activities:
cls.create(session, record, activity, instance_id, stack)
def process(self, signal=None, force_running=False, stack=None):
assert isinstance(force_running, bool)
assert stack is not None
cr = self.session.cr
cr.execute('select * from wkf_activity where id=%s', (self.workitem['act_id'],))
activity = cr.dictfetchone()
triggers = False
if self.workitem['state'] == 'active':
triggers = True
if not self._execute(activity, stack):
return False
if force_running or self.workitem['state'] == 'complete':
ok = self._split_test(activity['split_mode'], signal, stack)
triggers = triggers and not ok
if triggers:
cr.execute('select * from wkf_transition where act_from=%s ORDER BY sequence,id', (self.workitem['act_id'],))
for trans in cr.dictfetchall():
if trans['trigger_model']:
ids = self.wkf_expr_eval_expr(trans['trigger_expr_id'])
for res_id in ids:
cr.execute('select nextval(\'wkf_triggers_id_seq\')')
id =cr.fetchone()[0]
cr.execute('insert into wkf_triggers (model,res_id,instance_id,workitem_id,id) values (%s,%s,%s,%s,%s)', (trans['trigger_model'],res_id, self.workitem['inst_id'], self.workitem['id'], id))
return True
def _execute(self, activity, stack):
"""Send a signal to parenrt workflow (signal: subflow.signal_name)"""
result = True
cr = self.session.cr
signal_todo = []
if (self.workitem['state']=='active') and activity['signal_send']:
# signal_send']:
cr.execute("select i.id,w.osv,i.res_id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where i.id IN (select inst_id from wkf_workitem where subflow_id=%s)", (self.workitem['inst_id'],))
for instance_id, model_name, record_id in cr.fetchall():
record = Record(model_name, record_id)
signal_todo.append((instance_id, record, activity['signal_send']))
if activity['kind'] == WorkflowActivity.KIND_DUMMY:
if self.workitem['state']=='active':
self._state_set(activity, 'complete')
if activity['action_id']:
res2 = self.wkf_expr_execute_action(activity)
if res2:
stack.append(res2)
result=res2
elif activity['kind'] == WorkflowActivity.KIND_FUNCTION:
if self.workitem['state']=='active':
self._state_set(activity, 'running')
returned_action = self.wkf_expr_execute(activity)
if type(returned_action) in (dict,):
stack.append(returned_action)
if activity['action_id']:
res2 = self.wkf_expr_execute_action(activity)
# A client action has been returned
if res2:
stack.append(res2)
result=res2
self._state_set(activity, 'complete')
elif activity['kind'] == WorkflowActivity.KIND_STOPALL:
if self.workitem['state']=='active':
self._state_set(activity, 'running')
cr.execute('delete from wkf_workitem where inst_id=%s and id<>%s', (self.workitem['inst_id'], self.workitem['id']))
if activity['action']:
self.wkf_expr_execute(activity)
self._state_set(activity, 'complete')
elif activity['kind'] == WorkflowActivity.KIND_SUBFLOW:
if self.workitem['state']=='active':
self._state_set(activity, 'running')
if activity.get('action', False):
id_new = self.wkf_expr_execute(activity)
if not id_new:
cr.execute('delete from wkf_workitem where id=%s', (self.workitem['id'],))
return False
assert type(id_new)==type(1) or type(id_new)==type(1L), 'Wrong return value: '+str(id_new)+' '+str(type(id_new))
cr.execute('select id from wkf_instance where res_id=%s and wkf_id=%s', (id_new, activity['subflow_id']))
id_new = cr.fetchone()[0]
else:
inst = instance.WorkflowInstance(self.session, self.record)
id_new = inst.create(activity['subflow_id'])
cr.execute('update wkf_workitem set subflow_id=%s where id=%s', (id_new, self.workitem['id']))
self.workitem['subflow_id'] = id_new
if self.workitem['state']=='running':
cr.execute("select state from wkf_instance where id=%s", (self.workitem['subflow_id'],))
state = cr.fetchone()[0]
if state=='complete':
self._state_set(activity, 'complete')
for instance_id, record, signal_send in signal_todo:
wi = instance.WorkflowInstance(self.session, record, {'id': instance_id})
wi.validate(signal_send, force_running=True)
return result
def _state_set(self, activity, state):
self.session.cr.execute('update wkf_workitem set state=%s where id=%s', (state, self.workitem['id']))
self.workitem['state'] = state
logger.info('Changed state of work item %s to "%s" in activity %s',
self.workitem['id'], state, activity['id'],
extra={'ident': (self.session.uid, self.record.model, self.record.id)})
def _split_test(self, split_mode, signal, stack):
cr = self.session.cr
cr.execute('select * from wkf_transition where act_from=%s ORDER BY sequence,id', (self.workitem['act_id'],))
test = False
transitions = []
alltrans = cr.dictfetchall()
if split_mode in ('XOR', 'OR'):
for transition in alltrans:
if self.wkf_expr_check(transition,signal):
test = True
transitions.append((transition['id'], self.workitem['inst_id']))
if split_mode=='XOR':
break
else:
test = True
for transition in alltrans:
if not self.wkf_expr_check(transition, signal):
test = False
break
cr.execute('select count(*) from wkf_witm_trans where trans_id=%s and inst_id=%s', (transition['id'], self.workitem['inst_id']))
if not cr.fetchone()[0]:
transitions.append((transition['id'], self.workitem['inst_id']))
if test and transitions:
cr.executemany('insert into wkf_witm_trans (trans_id,inst_id) values (%s,%s)', transitions)
cr.execute('delete from wkf_workitem where id=%s', (self.workitem['id'],))
for t in transitions:
self._join_test(t[0], t[1], stack)
return True
return False
def _join_test(self, trans_id, inst_id, stack):
cr = self.session.cr
cr.execute('select * from wkf_activity where id=(select act_to from wkf_transition where id=%s)', (trans_id,))
activity = cr.dictfetchone()
if activity['join_mode']=='XOR':
WorkflowItem.create(self.session, self.record, activity, inst_id, stack=stack)
cr.execute('delete from wkf_witm_trans where inst_id=%s and trans_id=%s', (inst_id,trans_id))
else:
cr.execute('select id from wkf_transition where act_to=%s ORDER BY sequence,id', (activity['id'],))
trans_ids = cr.fetchall()
ok = True
for (id,) in trans_ids:
cr.execute('select count(*) from wkf_witm_trans where trans_id=%s and inst_id=%s', (id,inst_id))
res = cr.fetchone()[0]
if not res:
ok = False
break
if ok:
for (id,) in trans_ids:
cr.execute('delete from wkf_witm_trans where trans_id=%s and inst_id=%s', (id,inst_id))
WorkflowItem.create(self.session, self.record, activity, inst_id, stack=stack)
def wkf_expr_eval_expr(self, lines):
"""
Evaluate each line of ``lines`` with the ``Environment`` environment, returning
the value of the last line.
"""
assert lines, 'You used a NULL action in a workflow, use dummy node instead.'
result = False
for line in lines.split('\n'):
line = line.strip()
if not line:
continue
if line == 'True':
result = True
elif line == 'False':
result = False
else:
env = Environment(self.session, self.record)
result = safe_eval(line, env, nocopy=True)
return result
def wkf_expr_execute_action(self, activity):
"""
Evaluate the ir.actions.server action specified in the activity.
"""
context = {
'active_model': self.record.model,
'active_id': self.record.id,
'active_ids': [self.record.id]
}
env = odoo.api.Environment(self.session.cr, self.session.uid, context)
action = env['ir.actions.server'].browse(activity['action_id'])
result = action.run()
return result
def wkf_expr_execute(self, activity):
"""
Evaluate the action specified in the activity.
"""
return self.wkf_expr_eval_expr(activity['action'])
def wkf_expr_check(self, 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
if self.session.uid != odoo.SUPERUSER_ID and transition['group_id']:
env = odoo.api.Environment(self.session.cr, self.session.uid, {})
user_groups = env.user.groups_id
if transition['group_id'] not in user_groups.ids:
return False
return self.wkf_expr_eval_expr(transition['condition'])