Files
tryton/trytond/workflow/workflow.py
T
2011-11-02 15:49:38 +01:00

690 lines
25 KiB
Python

#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from trytond.model import ModelView, ModelSQL, fields
from trytond.report import Report
from trytond.backend import TableHandler
from trytond.pyson import Eval, Bool
from trytond.transaction import Transaction
from trytond.pool import Pool
class Workflow(ModelSQL, ModelView):
"Workflow"
_name = "workflow"
_table = "wkf"
_description = __doc__
name = fields.Char('Name', required=True, translate=True)
model = fields.Char('Resource Model', required=True, select=1)
on_create = fields.Boolean('On Create', select=2)
activities = fields.One2Many('workflow.activity', 'workflow',
'Activities')
def __init__(self):
super(Workflow, self).__init__()
self._error_messages.update({
'no_workflow_defined': 'No workflow defined!',
})
def init(self, module_name):
super(Workflow, self).init(module_name)
cursor = Transaction().cursor
table = TableHandler(cursor, self, module_name)
# Migration from 1.2 rename osv into model
if table.column_exist('osv'):
cursor.execute('UPDATE "' + self._table + '" ' \
'SET model = osv')
table.drop_column('osv', exception=True)
def default_on_create(self):
return 1
Workflow()
class WorkflowActivity(ModelSQL, ModelView):
"Workflow activity"
_name = "workflow.activity"
_table = "wkf_activity"
_description = __doc__
name = fields.Char('Name', required=True, translate=True)
workflow = fields.Many2One('workflow', 'Workflow', required=True,
select=1, ondelete='CASCADE')
split_mode = fields.Selection([
('XOR', 'Xor'),
('OR', 'Or'),
('AND', 'And'),
], 'Split Mode', required=True)
join_mode = fields.Selection([
('XOR', 'Xor'),
('AND', 'And'),
], 'Join Mode', required=True)
method = fields.Char('Method')
flow_start = fields.Boolean('Flow Start')
flow_stop = fields.Boolean('Flow Stop')
stop_other = fields.Boolean('Stop Other')
subflow = fields.Many2One('workflow', 'Subflow')
signal_send = fields.Char('Signal (subflow.*)')
out_transitions = fields.One2Many('workflow.transition', 'act_from',
'Outgoing transitions')
in_transitions = fields.One2Many('workflow.transition', 'act_to',
'Incoming transitions')
def init(self, module_name):
super(WorkflowActivity, self).init(module_name)
cursor = Transaction().cursor
table = TableHandler(cursor, self, module_name)
# Migration from 2.0
if table.column_exist('kind'):
cursor.execute('UPDATE "%s" SET stop_other = %%s '
"WHERE kind='stopall'" % self._table, (True,))
table.drop_column('kind', exception=True)
def default_kind(self):
return 'dummy'
def default_join_mode(self):
return 'XOR'
def default_split_mode(self):
return 'XOR'
def default_flow_start(self):
return False
def default_flow_stop(self):
return False
def default_stop_other(self):
return False
WorkflowActivity()
class WorkflowTransition(ModelSQL, ModelView):
"Workflow transition"
_table = "wkf_transition"
_name = "workflow.transition"
_rec_name = 'signal'
_description = __doc__
trigger_model = fields.Char('Trigger Type')
trigger_ids = fields.Char('Trigger Expr ID', states={
'required': Bool(Eval('trigger_model')),
}, depends=['trigger_model'])
signal = fields.Char('Signal (button Name)')
group = fields.Many2One('res.group', 'Group Required')
condition = fields.Char('Condition', required=True)
act_from = fields.Many2One('workflow.activity', 'Source Activity',
required=True, select=1, ondelete='CASCADE')
act_to = fields.Many2One('workflow.activity', 'Destination Activity',
required=True, select=1, ondelete='CASCADE')
instances = fields.Many2Many('workflow.transition-workflow.instance',
'trans_id', 'inst_id', 'Instances')
def init(self, module_name):
cursor = Transaction().cursor
super(WorkflowTransition, self).init(module_name)
# Migration from 2.0: condition is a method name
cursor.execute('UPDATE "%s" SET "condition" = %%s '
'WHERE "condition" = %%s' % self._table, ('', 'True'))
def default_condition(self):
return ''
WorkflowTransition()
class WorkflowInstance(ModelSQL, ModelView):
"Workflow instance"
_table = "wkf_instance"
_name = "workflow.instance"
_rec_name = 'res_type'
_description = __doc__
workflow = fields.Many2One('workflow', 'Workflow', ondelete="RESTRICT",
select=1)
uid = fields.Integer('User ID')
res_id = fields.Integer('Resource ID', required=True, select=1)
res_type = fields.Char('Resource Model', required=True, select=1)
state = fields.Char('State', required=True, select=1)
overflows = fields.One2Many('workflow.workitem', 'subflow',
'Overflow')
transitions = fields.Many2Many('workflow.transition-workflow.instance',
'inst_id', 'trans_id', 'Transitions')
workitems = fields.One2Many('workflow.workitem', 'instance', 'Workitems')
def __init__(self):
super(WorkflowInstance, self).__init__()
self._error_messages.update({
'no_instance_defined': 'No workflow instance defined!',
})
for i in ('create', 'write', 'delete', 'copy'):
del self._rpc[i]
#TODO add a constraint to have only one active instance by resource
def init(self, module_name):
super(WorkflowInstance, self).init(module_name)
table = TableHandler(Transaction().cursor, self, module_name)
table.index_action(['res_id', 'res_type', 'state'], 'add')
table.index_action(['res_id', 'workflow'], 'add')
def fields_get(self, fields_names=None):
res = super(WorkflowInstance, self).fields_get(
fields_names=fields_names)
for field in res:
res[field]['readonly'] = True
return res
def create(self, values):
pool = Pool()
activity_obj = pool.get('workflow.activity')
workitem_obj = pool.get('workflow.workitem')
instance_id = super(WorkflowInstance, self).create(values)
if 'workflow' in values:
with Transaction().set_user(0):
activity_ids = activity_obj.search([
('flow_start', '=', True),
('workflow', '=', values['workflow']),
])
for activity_id in activity_ids:
workitem_obj.create({
'activity': activity_id,
'instance': instance_id,
'state': 'active',
})
instance = self.browse(instance_id)
self.update(instance)
return instance_id
def update(self, instance):
'''
'''
pool = Pool()
workitem_obj = pool.get('workflow.workitem')
for workitem in instance.workitems:
workitem_obj.process(workitem)
instance = self.browse(instance.id)
return self._update_end(instance)
def validate(self, instance, signal, force_running=False):
'''
'''
pool = Pool()
workitem_obj = pool.get('workflow.workitem')
for workitem in instance.workitems:
workitem_obj.process(workitem, signal=signal,
force_running=force_running)
instance = self.browse(instance.id)
return self._update_end(instance)
def _update_end(self, instance):
pool = Pool()
workitem_obj = pool.get('workflow.workitem')
res = True
for workitem in instance.workitems:
if (workitem.state != 'complete') \
or not workitem.activity.flow_stop:
res = False
break
if res:
act_names = set()
for workitem in instance.workitems:
act_names.add(workitem.activity.name)
with Transaction().set_user(0):
self.write(instance.id, {
'state': 'complete',
})
workitem_ids = workitem_obj.search([
('subflow', '=', instance.id),
])
workitem_obj.write(workitem_ids, {
'state': 'complete',
})
for workitem in workitem_obj.browse(workitem_ids):
for act_name in act_names:
self.validate(workitem.instance,
signal='subflow.' + act_name)
return res
WorkflowInstance()
class WorkflowTransitionInstance(ModelSQL):
"Workflow Transition - Instance"
_name = 'workflow.transition-workflow.instance'
_table = 'wkf_witm_trans'
_description = __doc__
trans_id = fields.Many2One('workflow.transition', 'Transition',
ondelete='CASCADE', select=1, required=True)
inst_id = fields.Many2One('workflow.instance', 'Instance',
ondelete='CASCADE', select=1, required=True)
def __init__(self):
super(WorkflowTransitionInstance, self).__init__()
for i in ('create', 'write', 'delete', 'copy'):
del self._rpc[i]
def fields_get(self, fields_names=None):
res = super(WorkflowTransitionInstance, self).fields_get(
fields_names=fields_names)
for field in res:
res[field]['readonly'] = True
return res
WorkflowTransitionInstance()
class WorkflowWorkitem(ModelSQL, ModelView):
"Workflow workitem"
_table = "wkf_workitem"
_name = "workflow.workitem"
_rec_name = 'state'
_description = __doc__
activity = fields.Many2One('workflow.activity', 'Activity',
required=True, ondelete="CASCADE", select=1)
subflow = fields.Many2One('workflow.instance', 'Subflow',
ondelete="CASCADE", select=1)
instance = fields.Many2One('workflow.instance', 'Instance',
required=True, ondelete="CASCADE", select=1)
state = fields.Char('State', select=1)
def __init__(self):
super(WorkflowWorkitem, self).__init__()
for i in ('create', 'write', 'delete', 'copy'):
del self._rpc[i]
def fields_get(self, fields_names=None):
res = super(WorkflowWorkitem, self).fields_get(
fields_names=fields_names)
for field in res:
res[field]['readonly'] = True
return res
def create(self, values):
workitem_id = super(WorkflowWorkitem, self).create(values)
with Transaction().set_user(0):
workitem = self.browse(workitem_id)
self.process(workitem)
return workitem_id
def process(self, workitem, signal=None, force_running=False):
'''
Process a workitem
:param workitem: a BrowseRecord of the workflow.workitem
:param signal: the signal
:param force_running: a boolean
'''
pool = Pool()
trigger_obj = pool.get('workflow.trigger')
activity = workitem.activity
triggers = False
if workitem.state == 'active':
triggers = True
if not self._execute(workitem, activity):
return False
if workitem.state == 'complete' or force_running:
res = self._split_test(workitem, activity.split_mode, signal)
triggers = triggers and not res
if triggers:
trigger_transitions = (t for t in activity.out_transitions
if t.trigger_model)
for transition in trigger_transitions:
model_obj = pool.get(workitem.instance.res_type)
model_ids_fct = getattr(model_obj, transition.trigger_ids)
ids = model_ids_fct(model_obj.browse(workitem.instance.res_id))
with Transaction().set_user(0):
for res_id in ids:
trigger_obj.create({
'model': transition.trigger_model,
'res_id': res_id,
'instance': workitem.instance.id,
'workitem': workitem.id,
})
return True
def _state_set(self, workitem, state):
self.write(workitem.id, {
'state': state,
})
#XXX must be changed with a cache reset on BrowseRecord
workitem._data[workitem.id]['state'] = state
def _execute(self, workitem, activity):
pool = Pool()
instance_obj = pool.get('workflow.instance')
#send a signal to overflow
if (workitem.state == 'active') and activity.signal_send:
for overflow in workitem.overflows:
instance_obj.validate(overflow.instance, activity.signal_send,
force_running=True)
if workitem.state == 'active':
self._state_set(workitem, 'running')
if activity.stop_other:
ids = self.search([
('instance', '=', workitem.instance.id),
('id', '!=', workitem.id),
])
#XXX check if delete must not be replace by _state_set 'complete'
with Transaction().set_user(0):
self.delete(ids)
if activity.method:
id_new = self._execute_action(workitem, activity)
else:
id_new = None
if activity.subflow:
if activity.method and id_new:
with Transaction().set_user(0):
instance_id = instance_obj.search([
('res_id', '=', id_new),
('workflow', '=', activity.subflow.id),
], limit=1)[0]
elif not activity.method:
instance_id = instance_obj.create({
'res_type': workitem.instance.res_type,
'res_id': workitem.instance.res_id,
'workflow': activity.subflow.id,
})
else:
with Transaction().set_user(0):
self.delete(workitem.id)
return False
self.write(workitem.id, {
'subflow': instance_id,
})
#XXX must be changed with a cache reset on BrowseRecord
workitem._data[workitem.id]['subflow'] = instance_id
else:
self._state_set(workitem, 'complete')
elif workitem.state == 'running' and activity.subflow:
if workitem.subflow.state == 'complete':
self._state_set(workitem, 'complete')
return True
def _execute_action(self, workitem, activity):
model_obj = Pool().get(workitem.instance.res_type)
wkf_action = getattr(model_obj, activity.method)
return wkf_action(model_obj.browse(workitem.instance.res_id))
def _activate_transition(self, workitem, transition, signal):
pool = Pool()
user_obj = pool.get('res.user')
if transition.signal:
if signal != transition.signal:
return False
if transition.group and Transaction().user != 0:
user_groups = user_obj.get_groups()
if transition.group.id not in user_groups:
return False
model_obj = pool.get(workitem.instance.res_type)
if transition.condition:
test_fct = getattr(model_obj, transition.condition)
return test_fct(model_obj.browse(workitem.instance.res_id))
else:
return True
def _split_test(self, workitem, split_mode, signal=None):
pool = Pool()
instance_obj = pool.get('workflow.instance')
test = False
transitions = []
if split_mode == 'XOR' or split_mode == 'OR':
for transition in workitem.activity.out_transitions:
if self._activate_transition(workitem, transition, signal):
test = True
transitions.append(transition)
if split_mode == 'XOR':
break
else:
test = True
for transition in workitem.activity.out_transitions:
if not self._activate_transition(workitem, transition, signal):
test = False
break
if transition.id not in \
[x.id for x in workitem.instance.transitions]:
transitions.append(transition)
if test and len(transitions):
with Transaction().set_user(0):
instance_obj.write(workitem.instance.id, {
'transitions': [('add', [x.id for x in transitions])],
})
instance = workitem.instance
self.delete(workitem.id)
for transition in transitions:
self._join_test(transition, instance)
return True
return False
def _join_test(self, transition, instance):
pool = Pool()
instance_obj = pool.get('workflow.instance')
activity = transition.act_to
if activity.join_mode == 'XOR':
self.create({
'activity': activity.id,
'instance': instance.id,
'state': 'active',
})
with Transaction().set_user(0):
instance_obj.write(instance.id, {
'transitions': [('unlink', transition.id)],
})
else:
delete = True
for transition in activity.in_transitions:
if instance.id not in \
[x.id for x in transition.instances]:
delete = False
break
if delete:
with Transaction().set_user(0):
instance_obj.write(instance.id, {
'transitions': [('unlink', [x.id
for x in activity.in_transitions])],
})
self.create({
'activity': activity.id,
'instance': instance.id,
'state': 'active',
})
WorkflowWorkitem()
class WorkflowTrigger(ModelSQL, ModelView):
"Workflow trigger"
_table = "wkf_trigger"
_name = "workflow.trigger"
_description = __doc__
res_id = fields.Integer('Resource ID')
model = fields.Char('Model')
instance = fields.Many2One('workflow.instance',
'Destination Instance', ondelete="CASCADE")
workitem = fields.Many2One('workflow.workitem', 'Workitem',
required=True, ondelete="CASCADE")
def __init__(self):
super(WorkflowTrigger, self).__init__()
for i in ('create', 'write', 'delete', 'copy'):
del self._rpc[i]
def init(self, module_name):
super(WorkflowTrigger, self).init(module_name)
table = TableHandler(Transaction().cursor, self, module_name)
table.index_action(['res_id', 'model'], 'add')
def fields_get(self, fields_names=None):
res = super(WorkflowTrigger, self).fields_get(
fields_names=fields_names)
for field in res:
res[field]['readonly'] = True
return res
WorkflowTrigger()
class InstanceGraph(Report):
_name = 'workflow.instance.graph'
def execute(self, ids, datas):
import pydot
pool = Pool()
lang_obj = pool.get('ir.lang')
workflow_obj = pool.get('workflow')
instance_obj = pool.get('workflow.instance')
lang_id = lang_obj.search([
('code', '=', Transaction().language),
], limit=1)[0]
lang = lang_obj.browse(lang_id)
workflow_id = workflow_obj.search([
('model', '=', datas['model']),
], limit=1)
if not workflow_id:
workflow_obj.raise_user_error('no_workflow_defined')
workflow_id = workflow_id[0]
workflow = workflow_obj.browse(workflow_id)
instance_id = instance_obj.search([
('res_id', '=', datas['id']),
('workflow', '=', workflow.id),
], order=[('id', 'DESC')], limit=1)
if not instance_id:
instance_obj.raise_user_error('no_instance_defined')
instance_id = instance_id[0]
title = "Workflow: %s" % (workflow.name.encode('ascii', 'replace'),)
if pydot.__version__ == '1.0.2':
# version 1.0.2 doesn't quote correctly label on Dot object
title = '"' + title + '"'
graph = pydot.Dot(fontsize='16',
label=title)
graph.set('center', '1')
graph.set('ratio', 'auto')
if lang.direction == 'ltr':
if hasattr(graph, 'set_rankdir'):
graph.set_rankdir('LR')
else:
graph.set('rankdir', 'LR')
else:
if hasattr(graph, 'set_rankdir'):
graph.set_rankdir('RL')
else:
graph.set('rankdir', 'RL')
self.graph_instance_get(graph, instance_id, datas.get('nested', False))
data = graph.create(prog='dot', format='png')
return ('png', buffer(data), False, workflow.name)
def graph_instance_get(self, graph, instance_id, nested=False):
pool = Pool()
instance_obj = pool.get('workflow.instance')
instance = instance_obj.browse(instance_id)
self.graph_get(graph, instance.workflow.id, nested,
self.workitem_get(instance.id))
def workitem_get(self, instance_id):
res = {}
pool = Pool()
workitem_obj = pool.get('workflow.workitem')
workitem_ids = workitem_obj.search([
('instance', '=', instance_id),
])
workitems = workitem_obj.browse(workitem_ids)
for workitem in workitems:
res.setdefault(workitem.activity.id, 0)
res[workitem.activity.id] += 1
if workitem.subflow:
res.update(self.workitem_get(workitem.subflow.id))
return res
def graph_get(self, graph, workflow_id, nested=False, workitem=None):
import pydot
if workitem is None:
workitem = {}
pool = Pool()
activity_obj = pool.get('workflow.activity')
workflow_obj = pool.get('workflow')
transition_obj = pool.get('workflow.transition')
activity_ids = activity_obj.search([
('workflow', '=', workflow_id),
])
id2activities = {}
actfrom = {}
actto = {}
activities = activity_obj.browse(activity_ids)
start = 0
stop = {}
for activity in activities:
if activity.flow_start:
start = activity.id
if activity.flow_stop:
stop['subflow.' + activity.name] = activity.id
id2activities[activity.id] = activity
if activity.subflow and nested:
workflow = workflow_obj.browse(activity.subflow.id)
subgraph = pydot.Cluster('subflow' + str(workflow.id),
fontsize='12',
label="Subflow: " + activity.name.encode('ascii', 'replace'))
(substart, substop) = self.graph_get(subgraph, workflow.id,
nested, workitem)
graph.add_subgraph(subgraph)
actfrom[activity.id] = substart
actto[activity.id] = substop
else:
args = {}
args['label'] = activity.name.encode('ascii', 'replace')
args['fontsize'] = '10'
if activity.flow_start or activity.flow_stop:
args['style'] = 'filled'
args['color'] = 'lightgrey'
if activity.subflow:
args['shape'] = 'box'
else:
args['shape'] = 'octagon'
if activity.id in workitem:
args['label'] += '\\nx ' + str(workitem[activity.id])
args['color'] = 'red'
graph.add_node(pydot.Node(activity.id, **args))
actfrom[activity.id] = (activity.id, {})
actto[activity.id] = (activity.id, {})
transition_ids = transition_obj.search([
('act_from', 'in', [x.id for x in activities]),
])
transitions = transition_obj.browse(transition_ids)
for transition in transitions:
args = {}
args['label'] = ' '
if transition.condition:
args['label'] += transition.condition
if transition.signal:
args['label'] += '\\n' + str(transition.signal)
args['style'] = 'bold'
if id2activities[transition.act_from.id].split_mode == 'AND':
args['arrowtail'] = 'box'
elif id2activities[transition.act_from.id].split_mode == 'OR':
args['arrowtail'] = 'inv'
if id2activities[transition.act_to.id].join_mode == 'AND':
args['arrowhead'] = 'crow'
activity_from = actfrom[transition.act_from.id][1].get(
transition.signal, actfrom[transition.act_from.id][0])
activity_to = actto[transition.act_to.id][1].get(
transition.signal, actto[transition.act_to.id][0])
graph.add_edge(pydot.Edge(str(activity_from), str(activity_to),
fontsize='10', **args))
return ((start, {}), (stop.values()[0], stop))
InstanceGraph()