Raphael Collet (OpenERP) has proposed merging 
lp:~openerp-dev/openobject-server/trunk-user-groups-rco into 
lp:openobject-server.

Requested reviews:
  OpenERP Core Team (openerp)

For more details, see:
https://code.launchpad.net/~openerp-dev/openobject-server/trunk-user-groups-rco/+merge/85493

New implementation of tab "Access rights" on users form:
 - add category_id (ir.module.category) on groups;
 - use category_id for structured naming of groups;
 - classify groups by category instead of using name;
 - check for full ordering when using selection widget;
 - generate view that inherits users form view, instead of overriding 
fields_view_get.

-- 
https://code.launchpad.net/~openerp-dev/openobject-server/trunk-user-groups-rco/+merge/85493
Your team OpenERP R&D Team is subscribed to branch 
lp:~openerp-dev/openobject-server/trunk-user-groups-rco.
=== modified file 'openerp/addons/base/base_update.xml'
--- openerp/addons/base/base_update.xml	2011-10-01 21:14:25 +0000
+++ openerp/addons/base/base_update.xml	2011-12-13 14:13:37 +0000
@@ -19,6 +19,7 @@
             <field name="type">form</field>
             <field name="arch" type="xml">
                 <form string="Groups">
+                    <field name="category_id" select="1"/>
                     <field name="name" select="1"/>
                     <notebook colspan="4">
                         <page string="Users">
@@ -154,6 +155,17 @@
                 </form>
             </field>
         </record>
+
+        <record id="user_groups_view" model="ir.ui.view">
+            <field name="name">res.users.groups</field>
+            <field name="model">res.users</field>
+            <field name="inherit_id" ref="view_users_form"/>
+            <field name="arch" type="xml">
+                <!-- dummy, will be modified by groups -->
+                <field name="groups_id" position="after"/>
+            </field>
+        </record>
+
         <record id="view_users_tree" model="ir.ui.view">
             <field name="name">res.users.tree</field>
             <field name="model">res.users</field>

=== modified file 'openerp/addons/base/module/module.py'
--- openerp/addons/base/module/module.py	2011-12-13 11:44:42 +0000
+++ openerp/addons/base/module/module.py	2011-12-13 14:13:37 +0000
@@ -70,12 +70,12 @@
         return result
 
     _columns = {
-        'name': fields.char("Name", size=128, required=True, select=True),
+        'name': fields.char("Name", size=128, required=True, translate=True, select=True),
         'parent_id': fields.many2one('ir.module.category', 'Parent Application', select=True),
         'child_ids': fields.one2many('ir.module.category', 'parent_id', 'Child Applications'),
         'module_nr': fields.function(_module_nbr, method=True, string='Number of Modules', type='integer'),
         'module_ids' : fields.one2many('ir.module.module', 'category_id', 'Modules'),
-        'description' : fields.text("Description"),
+        'description' : fields.text("Description", translate=True),
         'sequence' : fields.integer('Sequence'),
         'visible' : fields.boolean('Visible'),
     }

=== modified file 'openerp/addons/base/module/module_data.xml'
--- openerp/addons/base/module/module_data.xml	2011-11-08 22:36:21 +0000
+++ openerp/addons/base/module/module_data.xml	2011-12-13 14:13:37 +0000
@@ -114,13 +114,44 @@
             <field name="sequence">15</field>
         </record>
 
