[MERGE] Sync with trunk
[odoo/odoo.git] / addons / process / process.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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 from openerp import tools
23 from openerp.osv import fields, osv
24
25 class Env(dict):
26
27     def __init__(self, obj, user):
28         self.__obj = obj
29         self.__usr = user
30
31     def __getitem__(self, name):
32         if name in ('__obj', '__user'):
33             return super(Env, self).__getitem__(name)
34         if name == 'user':
35             return self.__user
36         if name == 'object':
37             return self.__obj
38         return self.__obj[name]
39
40 class process_process(osv.osv):
41     _name = "process.process"
42     _description = "Process"
43     _columns = {
44         'name': fields.char('Name', size=30,required=True, translate=True),
45         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the process without removing it."),
46         'model_id': fields.many2one('ir.model', 'Object', ondelete='set null'),
47         'note': fields.text('Notes', translate=True),
48         'node_ids': fields.one2many('process.node', 'process_id', 'Nodes')
49     }
50     _defaults = {
51         'active' : lambda *a: True,
52     }
53
54     def search_by_model(self, cr, uid, res_model, context=None):
55         model_ids = (res_model or None) and self.pool['ir.model'].search(cr, uid, [('model', '=', res_model)])
56
57         domain = (model_ids or []) and [('model_id', 'in', model_ids)]
58         result = []
59
60         # search all processes
61         res = self.pool['process.process'].search(cr, uid, domain)
62         if res:
63             res = self.pool['process.process'].browse(cr, uid, res, context=context)
64             for process in res:
65                 result.append((process.id, process.name))
66             return result
67
68         # else search process nodes
69         res = self.pool['process.node'].search(cr, uid, domain)
70         if res:
71             res = self.pool['process.node'].browse(cr, uid, res, context=context)
72             for node in res:
73                 if (node.process_id.id, node.process_id.name) not in result:
74                     result.append((node.process_id.id, node.process_id.name))
75
76         return result
77
78     def graph_get(self, cr, uid, id, res_model, res_id, scale, context=None):
79
80
81         process = self.pool['process.process'].browse(cr, uid, id, context=context)
82
83         name = process.name
84         resource = False
85         state = 'N/A'
86
87         expr_context = {}
88         states = {}
89         perm = False
90
91         if res_model:
92             states = dict(self.pool[res_model].fields_get(cr, uid, context=context).get('state', {}).get('selection', {}))
93
94         if res_id:
95             current_object = self.pool[res_model].browse(cr, uid, res_id, context=context)
96             current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
97             expr_context = Env(current_object, current_user)
98             resource = current_object.name
99             if 'state' in current_object:
100                 state = states.get(current_object.state, 'N/A')
101             perm = self.pool[res_model].perm_read(cr, uid, [res_id], context=context)[0]
102
103         notes = process.note or "N/A"
104         nodes = {}
105         start = []
106         transitions = {}
107
108         for node in process.node_ids:
109             data = {}
110             data['name'] = node.name
111             data['model'] = (node.model_id or None) and node.model_id.model
112             data['kind'] = node.kind
113             data['subflow'] = (node.subflow_id or False) and [node.subflow_id.id, node.subflow_id.name]
114             data['notes'] = node.note
115             data['active'] = False
116             data['gray'] = False
117             data['url'] = node.help_url
118             data['model_states'] = node.model_states
119
120             # get assosiated workflow
121             if data['model']:
122                 wkf_ids = self.pool['workflow'].search(cr, uid, [('osv', '=', data['model'])])
123                 data['workflow'] = (wkf_ids or False) and wkf_ids[0]
124
125             if 'directory_id' in node and node.directory_id:
126                 data['directory_id'] = node.directory_id.id
127                 data['directory'] = self.pool['document.directory'].get_resource_path(cr, uid, data['directory_id'], data['model'], False)
128
129             if node.menu_id:
130                 data['menu'] = {'name': node.menu_id.complete_name, 'id': node.menu_id.id}
131
132             try:
133                 gray = True
134                 for cond in node.condition_ids:
135                     if cond.model_id and cond.model_id.model == res_model:
136                         gray = gray and eval(cond.model_states, expr_context)
137                 data['gray'] = not gray
138             except:
139                 pass
140
141             if not data['gray']:
142                 if node.model_id and node.model_id.model == res_model:
143                     try:
144                         data['active'] = eval(node.model_states, expr_context)
145                     except Exception:
146                         pass
147
148             nodes[node.id] = data
149             if node.flow_start:
150                 start.append(node.id)
151
152             for tr in node.transition_out:
153                 data = {}
154                 data['name'] = tr.name
155                 data['source'] = tr.source_node_id.id
156                 data['target'] = tr.target_node_id.id
157                 data['notes'] = tr.note
158                 data['buttons'] = buttons = []
159                 for b in tr.action_ids:
160                     button = {}
161                     button['name'] = b.name
162                     button['state'] = b.state
163                     button['action'] = b.action
164                     buttons.append(button)
165                 data['groups'] = groups = []
166                 for r in tr.transition_ids:
167                     if r.group_id:
168                         groups.append({'name': r.group_id.name})
169                 for r in tr.group_ids:
170                     groups.append({'name': r.name})
171                 transitions[tr.id] = data
172
173         # now populate resource information
174         def update_relatives(nid, ref_id, ref_model):
175             relatives = []
176
177             for dummy, tr in transitions.items():
178                 if tr['source'] == nid:
179                     relatives.append(tr['target'])
180                 if tr['target'] == nid:
181                     relatives.append(tr['source'])
182
183             if not ref_id:
184                 nodes[nid]['res'] = False
185                 return
186
187             nodes[nid]['res'] = resource = {'id': ref_id, 'model': ref_model}
188
189             refobj = self.pool[ref_model].browse(cr, uid, ref_id, context=context)
190             fields = self.pool[ref_model].fields_get(cr, uid, context=context)
191
192             # check for directory_id from inherited from document module
193             if nodes[nid].get('directory_id', False):
194                 resource['directory'] = self.pool['document.directory'].get_resource_path(cr, uid, nodes[nid]['directory_id'], ref_model, ref_id)
195
196             resource['name'] = self.pool[ref_model].name_get(cr, uid, [ref_id], context=context)[0][1]
197             resource['perm'] = self.pool[ref_model].perm_read(cr, uid, [ref_id], context=context)[0]
198
199             ref_expr_context = Env(refobj, current_user)
200             try:
201                 if not nodes[nid]['gray']:
202                     nodes[nid]['active'] = eval(nodes[nid]['model_states'], ref_expr_context)
203             except:
204                 pass 
205             for r in relatives:
206                 node = nodes[r]
207                 if 'res' not in node:
208                     for n, f in fields.items():
209                         if node['model'] == ref_model:
210                             update_relatives(r, ref_id, ref_model)
211
212                         elif f.get('relation') == node['model']:
213                             rel = refobj[n]
214                             if rel and isinstance(rel, list) :
215                                 rel = rel[0]
216                             try: # XXX: rel has been reported as string (check it)
217                                 _id = (rel or False) and rel.id
218                                 _model = node['model']
219                                 update_relatives(r, _id, _model)
220                             except:
221                                 pass
222
223         if res_id:
224             for nid, node in nodes.items():
225                 if not node['gray'] and (node['active'] or node['model'] == res_model):
226                     update_relatives(nid, res_id, res_model)
227                     break
228
229         # calculate graph layout
230         g = tools.graph(nodes.keys(), map(lambda x: (x['source'], x['target']), transitions.values()))
231         g.process(start)
232         g.scale(*scale) #g.scale(100, 100, 180, 120)
233         graph = g.result_get()
234
235         # fix the height problem
236         miny = -1
237         for k,v in nodes.items():
238             x = graph[k]['x']
239             y = graph[k]['y']
240             if miny == -1:
241                 miny = y
242             miny = min(y, miny)
243             v['x'] = x
244             v['y'] = y
245
246         for k, v in nodes.items():
247             y = v['y']
248             v['y'] = min(y - miny + 10, y)
249         
250         nodes = dict([str(n_key), n_val] for n_key, n_val in nodes.iteritems())
251         transitions = dict([str(t_key), t_val] for t_key, t_val in transitions.iteritems())
252         return dict(name=name, resource=resource, state=state, perm=perm, notes=notes, nodes=nodes, transitions=transitions)
253
254     def copy(self, cr, uid, id, default=None, context=None):
255         """ Deep copy the entire process.
256         """
257
258         if not default:
259             default = {}
260         
261         process = self.pool['process.process'].browse(cr, uid, id, context=context)
262
263         nodes = {}
264         transitions = {}
265
266         # first copy all nodes and and map the new nodes with original for later use in transitions
267         for node in process.node_ids:
268             for t in node.transition_in:
269                 tr = transitions.setdefault(t.id, {})
270                 tr['target'] = node.id
271             for t in node.transition_out:
272                 tr = transitions.setdefault(t.id, {})
273                 tr['source'] = node.id
274             nodes[node.id] = self.pool['process.node'].copy(cr, uid, node.id, context=context)
275
276         # then copy transitions with new nodes
277         for tid, tr in transitions.items():
278             vals = {
279                 'source_node_id': nodes[tr['source']],
280                 'target_node_id': nodes[tr['target']]
281             }
282             tr = self.pool['process.transition'].copy(cr, uid, tid, default=vals, context=context)
283
284         # and finally copy the process itself with new nodes
285         default.update({
286             'active': True,
287             'node_ids': [(6, 0, nodes.values())]
288         })
289         return super(process_process, self).copy(cr, uid, id, default, context)
290
291
292 class process_node(osv.osv):
293     _name = 'process.node'
294     _description ='Process Node'
295     _columns = {
296         'name': fields.char('Name', size=30,required=True, translate=True),
297         'process_id': fields.many2one('process.process', 'Process', required=True, ondelete='cascade'),
298         'kind': fields.selection([('state','Status'), ('subflow','Subflow')], 'Kind of Node', required=True),
299         'menu_id': fields.many2one('ir.ui.menu', 'Related Menu'),
300         'note': fields.text('Notes', translate=True),
301         'model_id': fields.many2one('ir.model', 'Object', ondelete='set null'),
302         'model_states': fields.char('States Expression', size=128),
303         'subflow_id': fields.many2one('process.process', 'Subflow', ondelete='set null'),
304         'flow_start': fields.boolean('Starting Flow'),
305         'transition_in': fields.one2many('process.transition', 'target_node_id', 'Starting Transitions'),
306         'transition_out': fields.one2many('process.transition', 'source_node_id', 'Ending Transitions'),
307         'condition_ids': fields.one2many('process.condition', 'node_id', 'Conditions'),
308         'help_url': fields.char('Help URL', size=255)
309     }
310     _defaults = {
311         'kind': lambda *args: 'state',
312         'model_states': lambda *args: False,
313         'flow_start': lambda *args: False,
314     }
315
316     def copy_data(self, cr, uid, id, default=None, context=None):
317         if not default:
318             default = {}
319         default.update({
320             'transition_in': [],
321             'transition_out': []
322         })
323         return super(process_node, self).copy_data(cr, uid, id, default, context=context)
324
325
326 class process_node_condition(osv.osv):
327     _name = 'process.condition'
328     _description = 'Condition'
329     _columns = {
330         'name': fields.char('Name', size=30, required=True),
331         'node_id': fields.many2one('process.node', 'Node', required=True, ondelete='cascade'),
332         'model_id': fields.many2one('ir.model', 'Object', ondelete='set null'),
333         'model_states': fields.char('Expression', required=True, size=128)
334     }
335
336 class process_transition(osv.osv):
337     _name = 'process.transition'
338     _description ='Process Transition'
339     _columns = {
340         'name': fields.char('Name', size=32, required=True, translate=True),
341         'source_node_id': fields.many2one('process.node', 'Source Node', required=True, ondelete='cascade'),
342         'target_node_id': fields.many2one('process.node', 'Target Node', required=True, ondelete='cascade'),
343         'action_ids': fields.one2many('process.transition.action', 'transition_id', 'Buttons'),
344         'transition_ids': fields.many2many('workflow.transition', 'process_transition_ids', 'ptr_id', 'wtr_id', 'Workflow Transitions'),
345         'group_ids': fields.many2many('res.groups', 'process_transition_group_rel', 'tid', 'rid', string='Required Groups'),
346         'note': fields.text('Description', translate=True),
347     }
348
349 class process_transition_action(osv.osv):
350     _name = 'process.transition.action'
351     _description ='Process Transitions Actions'
352     _columns = {
353         'name': fields.char('Name', size=32, required=True, translate=True),
354         'state': fields.selection([('dummy','Dummy'),
355                                    ('object','Object Method'),
356                                    ('workflow','Workflow Trigger'),
357                                    ('action','Action')], 'Type', required=True),
358         'action': fields.char('Action ID', size=64, states={
359             'dummy':[('readonly',1)],
360             'object':[('required',1)],
361             'workflow':[('required',1)],
362             'action':[('required',1)],
363         },),
364         'transition_id': fields.many2one('process.transition', 'Transition', required=True, ondelete='cascade')
365     }
366     _defaults = {
367         'state': lambda *args: 'dummy',
368     }
369
370     def copy_data(self, cr, uid, id, default=None, context=None):
371         if not default:
372             default = {}
373             
374         state = self.pool['process.transition.action'].browse(cr, uid, id, context=context).state
375         if state:
376             default['state'] = state
377
378         return super(process_transition_action, self).copy_data(cr, uid, id, default, context)
379
380
381 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: