1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 # Copyright (C) 2010-2012 OpenERP SA (<http://openerp.com>).
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as
10 # published by the Free Software Foundation, either version 3 of the
11 # License, or (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
26 from openerp.tools.safe_eval import safe_eval as eval
27 from openerp import tools
28 import openerp.modules
29 from openerp.osv import fields, osv
30 from openerp.tools.translate import _
31 from openerp import SUPERUSER_ID
33 MENU_ITEM_SEPARATOR = "/"
35 class ir_ui_menu(osv.osv):
38 def __init__(self, *args, **kwargs):
39 self.cache_lock = threading.RLock()
41 super(ir_ui_menu, self).__init__(*args, **kwargs)
42 self.pool.get('ir.model.access').register_cache_clearing_method(self._name, 'clear_cache')
44 def clear_cache(self):
46 # radical but this doesn't frequently happen
48 # Normally this is done by openerp.tools.ormcache
49 # but since we do not use it, set it by ourself.
50 self.pool._any_cache_cleared = True
53 def _filter_visible_menus(self, cr, uid, ids, context=None):
54 """Filters the give menu ids to only keep the menu items that should be
55 visible in the menu hierarchy of the current user.
56 Uses a cache for speeding up the computation.
59 modelaccess = self.pool.get('ir.model.access')
60 user_groups = set(self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['groups_id'])['groups_id'])
62 for menu in self.browse(cr, uid, ids, context=context):
63 # this key works because user access rights are all based on user's groups (cfr ir_model_access.check)
64 key = (cr.dbname, menu.id, tuple(user_groups))
65 if key in self._cache:
67 result.append(menu.id)
68 #elif not menu.groups_id and not menu.action:
69 # result.append(menu.id)
72 self._cache[key] = False
74 restrict_to_groups = [g.id for g in menu.groups_id]
75 if not user_groups.intersection(restrict_to_groups):
77 #result.append(menu.id)
78 #self._cache[key] = True
82 # we check if the user has access to the action of the menu
85 model_field = { 'ir.actions.act_window': 'res_model',
86 'ir.actions.report.xml': 'model',
87 'ir.actions.wizard': 'model',
88 'ir.actions.server': 'model_id',
91 field = model_field.get(menu.action._name)
92 if field and data[field]:
93 if not modelaccess.check(cr, uid, data[field], 'read', False):
96 # if there is no action, it's a 'folder' menu
98 # not displayed if there is no children
101 result.append(menu.id)
102 self._cache[key] = True
105 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
109 ids = super(ir_ui_menu, self).search(cr, uid, args, offset=0,
110 limit=None, order=order, context=context, count=False)
117 # menu filtering is done only on main menu tree, not other menu lists
118 if context.get('ir.ui.menu.full_list'):
121 result = self._filter_visible_menus(cr, uid, ids, context=context)
124 result = result[long(offset):]
126 result = result[:long(limit)]
132 def name_get(self, cr, uid, ids, context=None):
135 elmt = self.browse(cr, uid, id, context=context)
136 res.append((id, self._get_one_full_name(elmt)))
139 def _get_full_name(self, cr, uid, ids, name=None, args=None, context=None):
143 for elmt in self.browse(cr, uid, ids, context=context):
144 res[elmt.id] = self._get_one_full_name(elmt)
147 def _get_one_full_name(self, elmt, level=6):
151 parent_path = self._get_one_full_name(elmt.parent_id, level-1) + MENU_ITEM_SEPARATOR
154 return parent_path + elmt.name
156 def create(self, *args, **kwargs):
158 return super(ir_ui_menu, self).create(*args, **kwargs)
160 def write(self, *args, **kwargs):
162 return super(ir_ui_menu, self).write(*args, **kwargs)
164 def unlink(self, cr, uid, ids, context=None):
165 # Detach children and promote them to top-level, because it would be unwise to
166 # cascade-delete submenus blindly. We also can't use ondelete=set null because
167 # that is not supported when _parent_store is used (would silently corrupt it).
168 # TODO: ideally we should move them under a generic "Orphans" menu somewhere?
169 if isinstance(ids, (int, long)):
171 local_context = dict(context or {})
172 local_context['ir.ui.menu.full_list'] = True
173 direct_children_ids = self.search(cr, uid, [('parent_id', 'in', ids)], context=local_context)
174 if direct_children_ids:
175 self.write(cr, uid, direct_children_ids, {'parent_id': False})
177 result = super(ir_ui_menu, self).unlink(cr, uid, ids, context=context)
181 def copy(self, cr, uid, id, default=None, context=None):
182 ir_values_obj = self.pool.get('ir.values')
183 res = super(ir_ui_menu, self).copy(cr, uid, id, context=context)
184 datas=self.read(cr,uid,[res],['name'])[0]
185 rex=re.compile('\([0-9]+\)')
186 concat=rex.findall(datas['name'])
188 next_num=int(concat[0])+1
189 datas['name']=rex.sub(('(%d)'%next_num),datas['name'])
191 datas['name'] += '(1)'
192 self.write(cr,uid,[res],{'name':datas['name']})
193 ids = ir_values_obj.search(cr, uid, [
194 ('model', '=', 'ir.ui.menu'),
197 for iv in ir_values_obj.browse(cr, uid, ids):
198 ir_values_obj.copy(cr, uid, iv.id, default={'res_id': res},
202 def _action(self, cursor, user, ids, name, arg, context=None):
204 ir_values_obj = self.pool.get('ir.values')
205 value_ids = ir_values_obj.search(cursor, user, [
206 ('model', '=', self._name), ('key', '=', 'action'),
207 ('key2', '=', 'tree_but_open'), ('res_id', 'in', ids)],
210 for value in ir_values_obj.browse(cursor, user, value_ids, context=context):
211 values_action[value.res_id] = value.value
213 res[menu_id] = values_action.get(menu_id, False)
216 def _action_inv(self, cursor, user, menu_id, name, value, arg, context=None):
220 if self.CONCURRENCY_CHECK_FIELD in ctx:
221 del ctx[self.CONCURRENCY_CHECK_FIELD]
222 ir_values_obj = self.pool.get('ir.values')
223 values_ids = ir_values_obj.search(cursor, user, [
224 ('model', '=', self._name), ('key', '=', 'action'),
225 ('key2', '=', 'tree_but_open'), ('res_id', '=', menu_id)],
227 if value and values_ids:
228 ir_values_obj.write(cursor, user, values_ids, {'value': value}, context=ctx)
230 # no values_ids, create binding
231 ir_values_obj.create(cursor, user, {
236 'key2': 'tree_but_open',
240 # value is False, remove existing binding
241 ir_values_obj.unlink(cursor, user, values_ids, context=ctx)
243 def _get_icon_pict(self, cr, uid, ids, name, args, context):
245 for m in self.browse(cr, uid, ids, context=context):
246 res[m.id] = ('stock', (m.icon,'ICON_SIZE_MENU'))
249 def onchange_icon(self, cr, uid, ids, icon):
252 return {'type': {'icon_pict': 'picture'}, 'value': {'icon_pict': ('stock', (icon,'ICON_SIZE_MENU'))}}
254 def read_image(self, path):
257 path_info = path.split(',')
258 icon_path = openerp.modules.get_module_resource(path_info[0],path_info[1])
262 icon_file = tools.file_open(icon_path,'rb')
263 icon_image = base64.encodestring(icon_file.read())
268 def _get_image_icon(self, cr, uid, ids, names, args, context=None):
270 for menu in self.browse(cr, uid, ids, context=context):
271 res[menu.id] = r = {}
273 fn_src = fn[:-5] # remove _data
274 r[fn] = self.read_image(menu[fn_src])
278 def _get_needaction_enabled(self, cr, uid, ids, field_names, args, context=None):
279 """ needaction_enabled: tell whether the menu has a related action
280 that uses the needaction mechanism. """
281 res = dict.fromkeys(ids, False)
282 for menu in self.browse(cr, uid, ids, context=context):
283 if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model:
284 if menu.action.res_model in self.pool and self.pool[menu.action.res_model]._needaction:
288 def get_needaction_data(self, cr, uid, ids, context=None):
289 """ Return for each menu entry of ids :
290 - if it uses the needaction mechanism (needaction_enabled)
291 - the needaction counter of the related action, taking into account
298 for menu in self.browse(cr, uid, ids, context=context):
299 menu_ids.add(menu.id)
301 if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.context:
303 # use magical UnquoteEvalContext to ignore undefined client-side variables such as `active_id`
304 eval_ctx = tools.UnquoteEvalContext(**context)
305 ctx = eval(menu.action.context, locals_dict=eval_ctx, nocopy=True) or None
307 # if the eval still fails for some reason, we'll simply skip this menu
309 menu_ref = ctx and ctx.get('needaction_menu_ref')
311 if not isinstance(menu_ref, list):
312 menu_ref = [menu_ref]
313 model_data_obj = self.pool.get('ir.model.data')
314 for menu_data in menu_ref:
315 model, id = model_data_obj.get_object_reference(cr, uid, menu_data.split('.')[0], menu_data.split('.')[1])
316 if (model == 'ir.ui.menu'):
318 menu_ids = list(menu_ids)
320 for menu in self.browse(cr, uid, menu_ids, context=context):
322 'needaction_enabled': False,
323 'needaction_counter': False,
325 if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model:
326 if menu.action.res_model in self.pool:
327 obj = self.pool[menu.action.res_model]
329 if menu.action.type == 'ir.actions.act_window':
330 dom = menu.action.domain and eval(menu.action.domain, {'uid': uid}) or []
332 dom = eval(menu.action.params_store or '{}', {'uid': uid}).get('domain')
333 res[menu.id]['needaction_enabled'] = obj._needaction
334 res[menu.id]['needaction_counter'] = obj._needaction_count(cr, uid, dom, context=context)
338 'name': fields.char('Menu', size=64, required=True, translate=True),
339 'sequence': fields.integer('Sequence'),
340 'child_id': fields.one2many('ir.ui.menu', 'parent_id', 'Child IDs'),
341 'parent_id': fields.many2one('ir.ui.menu', 'Parent Menu', select=True, ondelete="restrict"),
342 'parent_left': fields.integer('Parent Left', select=True),
343 'parent_right': fields.integer('Parent Right', select=True),
344 'groups_id': fields.many2many('res.groups', 'ir_ui_menu_group_rel',
345 'menu_id', 'gid', 'Groups', help="If you have groups, the visibility of this menu will be based on these groups. "\
346 "If this field is empty, OpenERP will compute visibility based on the related object's read access."),
347 'complete_name': fields.function(_get_full_name,
348 string='Full Path', type='char', size=128),
349 'icon': fields.selection(tools.icons, 'Icon', size=64),
350 'icon_pict': fields.function(_get_icon_pict, type='char', size=32),
351 'web_icon': fields.char('Web Icon File', size=128),
352 'web_icon_hover': fields.char('Web Icon File (hover)', size=128),
353 'web_icon_data': fields.function(_get_image_icon, string='Web Icon Image', type='binary', readonly=True, store=True, multi='icon'),
354 'web_icon_hover_data': fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'),
355 'needaction_enabled': fields.function(_get_needaction_enabled,
358 string='Target model uses the need action mechanism',
359 help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.'),
360 'action': fields.function(_action, fnct_inv=_action_inv,
361 type='reference', string='Action',
363 ('ir.actions.report.xml', 'ir.actions.report.xml'),
364 ('ir.actions.act_window', 'ir.actions.act_window'),
365 ('ir.actions.wizard', 'ir.actions.wizard'),
366 ('ir.actions.act_url', 'ir.actions.act_url'),
367 ('ir.actions.server', 'ir.actions.server'),
368 ('ir.actions.client', 'ir.actions.client'),
372 def _rec_message(self, cr, uid, ids, context=None):
373 return _('Error ! You can not create recursive Menu.')
376 (osv.osv._check_recursion, _rec_message, ['parent_id'])
379 'icon': 'STOCK_OPEN',
380 'icon_pict': ('stock', ('STOCK_OPEN', 'ICON_SIZE_MENU')),
383 _order = "sequence,id"
386 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: