[FIX] web_kanban: parent can be undefined in some cases
[odoo/odoo.git] / openerp / workflow / workitem.py
1
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2014 OpenERP S.A. (<http://openerp.com).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 #
23 # TODO:
24 # cr.execute('delete from wkf_triggers where model=%s and res_id=%s', (res_type,res_id))
25 #
26 import logging
27 import instance
28
29 from openerp.workflow.helpers import Session
30 from openerp.workflow.helpers import Record
31 from openerp.workflow.helpers import WorkflowActivity
32
33 logger = logging.getLogger(__name__)
34
35 import openerp
36 from openerp.tools.safe_eval import safe_eval as eval
37
38 class Environment(dict):
39     """
40     Dictionary class used as an environment to evaluate workflow code (such as
41     the condition on transitions).
42
43     This environment provides sybmols for cr, uid, id, model name, model
44     instance, column names, and all the record (the one obtained by browsing
45     the provided ID) attributes.
46     """
47     def __init__(self, session, record):
48         self.cr = session.cr
49         self.uid = session.uid
50         self.model = record.model
51         self.id = record.id
52         self.ids = [record.id]
53         self.obj = openerp.registry(self.cr.dbname)[self.model]
54         self.columns = self.obj._columns.keys() + self.obj._inherit_fields.keys()
55
56     def __getitem__(self, key):
57         if (key in self.columns) or (key in dir(self.obj)):
58             res = self.obj.browse(self.cr, self.uid, self.id)
59             return res[key]
60         else:
61             return super(Environment, self).__getitem__(key)
62
63
64 class WorkflowItem(object):
65     def __init__(self, session, record, work_item_values):
66         assert isinstance(session, Session)
67         assert isinstance(record, Record)
68         self.session = session
69         self.record = record
70
71         if not work_item_values:
72             work_item_values = {}
73
74         assert isinstance(work_item_values, dict)
75         self.workitem = work_item_values
76
77     @classmethod
78     def create(cls, session, record, activity, instance_id, stack):
79         assert isinstance(session, Session)
80         assert isinstance(record, Record)
81         assert isinstance(activity, dict)
82         assert isinstance(instance_id, (long, int))
83         assert isinstance(stack, list)
84
85         cr = session.cr
86         cr.execute("select nextval('wkf_workitem_id_seq')")
87         id_new = cr.fetchone()[0]
88         cr.execute("insert into wkf_workitem (id,act_id,inst_id,state) values (%s,%s,%s,'active')", (id_new, activity['id'], instance_id))
89         cr.execute('select * from wkf_workitem where id=%s',(id_new,))
90         work_item_values = cr.dictfetchone()
91         logger.info('Created workflow item in activity %s',
92                     activity['id'],
93                     extra={'ident': (session.uid, record.model, record.id)})
94
95         workflow_item = WorkflowItem(session, record, work_item_values)
96         workflow_item.process(stack=stack)
97
98     @classmethod
99     def create_all(cls, session, record, activities, instance_id, stack):
100         assert isinstance(activities, list)
101
102         for activity in activities:
103             cls.create(session, record, activity, instance_id, stack)
104
105     def process(self, signal=None, force_running=False, stack=None):
106         assert isinstance(force_running, bool)
107         assert stack is not None
108
109         cr = self.session.cr
110
111         cr.execute('select * from wkf_activity where id=%s', (self.workitem['act_id'],))
112         activity = cr.dictfetchone()
113
114         triggers = False
115         if self.workitem['state'] == 'active':
116             triggers = True
117             if not self._execute(activity, stack):
118                 return False
119
120         if force_running or self.workitem['state'] == 'complete':
121             ok = self._split_test(activity['split_mode'], signal, stack)
122             triggers = triggers and not ok
123
124         if triggers:
125             cr.execute('select * from wkf_transition where act_from=%s', (self.workitem['act_id'],))
126             for trans in cr.dictfetchall():
127                 if trans['trigger_model']:
128                     ids = self.wkf_expr_eval_expr(trans['trigger_expr_id'])
129                     for res_id in ids:
130                         cr.execute('select nextval(\'wkf_triggers_id_seq\')')
131                         id =cr.fetchone()[0]
132                         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))
133
134         return True
135
136     def _execute(self, activity, stack):
137         """Send a signal to parenrt workflow (signal: subflow.signal_name)"""
138         result = True
139         cr = self.session.cr
140         signal_todo = []
141
142         if (self.workitem['state']=='active') and activity['signal_send']:
143             # signal_send']:
144             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'],))
145             for instance_id, model_name, record_id in cr.fetchall():
146                 record = Record(model_name, record_id)
147                 signal_todo.append((instance_id, record, activity['signal_send']))
148
149
150         if activity['kind'] == WorkflowActivity.KIND_DUMMY:
151             if self.workitem['state']=='active':
152                 self._state_set(activity, 'complete')
153                 if activity['action_id']:
154                     res2 = self.wkf_expr_execute_action(activity)
155                     if res2:
156                         stack.append(res2)
157                         result=res2
158
159         elif activity['kind'] == WorkflowActivity.KIND_FUNCTION:
160
161             if self.workitem['state']=='active':
162                 self._state_set(activity, 'running')
163                 returned_action = self.wkf_expr_execute(activity)
164                 if type(returned_action) in (dict,):
165                     stack.append(returned_action)
166                 if activity['action_id']:
167                     res2 = self.wkf_expr_execute_action(activity)
168                     # A client action has been returned
169                     if res2:
170                         stack.append(res2)
171                         result=res2
172                 self._state_set(activity, 'complete')
173
174         elif activity['kind'] == WorkflowActivity.KIND_STOPALL:
175             if self.workitem['state']=='active':
176                 self._state_set(activity, 'running')
177                 cr.execute('delete from wkf_workitem where inst_id=%s and id<>%s', (self.workitem['inst_id'], self.workitem['id']))
178                 if activity['action']:
179                     self.wkf_expr_execute(activity)
180                 self._state_set(activity, 'complete')
181
182         elif activity['kind'] == WorkflowActivity.KIND_SUBFLOW:
183
184             if self.workitem['state']=='active':
185
186                 self._state_set(activity, 'running')
187                 if activity.get('action', False):
188                     id_new = self.wkf_expr_execute(activity)
189                     if not id_new:
190                         cr.execute('delete from wkf_workitem where id=%s', (self.workitem['id'],))
191                         return False
192                     assert type(id_new)==type(1) or type(id_new)==type(1L), 'Wrong return value: '+str(id_new)+' '+str(type(id_new))
193                     cr.execute('select id from wkf_instance where res_id=%s and wkf_id=%s', (id_new, activity['subflow_id']))
194                     id_new = cr.fetchone()[0]
195                 else:
196                     inst = instance.WorkflowInstance(self.session, self.record)
197                     id_new = inst.create(activity['subflow_id'])
198
199                 cr.execute('update wkf_workitem set subflow_id=%s where id=%s', (id_new, self.workitem['id']))
200                 self.workitem['subflow_id'] = id_new
201
202             if self.workitem['state']=='running':
203                 cr.execute("select state from wkf_instance where id=%s", (self.workitem['subflow_id'],))
204                 state = cr.fetchone()[0]
205                 if state=='complete':
206                     self._state_set(activity, 'complete')
207
208         for instance_id, record, signal_send in signal_todo:
209             wi = instance.WorkflowInstance(self.session, record, {'id': instance_id})
210             wi.validate(signal_send, force_running=True)
211
212         return result
213
214     def _state_set(self, activity, state):
215         self.session.cr.execute('update wkf_workitem set state=%s where id=%s', (state, self.workitem['id']))
216         self.workitem['state'] = state
217         logger.info('Changed state of work item %s to "%s" in activity %s',
218                     self.workitem['id'], state, activity['id'],
219                     extra={'ident': (self.session.uid, self.record.model, self.record.id)})
220
221     def _split_test(self, split_mode, signal, stack):
222         cr = self.session.cr
223         cr.execute('select * from wkf_transition where act_from=%s', (self.workitem['act_id'],))
224         test = False
225         transitions = []
226         alltrans = cr.dictfetchall()
227
228         if split_mode in ('XOR', 'OR'):
229             for transition in alltrans:
230                 if self.wkf_expr_check(transition,signal):
231                     test = True
232                     transitions.append((transition['id'], self.workitem['inst_id']))
233                     if split_mode=='XOR':
234                         break
235         else:
236             test = True
237             for transition in alltrans:
238                 if not self.wkf_expr_check(transition, signal):
239                     test = False
240                     break
241                 cr.execute('select count(*) from wkf_witm_trans where trans_id=%s and inst_id=%s', (transition['id'], self.workitem['inst_id']))
242                 if not cr.fetchone()[0]:
243                     transitions.append((transition['id'], self.workitem['inst_id']))
244
245         if test and transitions:
246             cr.executemany('insert into wkf_witm_trans (trans_id,inst_id) values (%s,%s)', transitions)
247             cr.execute('delete from wkf_workitem where id=%s', (self.workitem['id'],))
248             for t in transitions:
249                 self._join_test(t[0], t[1], stack)
250             return True
251         return False
252
253     def _join_test(self, trans_id, inst_id, stack):
254         cr = self.session.cr
255         cr.execute('select * from wkf_activity where id=(select act_to from wkf_transition where id=%s)', (trans_id,))
256         activity = cr.dictfetchone()
257         if activity['join_mode']=='XOR':
258             WorkflowItem.create(self.session, self.record, activity, inst_id, stack=stack)
259             cr.execute('delete from wkf_witm_trans where inst_id=%s and trans_id=%s', (inst_id,trans_id))
260         else:
261             cr.execute('select id from wkf_transition where act_to=%s', (activity['id'],))
262             trans_ids = cr.fetchall()
263             ok = True
264             for (id,) in trans_ids:
265                 cr.execute('select count(*) from wkf_witm_trans where trans_id=%s and inst_id=%s', (id,inst_id))
266                 res = cr.fetchone()[0]
267                 if not res:
268                     ok = False
269                     break
270             if ok:
271                 for (id,) in trans_ids:
272                     cr.execute('delete from wkf_witm_trans where trans_id=%s and inst_id=%s', (id,inst_id))
273                 WorkflowItem.create(self.session, self.record, activity, inst_id, stack=stack)
274
275     def wkf_expr_eval_expr(self, lines):
276         """
277         Evaluate each line of ``lines`` with the ``Environment`` environment, returning
278         the value of the last line.
279         """
280         assert lines, 'You used a NULL action in a workflow, use dummy node instead.'
281         result = False
282         for line in lines.split('\n'):
283             line = line.strip()
284             if not line:
285                 continue
286             if line == 'True':
287                 result = True
288             elif line == 'False':
289                 result = False
290             else:
291                 env = Environment(self.session, self.record)
292                 result = eval(line, env, nocopy=True)
293         return result
294
295     def wkf_expr_execute_action(self, activity):
296         """
297         Evaluate the ir.actions.server action specified in the activity.
298         """
299         context = {
300             'active_model': self.record.model,
301             'active_id': self.record.id,
302             'active_ids': [self.record.id]
303         }
304
305         ir_actions_server = openerp.registry(self.session.cr.dbname)['ir.actions.server']
306         result = ir_actions_server.run(self.session.cr, self.session.uid, [activity['action_id']], context)
307
308         return result
309
310     def wkf_expr_execute(self, activity):
311         """
312         Evaluate the action specified in the activity.
313         """
314         return self.wkf_expr_eval_expr(activity['action'])
315
316     def wkf_expr_check(self, transition, signal):
317         """
318         Test if a transition can be taken. The transition can be taken if:
319
320         - the signal name matches,
321         - the uid is SUPERUSER_ID or the user groups contains the transition's
322           group,
323         - the condition evaluates to a truish value.
324         """
325         if transition['signal'] and signal != transition['signal']:
326             return False
327
328         if self.session.uid != openerp.SUPERUSER_ID and transition['group_id']:
329             registry = openerp.registry(self.session.cr.dbname)
330             user_groups = registry['res.users'].read(self.session.cr, self.session.uid, [self.session.uid], ['groups_id'])[0]['groups_id']
331             if transition['group_id'] not in user_groups:
332                 return False
333
334         return self.wkf_expr_eval_expr(transition['condition'])
335
336 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
337