[REM] workflow: Remove workflow engine, documentation and tests
@@ -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 {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'] == '':
|
||||
|
||||
@@ -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
|
||||
---------------
|
||||
|
||||
|
||||
@@ -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
|
||||
========
|
||||
|
||||
|
||||
@@ -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 > 50</field>
|
||||
+ </record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,6 @@ Reference
|
||||
reference/javascript
|
||||
reference/translations
|
||||
reference/reports
|
||||
reference/workflows
|
||||
reference/mixins
|
||||
reference/guidelines
|
||||
reference/upgrade_api
|
||||
|
||||
@@ -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``)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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``
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 $< > $@
|
||||
@@ -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
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -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->Activity -->
|
||||
<g id="edge1" class="edge"><title>a->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->Activity -->
|
||||
<g id="edge2" class="edge"><title>b->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->Activity -->
|
||||
<g id="edge3" class="edge"><title>c->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 |
@@ -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
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB |
@@ -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->Confirmed -->
|
||||
<g id="edge1" class="edge"><title>Draft->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->Closed -->
|
||||
<g id="edge2" class="edge"><title>Confirmed->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->Canceled -->
|
||||
<g id="edge3" class="edge"><title>Confirmed->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 |
@@ -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
|
||||
}
|
||||
|
Before Width: | Height: | Size: 20 KiB |
@@ -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->Confirmed -->
|
||||
<g id="edge1" class="edge"><title>Draft->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 <= 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->Validation -->
|
||||
<g id="edge2" class="edge"><title>Draft->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 > 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->Closed -->
|
||||
<g id="edge4" class="edge"><title>Confirmed->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->Canceled -->
|
||||
<g id="edge5" class="edge"><title>Confirmed->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->Confirmed -->
|
||||
<g id="edge3" class="edge"><title>Validation->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 |
@@ -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
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -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->a -->
|
||||
<g id="edge1" class="edge"><title>Activity->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->b -->
|
||||
<g id="edge2" class="edge"><title>Activity->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->c -->
|
||||
<g id="edge3" class="edge"><title>Activity->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 |
@@ -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.
|
||||
@@ -68,7 +68,6 @@ import report
|
||||
import service
|
||||
import sql_db
|
||||
import tools
|
||||
import workflow
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Model classes, fields, api decorators, and translations
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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',
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import workflow
|
||||
import workflow_report
|
||||
@@ -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')
|
||||
@@ -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','<>','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>
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,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)
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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'])
|
||||