-<!--
-        <record id="ir_ui_view_sc_modules0" model="ir.ui.view_sc">
-            <field name="name">Modules</field>
-            <field name="resource">ir.ui.menu</field>
-            <field name="user_id" ref="base.user_root"/>
-            <field name="res_id" ref="base.menu_module_tree"/>
-        </record>
--->
+        <record model="ir.module.category" id="module_category_administration">
+            <field name="name">Administration</field>
+            <field name="sequence">100</field>
+        </record>
+
+        <record model="ir.module.category" id="module_category_usability">
+            <field name="name">Usability</field>
+            <field name="sequence">101</field>
+        </record>
+
+        <!-- add applications to base groups -->
+        <record model="res.groups" id="group_erp_manager">
+            <field name="category_id" ref="module_category_administration"/>
+        </record>
+        <record model="res.groups" id="group_system">
+            <field name="category_id" ref="module_category_administration"/>
+        </record>
+
+        <record model="res.groups" id="group_user">
+            <field name="category_id" ref="module_category_human_resources"/>
+        </record>
+
+        <record model="res.groups" id="group_multi_company">
+            <field name="category_id" ref="module_category_usability"/>
+        </record>
+        <record model="res.groups" id="group_extended">
+            <field name="category_id" ref="module_category_usability"/>
+        </record>
+        <record model="res.groups" id="group_no_one">
+            <field name="category_id" ref="module_category_usability"/>
+        </record>
+
+        <record model="res.groups" id="group_sale_salesman">
+            <field name="category_id" ref="module_category_sales_management"/>
+        </record>
+        <record model="res.groups" id="group_sale_manager">
+            <field name="category_id" ref="module_category_sales_management"/>
+        </record>
+
     </data>
 </openerp>

=== modified file 'openerp/addons/base/module/module_view.xml'
--- openerp/addons/base/module/module_view.xml	2011-12-13 12:34:12 +0000
+++ openerp/addons/base/module/module_view.xml	2011-12-13 14:13:37 +0000
@@ -11,8 +11,10 @@
             <field name="field_parent">child_ids</field>
             <field name="arch" type="xml">
                 <form string="Module Category">
-                    <field colspan="4" name="name"/>
-                    <field colspan="4" name="parent_id"/>
+                    <field name="name"/>
+                    <field name="parent_id"/>
+                    <field name="sequence"/>
+                    <field name="description" colspan="4"/>
                 </form>
             </field>
         </record>

=== modified file 'openerp/addons/base/res/res_users.py'
--- openerp/addons/base/res/res_users.py	2011-12-12 16:21:33 +0000
+++ openerp/addons/base/res/res_users.py	2011-12-13 14:13:37 +0000
@@ -22,11 +22,8 @@
 
 import logging
 from functools import partial
-from xml.sax.saxutils import quoteattr
 
-import simplejson
 import pytz
-from lxml import etree
 
 import netsvc
 import pooler
@@ -40,21 +37,44 @@
 
 class groups(osv.osv):
     _name = "res.groups"
-    _order = 'name'
     _description = "Access Groups"
+    _rec_name = 'full_name'
+
+    def _get_full_name(self, cr, uid, ids, field, arg, context=None):
+        res = {}
+        for g in self.browse(cr, uid, ids, context):
+            if g.category_id:
+                res[g.id] = '%s / %s' % (g.category_id.name, g.name)
+            else:
+                res[g.id] = g.name
+        return res
+
     _columns = {
-        'name': fields.char('Group Name', size=64, required=True, translate=True),
+        'name': fields.char('Name', size=64, required=True, translate=True),
         'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
         'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
         'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
             'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
         'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
-        'comment' : fields.text('Comment',size=250),
+        'comment' : fields.text('Comment', size=250, translate=True),
+        'category_id': fields.many2one('ir.module.category', 'Application', select=True),
+        'full_name': fields.function(_get_full_name, type='char', string='Group Name'),
     }
+
     _sql_constraints = [
-        ('name_uniq', 'unique (name)', 'The name of the group must be unique !')
+        ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
     ]
 
+    def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
+        # add explicit ordering if search is sorted on full_name
+        if order and order.startswith('full_name'):
+            ids = super(groups, self).search(cr, uid, args, context=context)
+            gs = self.browse(cr, uid, ids, context)
+            gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
+            gs = gs[offset:offset+limit] if limit else gs[offset:]
+            return map(int, gs)
+        return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
+
     def copy(self, cr, uid, id, default=None, context=None):
         group_name = self.read(cr, uid, [id], ['name'])[0]['name']
         default.update({'name': _('%s (copy)')%group_name})
