# 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 collections import defaultdict from itertools import groupby from trytond.model import ( DeactivableMixin, ModelSQL, ModelView, fields, sequence_ordered, tree) from trytond.pool import Pool from trytond.rpc import RPC from trytond.tools import grouped_slice from trytond.transaction import Transaction, inactive_records def one_in(i, j): """Check the presence of an element of setA in setB """ for k in i: if k in j: return True return False CLIENT_ICONS = [(x, x) for x in [ 'tryton-add', 'tryton-archive', 'tryton-attach', 'tryton-back', 'tryton-barcode-scanner', 'tryton-bookmark-border', 'tryton-bookmark', 'tryton-bookmarks', 'tryton-cancel', 'tryton-clear', 'tryton-close', 'tryton-copy', 'tryton-create', 'tryton-date', 'tryton-delete', 'tryton-download', 'tryton-email', 'tryton-error', 'tryton-exit', 'tryton-export', 'tryton-filter', 'tryton-format-align-center', 'tryton-format-align-justify', 'tryton-format-align-left', 'tryton-format-align-right', 'tryton-format-bold', 'tryton-format-color-text', 'tryton-format-italic', 'tryton-format-underline', 'tryton-forward', 'tryton-history', 'tryton-import', 'tryton-info', 'tryton-launch', 'tryton-link', 'tryton-log', 'tryton-menu', 'tryton-note', 'tryton-ok', 'tryton-open', 'tryton-print', 'tryton-public', 'tryton-refresh', 'tryton-remove', 'tryton-save', 'tryton-search', 'tryton-sound-off', 'tryton-sound-on', 'tryton-star-border', 'tryton-star', 'tryton-switch', 'tryton-translate', 'tryton-unarchive', 'tryton-undo', 'tryton-warning', ]] class UIMenu( DeactivableMixin, sequence_ordered(order='ASC NULLS LAST'), tree(separator=' / '), ModelSQL, ModelView): "UI menu" __name__ = 'ir.ui.menu' name = fields.Char('Menu', required=True, translate=True) childs = fields.One2Many('ir.ui.menu', 'parent', 'Children') parent = fields.Many2One('ir.ui.menu', 'Parent Menu', ondelete='CASCADE') complete_name = fields.Function(fields.Char('Complete Name'), 'get_rec_name', searcher='search_rec_name') icon = fields.Selection('list_icons', 'Icon', translate=False) action = fields.Function(fields.Reference('Action', selection=[ ('', ''), ('ir.action.report', 'ir.action.report'), ('ir.action.act_window', 'ir.action.act_window'), ('ir.action.wizard', 'ir.action.wizard'), ('ir.action.url', 'ir.action.url'), ], translate=False), 'get_action', setter='set_action') action_keywords = fields.One2Many( 'ir.action.keyword', 'model', "Action Keywords", filter=[ ('keyword', '=', 'tree_open'), ]) favorite = fields.Function(fields.Boolean('Favorite'), 'get_favorite') @classmethod def order_complete_name(cls, tables): return cls.name.convert_order('name', tables, cls) @staticmethod def default_icon(): return 'tryton-folder' @classmethod def default_sequence(cls): return 50 @staticmethod def list_icons(): pool = Pool() Icon = pool.get('ir.ui.icon') return sorted(CLIENT_ICONS + [(name, name) for _, name in Icon.list_icons()]) @classmethod def search_global(cls, text): # TODO improve search clause for record in cls.search([ ('rec_name', 'ilike', '%%%s%%' % text), ]): if record.action_keywords: yield record, record.rec_name, record.icon @classmethod def search(cls, domain, offset=0, limit=None, order=None, count=False, query=False): pool = Pool() ModelAccess = pool.get('ir.model.access') transaction = Transaction() def has_action_access(menu): if menu.action_keywords and all( k.action.type == 'ir.action.act_window' for k in menu.action_keywords): for keyword in menu.action_keywords: res_model = keyword.action.action.res_model if not res_model: break if ModelAccess.check(res_model, raise_exception=False): break else: return False return True menus = super(UIMenu, cls).search(domain, offset=offset, limit=limit, order=order, count=False, query=query) if query: return menus if transaction.check_access and menus: menus = list(filter(has_action_access, menus)) parent_ids = {x.parent.id for x in menus if x.parent} parents = set() for sub_parent_ids in grouped_slice(parent_ids): parents.update(cls.search([ ('id', 'in', list(sub_parent_ids)), ])) # Re-browse to avoid side-cache access menus = cls.browse([x.id for x in menus if (x.parent and x.parent in parents) or not x.parent]) if count: return len(menus) return menus @classmethod @inactive_records def get_action(cls, menus, name): pool = Pool() actions = dict((m.id, None) for m in menus) menus = cls.browse(menus) action_keywords = sum((list(m.action_keywords) for m in menus), []) def action_type(keyword): return keyword.action.type action_keywords.sort(key=action_type) for type, action_keywords in groupby(action_keywords, key=action_type): action_keywords = list(action_keywords) action2keywords = defaultdict(list) for action_keyword in action_keywords: model = action_keyword.model actions[model.id] = '%s,-1' % type action2keywords[action_keyword.action.id].append( action_keyword) Action = pool.get(type) factions = Action.search([ ('action', 'in', list(action2keywords.keys())), ]) for action in factions: for action_keyword in action2keywords[action.id]: actions[action_keyword.model.id] = str(action) return actions @classmethod def set_action(cls, menus, name, value): pool = Pool() ActionKeyword = pool.get('ir.action.keyword') action_keywords = [] transaction = Transaction() for i in range(0, len(menus), transaction.database.IN_MAX): sub_menus = menus[i:i + transaction.database.IN_MAX] action_keywords += ActionKeyword.search([ ('keyword', '=', 'tree_open'), ('model', 'in', [str(menu) for menu in sub_menus]), ]) if action_keywords: with Transaction().set_context(_timestamp=False): ActionKeyword.delete(action_keywords) if not value: return if isinstance(value, str): action_type, action_id = value.split(',') else: action_type, action_id = value if int(action_id) <= 0: return Action = pool.get(action_type) action = Action(int(action_id)) to_create = [] for menu in menus: with Transaction().set_context(_timestamp=False): to_create.append({ 'keyword': 'tree_open', 'model': str(menu), 'action': action.action.id, }) if to_create: ActionKeyword.create(to_create) @classmethod def get_favorite(cls, menus, name): pool = Pool() Favorite = pool.get('ir.ui.menu.favorite') user = Transaction().user favorites = Favorite.search([ ('menu', 'in', [m.id for m in menus]), ('user', '=', user), ]) menu2favorite = { m.id: False if m.action_keywords else None for m in menus} menu2favorite.update(dict((f.menu.id, True) for f in favorites)) return menu2favorite class UIMenuFavorite(sequence_ordered(), ModelSQL, ModelView): "Menu Favorite" __name__ = 'ir.ui.menu.favorite' menu = fields.Many2One('ir.ui.menu', 'Menu', required=True, ondelete='CASCADE') user = fields.Many2One('res.user', 'User', required=True, ondelete='CASCADE') @classmethod def __setup__(cls): super(UIMenuFavorite, cls).__setup__() cls.__rpc__.update({ 'get': RPC(check_access=False), 'set': RPC(check_access=False, readonly=False), 'unset': RPC(check_access=False, readonly=False), }) @staticmethod def default_user(): return Transaction().user @classmethod def get(cls): user = Transaction().user favorites = cls.search([ ('user', '=', user), ]) return [(f.menu.id, f.menu.rec_name, f.menu.icon) for f in favorites] @classmethod def set(cls, menu_id): user = Transaction().user cls.create([{ 'menu': menu_id, 'user': user, }]) @classmethod def unset(cls, menu_id): user = Transaction().user favorites = cls.search([ ('menu', '=', menu_id), ('user', '=', user), ]) cls.delete(favorites)