@@ -522,30 +542,68 @@
 
 users()
 
+
+
 #
 # Extension of res.groups and res.users with a relation for "implied" or 
 # "inherited" groups.  Once a user belongs to a group, it automatically belongs
 # to the implied groups (transitively).
 #
 
+class cset(object):
+    """ A cset (constrained set) is a set of elements that may be constrained to
+        be a subset of other csets.  Elements added to a cset are automatically
+        added to its supersets.  Cycles in the subset constraints are supported.
+    """
+    def __init__(self, xs):
+        self.supersets = set()
+        self.elements = set(xs)
+    def subsetof(self, other):
+        if other is not self:
+            self.supersets.add(other)
+            other.update(self.elements)
+    def update(self, xs):
+        xs = set(xs) - self.elements
+        if xs:      # xs will eventually be empty in case of a cycle
+            self.elements.update(xs)
+            for s in self.supersets:
+                s.update(xs)
+    def __iter__(self):
+        return iter(self.elements)
+
+def concat(ls):
+    """ return the concatenation of a list of iterables """
+    res = []
+    for l in ls: res.extend(l)
+    return res
+
+
+
 class groups_implied(osv.osv):
     _inherit = 'res.groups'
+
+    def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
+        "computes the transitive closure of relation implied_ids"
+        memo = {}           # use a memo for performance and cycle avoidance
+        def computed_set(g):
+            if g not in memo:
+                memo[g] = cset(g.implied_ids)
+                for h in g.implied_ids:
+                    computed_set(h).subsetof(memo[g])
+            return memo[g]
+
+        res = {}
+        for g in self.browse(cr, 1, ids, context):
+            res[g.id] = map(int, computed_set(g))
+        return res
+
     _columns = {
         'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
             string='Inherits', help='Users of this group automatically inherit those groups'),
+        'trans_implied_ids': fields.function(_get_trans_implied,
+            type='many2many', relation='res.groups', string='Transitively inherits'),
     }
 
-    def get_closure(self, cr, uid, ids, context=None):
-        "return the closure of ids, i.e., all groups recursively implied by ids"
-        closure = set()
-        todo = self.browse(cr, 1, ids)
-        while todo:
-            g = todo.pop()
-            if g.id not in closure:
-                closure.add(g.id)
-                todo.extend(g.implied_ids)
-        return list(closure)
-
     def create(self, cr, uid, values, context=None):
         users = values.pop('users', None)
         gid = super(groups_implied, self).create(cr, uid, values, context)
@@ -557,27 +615,13 @@
     def write(self, cr, uid, ids, values, context=None):
         res = super(groups_implied, self).write(cr, uid, ids, values, context)
         if values.get('users') or values.get('implied_ids'):
-            # add implied groups (to all users of each group)
+            # add all implied groups (to all users of each group)
             for g in self.browse(cr, uid, ids):
-                gids = self.get_closure(cr, uid, [g.id], context)
-                users = [(4, u.id) for u in g.users]
-                super(groups_implied, self).write(cr, uid, gids, {'users': users}, context)
+                gids = map(int, g.trans_implied_ids)
+                vals = {'users': [(4, u.id) for u in g.users]}
+                super(groups_implied, self).write(cr, uid, gids, vals, context)
         return res
 
-    def get_maximal(self, cr, uid, ids, context=None):
-        "return the maximal element among the group ids"
-        max_set, max_closure = set(), set()
-        for gid in ids:
-            if gid not in max_closure:
-                closure = set(self.get_closure(cr, uid, [gid], context))
-                max_set -= closure          # remove implied groups from max_set
-                max_set.add(gid)            # gid is maximal
-                max_closure |= closure      # update closure of max_set
-        if len(max_set) > 1:
-            log = logging.getLogger('res.groups')
-            log.warning('Groups %s are maximal among %s, only one expected.', max_set, ids)
-        return bool(max_set) and max_set.pop()
-
 groups_implied()
 
 class users_implied(osv.osv):
@@ -597,13 +641,10 @@
         res = super(users_implied, self).write(cr, uid, ids, values, context)
         if values.get('groups_id'):
             # add implied groups for all users
-            groups_obj = self.pool.get('res.groups')
-            for u in self.browse(cr, uid, ids):
-                old_gids = map(int, u.groups_id)
-                new_gids = groups_obj.get_closure(cr, uid, old_gids, context)
-                if len(old_gids) != len(new_gids):
-                    values = {'groups_id': [(6, 0, new_gids)]}
-                    super(users_implied, self).write(cr, uid, [u.id], values, context)
+            for user in self.browse(cr, uid, ids):
+                gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
+                vals = {'groups_id': [(4, g.id) for g in gs]}
+                super(users_implied, self).write(cr, uid, [user.id], vals, context)
         return res
 
 users_implied()
@@ -613,56 +654,16 @@
 #
 # Extension of res.groups and res.users for the special groups view in the users
 # form.  This extension presents groups with selection and boolean widgets:
-# - Groups named as "App/Name" (corresponding to root menu "App") are presented
-#   per application, with one boolean and selection field each.  The selection
-#   field defines a role "Name" for the given application.
-# - Groups named as "Stuff/Name" are presented as boolean fields and grouped
-#   under sections "Stuff".
-# - The remaining groups are presented as boolean fields and grouped in a
+# - Groups are shown by application, with boolean and/or selection fields.
+#   Selection fields typically defines a role "Name" for the given application.
+# - Uncategorized groups are presented as boolean fields and grouped in a
 #   section "Others".
 #
-
-class groups_view(osv.osv):
-    _inherit = 'res.groups'
-
-    def get_classified(self, cr, uid, context=None):
-        """ classify all groups by prefix; return a pair (apps, others) where
-            - both are lists like [("App", [("Name", browse_group), ...]), ...];
-            - apps is sorted in menu order;
-            - others are sorted in alphabetic order;
-            - groups not like App/Name are at the end of others, under _('Others')
-        """
-        # sort groups by implication, with implied groups first
-        groups = self.browse(cr, uid, self.search(cr, uid, []), context)
-        groups.sort(key=lambda g: set(self.get_closure(cr, uid, [g.id], context)))
-        
-        # classify groups depending on their names
-        classified = {}
-        for g in groups:
-            # split() returns 1 or 2 elements, so names[-2] is prefix or None
-            names = [None] + [s.strip() for s in g.name.split('/', 1)]
-            classified.setdefault(names[-2], []).append((names[-1], g))
-        
-        # determine the apps (that correspond to root menus, in order)
-        menu_obj = self.pool.get('ir.ui.menu')
-        menu_ids = menu_obj.search(cr, uid, [('parent_id','=',False)], context={'ir.ui.menu.full_list': True})
-        apps = []
-        for m in menu_obj.browse(cr, uid, menu_ids, context):
-            if m.name in classified:
-                # application groups are already sorted by implication
-                apps.append((m.name, classified.pop(m.name)))
-        
-        # other groups
-        others = sorted(classified.items(), key=lambda pair: pair[0])
-        if others and others[0][0] is None:
-            others.append((_('Others'), others.pop(0)[1]))
-        for sec, groups in others:
-            groups.sort(key=lambda pair: pair[0])
-        
-        return (apps, others)
-
-groups_view()
-
+# The user form view is modified by an inherited view (base.user_groups_view);
+# the inherited view replaces the field 'groups_id' by a set of reified group
+# fields (boolean or selection fields).  The arch of that view is regenerated
+# each time groups are changed.
+#
 # Naming conventions for reified groups fields:
 # - boolean field 'in_group_ID' is True iff
 #       ID is in 'groups_id'
@@ -678,155 +679,208 @@
 def is_boolean_group(name): return name.startswith('in_group_')
 def is_boolean_groups(name): return name.startswith('in_groups_')
 def is_selection_groups(name): return name.startswith('sel_groups_')
-def is_field_group(name):
+def is_reified_group(name):
     return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
 
 def get_boolean_group(name): return int(name[9:])
 def get_boolean_groups(name): return map(int, name[10:].split('_'))
 def get_selection_groups(name): return map(int, name[11:].split('_'))
 
-def encode(s): return s.encode('utf8') if isinstance(s, unicode) else s
 def partition(f, xs):
     "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
     yes, nos = [], []
     for x in xs:
-        if f(x):
-            yes.append(x)
-        else:
-            nos.append(x)
+        (yes if f(x) else nos).append(x)
     return yes, nos
 
+
+
+class groups_view(osv.osv):
+    _inherit = 'res.groups'
+
+    # this defines a domain for searching all groups in get_groups_by_application()
+    groups_by_application_domain = []
+
+    def create(self, cr, uid, values, context=None):
+        res = super(groups_view, self).create(cr, uid, values, context)
+        self.update_user_groups_view(cr, uid, context)
+        return res
+
+    def write(self, cr, uid, ids, values, context=None):
+        res = super(groups_view, self).write(cr, uid, ids, values, context)
+        self.update_user_groups_view(cr, uid, context)
+        return res
+
+    def unlink(self, cr, uid, ids, context=None):
+        res = super(groups_view, self).unlink(cr, uid, ids, context)
+        self.update_user_groups_view(cr, uid, context)
+        return res
+
+    def update_user_groups_view(self, cr, uid, context=None):
+        # the view with id 'base.user_groups_view' inherits the user form view,
+        # and introduces the reified group fields
+        view = self.get_user_groups_view(cr, uid, context)
+        if view:
+            xml = u"""<?xml version="1.0" encoding="utf-8"?>
+<!-- GENERATED AUTOMATICALLY BY GROUPS -->
+<field name="groups_id" position="replace">
+%s
+%s
+</field>
+"""
+            xml1, xml2 = [], []
+            xml1.append('<separator string="%s" colspan="4"/>' % _('Applications'))
+            for app, kind, gs in self.get_groups_by_application(cr, uid, context):
+                if kind == 'selection':
+                    # application name with a selection field
+                    field_name = name_selection_groups(map(int, gs))
+                    xml1.append('<field name="%s"/>' % field_name)
+                    xml1.append('<newline/>')
+                else:
+                    # application separator with boolean fields
+                    app_name = app and app.name or _('Other')
+                    xml2.append('<separator string="%s" colspan="4"/>' % app_name)
+                    for g in gs:
+                        field_name = name_boolean_group(g.id)
+                        xml2.append('<field name="%s"/>' % field_name)
+            view.write({'arch': xml % ('\n'.join(xml1), '\n'.join(xml2))})
+        return True
+
+    def get_user_groups_view(self, cr, uid, context=None):
+        try:
+            view = self.pool.get('ir.model.data').get_object(cr, 1, 'base', 'user_groups_view', context)
+            assert view and view._table_name == 'ir.ui.view'
+        except Exception:
+            view = False
+        return view
+
+    def get_groups_by_application(self, cr, uid, context=None):
+        """ return all groups classified by application (module category), as a list of pairs:
+                [(app, kind, [group, ...]), ...],
+            where app and group are browse records, and kind is either 'boolean' or 'selection'.
+            Applications are given in sequence order.  If kind is 'selection', the groups are
+            given in reverse implication order.
+        """
+        def linearized(gs):
+            gs = set(gs)
+            # determine sequence order: a group should appear after its implied groups
+            order = dict.fromkeys(gs, 0)
+            for g in gs:
+                for h in gs.intersection(g.trans_implied_ids):
+                    order[h] -= 1
+            # check whether order is total, i.e., sequence orders are distinct
+            if len(set(order.itervalues())) == len(gs):
+                return sorted(gs, key=lambda g: order[g])
+            return None
+
+        # classify all groups by application
+        gids = self.search(cr, uid, self.groups_by_application_domain)
+        by_app, others = {}, []
+        for g in self.browse(cr, uid, gids, context):
+            if g.category_id:
+                by_app.setdefault(g.category_id, []).append(g)
+            else:
+                others.append(g)
+        # build the result
+        res = []
+        apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
+        for app in apps:
+            gs = linearized(by_app[app])
+            if gs:
+                res.append((app, 'selection', gs))
+            else:
+                res.append((app, 'boolean', by_app[app]))
+        if others:
+            res.append((False, 'boolean', others))
+        return res
+
+groups_view()
+
 class users_view(osv.osv):
     _inherit = 'res.users'
 
-    def _process_values_groups(self, cr, uid, values, context=None):
-        """ transform all reified group fields into a 'groups_id', adding 
-            also the implied groups """
-        add, rem = [], []
-        for k in values.keys():
-            if is_boolean_group(k):
-                if values.pop(k):
-                    add.append(get_boolean_group(k))
-                else:
-                    rem.append(get_boolean_group(k))
-            elif is_boolean_groups(k):
-                if not values.pop(k):
-                    rem.extend(get_boolean_groups(k))
-            elif is_selection_groups(k):
-                gid = values.pop(k)
-                if gid:
-                    rem.extend(get_selection_groups(k))
-                    add.append(gid)
-        if add or rem:
-            # remove groups in 'rem' and add groups in 'add'
-            gdiff = [(3, id) for id in rem] + [(4, id) for id in add]
-            values.setdefault('groups_id', []).extend(gdiff)
-        return True
-
     def create(self, cr, uid, values, context=None):
-        self._process_values_groups(cr, uid, values, context)
+        self._set_reified_groups(values)
         return super(users_view, self).create(cr, uid, values, context)
 
     def write(self, cr, uid, ids, values, context=None):
-        self._process_values_groups(cr, uid, values, context)
+        self._set_reified_groups(values)
         return super(users_view, self).write(cr, uid, ids, values, context)
 
+    def _set_reified_groups(self, values):
+        """ reflect reified group fields in values['groups_id'] """
+        if 'groups_id' in values:
+            # groups are already given, ignore group fields
+            for f in filter(is_reified_group, values.iterkeys()):
+                del values[f]
+            return
+
+        add, remove = [], []
+        for f in values.keys():
+            if is_boolean_group(f):
+                target = add if values.pop(f) else remove
+                target.append(get_boolean_group(f))
+            elif is_boolean_groups(f):
+                if not values.pop(f):
+                    remove.extend(get_boolean_groups(f))
+            elif is_selection_groups(f):
+                remove.extend(get_selection_groups(f))
+                selected = values.pop(f)
+                if selected:
+                    add.append(selected)
+        # remove groups in 'remove' and add groups in 'add'
+        values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
+
+    def default_get(self, cr, uid, fields, context=None):
+        group_fields, fields = partition(is_reified_group, fields)
+        fields1 = (fields + ['groups_id']) if group_fields else fields
+        values = super(users_view, self).default_get(cr, uid, fields1, context)
+        self._get_reified_groups(group_fields, values)
+        return values
+
     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
         if not fields:
-            group_fields, fields = [], self.fields_get(cr, uid, context=context).keys()
-        else:
-            group_fields, fields = partition(is_field_group, fields)
-        if group_fields:
-            group_obj = self.pool.get('res.groups')
-            fields.append('groups_id')
-            # read the normal fields (and 'groups_id')
-            res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
-            records = res if isinstance(res, list) else [res]
-            for record in records:
-                # get the field 'groups_id' and insert the group_fields
-                groups = set(record['groups_id'])
-                for f in group_fields:
-                    if is_boolean_group(f):
-                        record[f] = get_boolean_group(f) in groups
-                    elif is_boolean_groups(f):
-                        record[f] = not groups.isdisjoint(get_boolean_groups(f))
-                    elif is_selection_groups(f):
-                        selected = groups.intersection(get_selection_groups(f))
-                        record[f] = group_obj.get_maximal(cr, uid, selected, context=context)
-            return res
-        return super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
-
-    def fields_get(self, cr, user, allfields=None, context=None, write_access=True):
-        res = super(users_view, self).fields_get(cr, user, allfields, context, write_access)
-        apps, others = self.pool.get('res.groups').get_classified(cr, user, context)
-        for app, groups in apps:
-            ids = [g.id for name, g in groups]
-            app_name = name_boolean_groups(ids)
-            sel_name = name_selection_groups(ids)
-            selection = [(g.id, name) for name, g in groups]
-            res[app_name] = {'type': 'boolean', 'string': app}
-            tips = [name + ': ' + (g.comment or '') for name, g in groups]
-            if tips:
-                res[app_name].update(help='\n'.join(tips))
-            res[sel_name] = {'type': 'selection', 'string': 'Group', 'selection': selection}
-
-        for sec, groups in others:
-            for gname, g in groups:
-                name = name_boolean_group(g.id)
-                res[name] = {'type': 'boolean', 'string': gname}
-                if g.comment:
-                    res[name].update(help=g.comment)
+            fields = self.fields_get(cr, uid, context=context).keys()
+        group_fields, fields = partition(is_reified_group, fields)
+        fields.append('groups_id')
+        res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
+        for values in (res if isinstance(res, list) else [res]):
+            self._get_reified_groups(group_fields, values)
         return res
 
-    def fields_view_get(self, cr, uid, view_id=None, view_type='form',
-                context=None, toolbar=False, submenu=False):
-        # in form views, transform 'groups_id' into reified group fields
-        res = super(users_view, self).fields_view_get(cr, uid, view_id, view_type,
-                context, toolbar, submenu)
-        if view_type == 'form':
-            root = etree.fromstring(encode(res['arch']))
-            nodes = root.xpath("//field[@name='groups_id']")
-            if nodes:
-                # replace node by the reified group fields
-                fields = res['fields']
-                elems = []
-                apps, others = self.pool.get('res.groups').get_classified(cr, uid, context)
-                # create section Applications
-                elems.append('<separator colspan="6" string="%s"/>' % _('Applications'))
-                for app, groups in apps:
-                    ids = [g.id for name, g in groups]
-                    app_name = name_boolean_groups(ids)
-                    sel_name = name_selection_groups(ids)
-                    selection = [(g.id, name) for name, g in groups]
-                    fields[app_name] = {'type': 'boolean', 'string': app}
-                    tips = [name + ': ' + (g.comment or '') for name, g in groups]
-                    if tips:
-                        fields[app_name].update(help='\n'.join(tips))
-                    fields[sel_name] = {'type': 'selection', 'string': 'Group', 'selection': selection}
-                    attrs = {'invisible': [('%s' % app_name, '=', False)]}
-                    elems.append("""
-                        <field name="%(app)s"/>
-                        <field name="%(sel)s" nolabel="1" colspan="2"
-                            attrs=%(attrs)s modifiers=%(json_attrs)s/>
-                        <newline/>
-                        """ % {'app': app_name, 'sel': sel_name,
-                               'attrs': quoteattr(str(attrs)),
-                               'json_attrs': quoteattr(simplejson.dumps(attrs))})
-                # create other sections
-                for sec, groups in others:
-                    elems.append('<separator colspan="6" string="%s"/>' % sec)
-                    for gname, g in groups:
-                        name = name_boolean_group(g.id)
-                        fields[name] = {'type': 'boolean', 'string': gname}
-                        if g.comment:
-                            fields[name].update(help=g.comment)
-                        elems.append('<field name="%s"/>' % name)
-                    elems.append('<newline/>')
-                # replace xml node by new arch
-                new_node = etree.fromstring('<group col="6">' + ''.join(elems) + '</group>')
-                for node in nodes:
-                    node.getparent().replace(node, new_node)
-                res['arch'] = etree.tostring(root)
+    def _get_reified_groups(self, fields, values):
+        """ compute the given reified group fields from values['groups_id'] """
+        gids = set(values.get('groups_id') or [])
+        for f in fields:
+            if is_boolean_group(f):
+                values[f] = get_boolean_group(f) in gids
+            elif is_boolean_groups(f):
+                values[f] = not gids.isdisjoint(get_boolean_groups(f))
+            elif is_selection_groups(f):
+                selected = [gid for gid in get_selection_groups(f) if gid in gids]
+                values[f] = selected and selected[-1] or False
+
+    def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
+        res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
+        # add reified groups fields
+        for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
+            if kind == 'selection':
+                # selection group field
+                tips = ['%s: %s' % (g.name, g.comment or '') for g in gs]
+                res[name_selection_groups(map(int, gs))] = {
+                    'type': 'selection',
+                    'string': app and app.name or _('Other'),
+                    'selection': [(g.id, g.name) for g in gs],
+                    'help': '\n'.join(tips),
+                }
+            else:
+                # boolean group fields
+                for g in gs:
+                    res[name_boolean_group(g.id)] = {
+                        'type': 'boolean',
+                        'string': g.name,
+                        'help': g.comment,
+                    }
         return res
 
 users_view()

=== modified file 'openerp/addons/base/security/base_security.xml'
--- openerp/addons/base/security/base_security.xml	2011-11-22 13:11:10 +0000
+++ openerp/addons/base/security/base_security.xml	2011-12-13 14:13:37 +0000
@@ -4,36 +4,37 @@
 
 <!--
  Users Groups
+    [Note] Field 'category_id' is set later in base/module/module_data.xml
 -->
     <record model="res.groups" id="group_erp_manager">
-        <field name="name">Administration / Access Rights</field>
+        <field name="name">Access Rights</field>
     </record>
     <record model="res.groups" id="group_system">
-        <field name="name">Administration / Configuration</field>
+        <field name="name">Configuration</field>
         <field name="implied_ids" eval="[(4, ref('group_erp_manager'))]"/>
     </record>
 
     <record model="res.groups" id="group_user">
-        <field name="name">Human Resources / Employee</field>
+        <field name="name">Employee</field>
     </record>
 
     <record model="res.groups" context="{'noadmin':True}" id="group_multi_company">
-        <field name="name">Useability / Multi Companies</field>
+        <field name="name">Multi Companies</field>
     </record>
 
     <record model="res.groups" context="{'noadmin':True}" id="group_extended">
-        <field name="name">Useability / Extended View</field>
+        <field name="name">Extended View</field>
     </record>
 
     <record model="res.groups" id="group_no_one" context="{'noadmin':True}">
-        <field name="name">Useability / Technical Features</field>
+        <field name="name">Technical Features</field>
     </record>
 
     <record id="group_sale_salesman" context="{'noadmin':True}" model="res.groups">
-        <field name="name">Sales / User</field>
+        <field name="name">User</field>
     </record>
     <record id="group_sale_manager" context="{'noadmin':True}" model="res.groups">
-        <field name="name">Sales / Manager</field>
+        <field name="name">Manager</field>
         <field name="implied_ids" eval="[(4, ref('group_sale_salesman'))]"/>
     </record>
 

_______________________________________________
Mailing list: https://launchpad.net/~openerp-dev-gtk
Post to     : [email protected]
Unsubscribe : https://launchpad.net/~openerp-dev-gtk
More help   : https://help.launchpad.net/ListHelp

Reply via email to