22. Desarrollando con Odoo (17). ORM de atributos many2many y one2many. Arreglando el módulo hide_menu_partner de Cybrosys

0. Manejar atributos many2many y one2many por ORM

Supongamos que tenemos :
  • un registro existente llamado "record" con un atributo llamado "m2m_padre" tipo many2many y otro atributo "o2m_padre" de tipo one3many
  • otro registro existente llamado "related" con su atributo "id" que es que forma parte de las relaciones anterior, y que tiene el atributo "m2m_hijo" many2many que engancha con "m2m_padre" y "m2o_hijo" que engancha con "o2m_padre"

Veamos como hacemos las operaciones CRUD

1. Vincular el registro related a la relación many2many

record.m2m = [ ( 4, related.id ) ]


2. Vincular el registro related a la relación one2many

La única manera es vincularlo desde el hijo related.

related.m2o_hijo = record


3. Desvincular el registro related de la relación many2many

record.m2m = [ ( 3related.id ) ] 


4. Desvincular el registro related a la relación one2many

La única manera es desvincularlo desde el hijo related.

related.m2o_hijo = another_record


5. Otras opciones que yo no recomiendo

# 1. Valores posibles de "_"
#    0 o False si está en medio (2ª posición)
#    Eliminar  si está al final (3ª posición)

# 2. No recomendables
    (0, _ , values) adds a new record created from the provided value dict.
    (1, id, values) updates an existing record of id id with the values in values. Can not be used in create().
    (2, id, _     ) removes the record of id id from the set, then deletes it (from the database). Can not be used in create().
    (5, _ , _     ) removes all records from the set, equivalent to using the command 3 on every record explicitly. Can not be used on One2many. Can not be used in create().
    (6, _ , ids   ) replaces all existing records in the set by the ids list, equivalent to using the command 5 followed by a command 4 for each id in ids.


# Hemo visto;
    (3, id, _     ) removes the record of id id from the set, but does not delete it. Can not be used on One2many. Can not be used in create().
    (4, id, _     ) adds an existing record of id id to the set. Can not be used on One2many.


1. Recordatorio de la relación many2many

# TodoTask class: Task <-> Tag relation (long form):
tag_ids = fields.Many2many(
    comodel_name='todo.task.tag', # related model
    relation='todo_task_tag_rel', # relation table name
    column1='task_id',            # field for "this" record
    column2='tag_id',             # field for "other" record
    string='Tasks')

#---------------------------------------
# The inverse relation between tasks and tags can be implemented like this:
# class Tag(models.Model):
# _name = 'todo.task.tag'
# Tag class relation to Tasks:
#---------------------------------------
task_ids = fields.Many2many(
    comodel_name='todo.task',     # related model
    string='Tasks')


2. Arreglar el módulo hide_menu_partner de Cybrosys

Queremos mostrar:

  1. Una pestaña en los usarios que se vean los menús que son accessibles en base a los grupos a los que pertenec
  2. Un campo booleano calculado que diga si está oculto o no dicho menú al usuario. 
  3. Un botón de mostrar / Ocultar menú 

Para ello, vamos a: 

1. Separar los modelos y las vistas en ficheros individuales

2. Modificar el modelo RestrictMenu:

  • Añadir el campo calculado is_user_hidden, de tipo booleano que indicará si el menú está oculto al usuario.
  • Añadir la función de cálculo de dicho campo _get_is_user_hidden 
  • Añadir la función de ocultación de un menú a un usuario _do_hide_this_menu
  • Añadir la función de des-ocultación de un menú a un usuario _do_show_this_menu

3. Modificar el modelo HideMenuUser:

  • Añadir el campo calculado all_user_menus_ids, de tipo one2many calculado para recoger los menús que son accessibles por el ususario por su pertenencia al grupo
  • Añadir la función de cálculo de dicho campo _get_all_user_menus 

4. Modificar la vista de HideMenuUser:

  • Añadiendo la vista de RestrictMenus en una pestaña indicando si está oculto el menú o no
  • Añadir boton de ocultar / mostrar a cada RestrictMenu
  • Añadir una pestaña para mostrar los grupos a los que pertenece el usuario (peligrosa)

2.1 Separar vistas y modelos

Ya se vió en apartados anteriores.

Para las vistas, hay que añadir cada fichero al __manifest__.py que cuelga de la carpeta hide_menu_user modificando el apartado data:

'data': [
        #'views/res_users.xml',
        'views/hide_menu_user.xml',
        'views/restrict_menu.xml',
        'security/security.xml'
    ],

Para los modelos, hay que añadir cada fichero al __init__.py que cuelga de la carpeta hide_menu_user/models 

#from . import res_user
from . import hide_menu_user
from . import restrict_menu


2.2 Modificar el modelo RestrictMenu

2.2.1 Crear un campo calculado "is_user_hidden" y su función de calculo leyendo el contexto.

El objetivo es obtener desde el context el usuario que ha llamado al form (variable "current_user"). Si este usuario se encuentra en la colección restrict_user_ids del menu entonces estará oculto. 

Para ello nos cremos el campo calculado "is_user_hidden" y una función de cálculo "get_is_user_hidden":

Para ver si el usuario tiene el menú oculto, basta con ver si el "id" del usuario se puede localizar en el campo "id" de todos los usuarios que se encuentran en la relación restrict_user_ids

    def _get_is_user_hidden(self):
        users= self.browse(self._context.get('current_user'))
        user=False
        if (users) and len(users)>0:
            user=users[0]
        
        for record in self:
            isHidden=False;
            if user:
                _logger.info(record.name + "  " + str(record.restrict_user_ids))
                if (record.restrict_user_ids and len(record.restrict_user_ids)>0):
                    if any(x.id == user.id for x in record.restrict_user_ids):
                        isHidden=True
                        _logger.info("      isHidden=True")
            
            record.is_user_hidden=isHidden

    is_user_hidden =fields.Boolean(string="Oculto al Usuario", compute="_get_is_user_hidden", store=False)


2.2.2 Crear las funciones de los botones de ocultar/mostrar menu

Creamos las funciones de dichos botones:

Para ocultar un menú, el usuario del contexto se incluirá en el campo restrict_user_ids solo si NO estaba incluido. Debemos recordar que para vincular el id de usuario a la relación utilizabamos la opción 4

Para mostrar el menú, el usuario del contexto se eliminará del campo restrict_user_ids solo si estaba incluido.Debemos recordar que para desvincular el id de usuario a la relación utilizabamos la opción 3
 

    def do_hide_this_menu(self):
        users= self.browse(self._context.get('current_user')) ; userName='No User' ; userId=-1000
        if (users) and len(users)>0:
            user=users[0] ; userName=user.name ; userId=user.id
            for record in self:
                toHide=False
                if user:
                    if (not record.restrict_user_ids or len(record.restrict_user_ids)==0):
                        toHide=True
                    else:
                        if not any(x.id == userId for x in record.restrict_user_ids):
                            toHide=True
                if toHide:
                    record.restrict_user_ids = [(4,user.id)]  # 4= Link
                    _logger.info('1. User.id=' + str(userId)+ ' user.name=' + userName + ' in menu:' + record.name + '('+ str(record.id) +')' )
                else:    
                    _logger.warn('2. User '+userName + '('+ str(userId) +') already hidden in menu:' + record.name + '('+ str(record.id) +')' )
        else:
             _logger.warn('3. User '+userName + '('+ str(userId) +') cannot be hidden in menu:' + record.name + '('+ str(record.id) +')' )            
            
    
    
    def do_show_this_menu(self):
        users= self.browse(self._context.get('current_user')) ; userName='No User' ; userId=-1000
        if (users) and len(users)>0:
            user=users[0] ; userName=user.name ; userId=user.id
            for record in self:
                toShow=False
                if user:
                    if (record.restrict_user_ids and len(record.restrict_user_ids)>0):
                        if any(x.id == userId for x in record.restrict_user_ids):
                            toShow=True
                        
                    if toShow:
                        record.restrict_user_ids = [(3,user.id)] # 3 = Unlink   
                        _logger.info('2. do_show_this_menu from restrict_menu menu.id='+ str(record.id) + " menu.name="+ record.name + " user id="+ str(user.id) + ' user.name= ' + user.name  )
                    else:    
                        _logger.warn('2. User '+userName + '('+ str(userId) +') already showed in menu:' + record.name + '('+ str(record.id) +')' )    
        else:
             _logger.warn('4. No user specified to hide menu '   )            
  


Al final el modelo entero RestrictMenu.py quedaría:

from odoo import models, fields, api
import logging

_logger = logging.getLogger(__name__) # Recoge la información del fichero "odoo.conf"

class RestrictMenu(models.Model):
    _inherit = 'ir.ui.menu'

    restrict_user_ids = fields.Many2many('res.users')


    #---- Ximo BEGIN CODE -----
    def _get_is_user_hidden(self):
        users= self.browse(self._context.get('current_user'))
        user=False
        if (users) and len(users)>0:
            user=users[0]
        
        for record in self:
            isHidden=False;
            if user:
                _logger.info(record.name + "  " + str(record.restrict_user_ids))
                if (record.restrict_user_ids and len(record.restrict_user_ids)>0):
                    if any(x.id == user.id for x in record.restrict_user_ids):
                        isHidden=True
                        _logger.info("      isHidden=True")
            
            record.is_user_hidden=isHidden

    is_user_hidden =fields.Boolean(string="Oculto al Usuario", compute="_get_is_user_hidden", store=False)

    
    # -- Action forn hide menu botton
    def do_hide_this_menu(self):
        users= self.browse(self._context.get('current_user')) ; userName='No User' ; userId=-1000
        if (users) and len(users)>0:
            user=users[0] ; userName=user.name ; userId=user.id
            for record in self:
                toHide=False
                if user:
                    if (not record.restrict_user_ids or len(record.restrict_user_ids)==0):
                        toHide=True
                    else:
                        if not any(x.id == userId for x in record.restrict_user_ids):
                            toHide=True
                if toHide:
                    record.restrict_user_ids = [(4,user.id)]  # 4= Add
                    _logger.info('1. User.id=' + str(userId)+ ' user.name=' + userName + ' in menu:' + record.name + '('+ str(record.id) +')' )
                else:    
                    _logger.warn('2. User '+userName + '('+ str(userId) +') already hidden in menu:' + record.name + '('+ str(record.id) +')' )
        else:
             _logger.warn('3. User '+userName + '('+ str(userId) +') cannot be hidden in menu:' + record.name + '('+ str(record.id) +')' )            
            
    
    # -- Action for show Menu button 
    def do_show_this_menu(self):
        users= self.browse(self._context.get('current_user')) ; userName='No User' ; userId=-1000
        if (users) and len(users)>0:
            user=users[0] ; userName=user.name ; userId=user.id
            for record in self:
                toShow=False
                if user:
                    if (record.restrict_user_ids and len(record.restrict_user_ids)>0):
                        if any(x.id == userId for x in record.restrict_user_ids):
                            toShow=True
                        
                    if toShow:
                        record.restrict_user_ids = [(3,user.id)] # 3 = Unlink   
                        _logger.info('2. do_show_this_menu from restrict_menu menu.id='+ str(record.id) + " menu.name="+ record.name + " user id="+ str(user.id) + ' user.name= ' + user.name  )
                    else:    
                        _logger.warn('2. User '+userName + '('+ str(userId) +') already showed in menu:' + record.name + '('+ str(record.id) +')' )    
        else:
             _logger.warn('4. No user specified to hide menu '   )            
    #---- Ximo END CODE-----       
    

2.3 Modificar el modelo HideMenuUser

2.2.1 Crear un campo calculado "all_user_menus_ids" y su función de cálculo.

Para ello buscamos los grupos a los que pertenece el usuario y después buscamos los menús cuyos grupos tengan alguno de los grupos a los que pertenece el usuario. Para no duplicar menús utilizamos un "set" o conjunto

    # INICI: Codi meu 
    @api.depends("groups_id")
    def _get_all_user_menus(self):
        for record in self:
            menuSet=set(())
            groups=self.env['res.groups'].search([('id', 'in', record.groups_id.ids)])
            for group in groups:
                menus=self.env["ir.ui.menu"].search([('groups_id', 'in', [group.id])])
                for menu in menus:
                    menuSet.add(menu.id)
            record.all_user_menus_ids =   list(menuSet)     
        
    all_user_menus_ids = fields.One2many(comodel_name='ir.ui.menu', compute="_get_all_user_menus", string='All user menus', store=False)
    # FI: Codi meu 

Al final HideMenuUser.py queda:

from odoo import models, fields, api
import logging

_logger = logging.getLogger(__name__) # Recoge la información del fichero "odoo.conf"


class HideMenuUser(models.Model):
    _inherit = 'res.users'

    @api.model_create_multi
    def create(self, vals_list):
        """
        Else the menu will be still hidden even after removing from the list
        """
        self.clear_caches()
        return super(HideMenuUser, self).create(vals_list)

    def write(self, vals):
        """
        Else the menu will be still hidden even after removing from the list
        """
        res = super(HideMenuUser, self).write(vals)
        for record in self:
            for menu in record.hide_menu_ids:
                menu.write({
                    'restrict_user_ids': [(4, record.id)]
                })
        self.clear_caches()
        return res
    def _get_is_admin(self):
        """
        The Hide specific menu tab will be hidden for the Admin user form.
        Else once the menu is hidden, it will be difficult to re-enable it.
        """
        for rec in self:
            rec.is_admin = False
            if rec.id == self.env.ref('base.user_admin').id:
                rec.is_admin = True
    
    
    
    hide_menu_ids = fields.Many2many('ir.ui.menu', string="Menu", store=True,
                                     help='Select menu items that needs to be '
                                          'hidden to this user ')
                                        
    is_admin = fields.Boolean(compute=_get_is_admin, string="Admin")

    # INICI: Codi meu 
    @api.depends("groups_id")
    def _get_all_user_menus(self):
        for record in self:
            menuSet=set(())
            #lstGroupIds=record.groups_id.ids
            groups=self.env['res.groups'].search([('id', 'in', record.groups_id.ids)])
            for group in groups:
                menus=self.env["ir.ui.menu"].search([('groups_id', 'in', [group.id])])
                for menu in menus:
                    menuSet.add(menu.id)
            record.all_user_menus_ids =   list(menuSet)     
        
    all_user_menus_ids = fields.One2many(comodel_name='ir.ui.menu', compute="_get_all_user_menus", string='All user menus', store=False)
    # FI: Codi meu 
    
    

2.4. Modificar la vista de HideMenuUser

El objetivo es pasar el usuario actual al context en el campo "all_user_menus_id".  Para ello  en el context nos creamos la variable "current_user" que guardará el usuario que se expresa como "active_id". Tambien metemos algunos campos adicionales y los botones de ocultar  y mostrar menús, too ello en negrita. Veamos todo el fichero hide_menu_user.xml:

<odoo>
    <data>
        <record id="hide_user_menu" model="ir.ui.view">
            <field name="name">hide.menu</field>
            <field name="model">res.users</field>
            <field name="inherit_id" ref="base.view_users_form"/>
            <field name="arch" type="xml">
                
                <xpath expr="//notebook" position="inside">
                    <page string="Hide Specific Menu"  attrs="{'invisible': [('is_admin','=', True)]}">
                        <field 
                                name="all_user_menus_ids"
                                context="{'current_user':active_id}"
                                readonly="False"
                            >  
                            <tree>
                                <field name= "name"/>
                                <field name= "is_user_hidden" />
                                <field name= "id" />
                                <button name="do_hide_this_menu" 
                                        type="object" 
                                        icon= "fa-eye-slash" 
                                        string="Hide this menu" 
                                        attrs="{'invisible': [('is_user_hidden','=', True)]}"/>
                                <button name="do_show_this_menu" 
                                        type="object" 
                                        icon= "fa-eye" 
                                        string="Show this menu" 
                                        attrs="{'invisible': [('is_user_hidden','=', False)]}"/>
                                
                            </tree>
                        </field>    
                    </page>
                </xpath>
                <xpath expr="//notebook" position="inside">
                    <!--
                    <page string="My groups"  attrs="{'invisible': [('is_admin','=', True)]}">
                    -->
                    <page string="My groups Ximo DANGEROUS !!!!!!" >
                        <tree>
                            <field name="groups_id"/>
                                <!--   context="{'form_view_ref':'ir_ui_menu_users', 'current_user':active_id}"> -->
                        </tree>
                    </page>
                </xpath>
                <field name="name" position="after">
                    <field name="is_admin" invisible="1"/>
                </field>
            </field>
        </record>
    </data>
</odoo>    


2.5. Modificar la vista de RestrictMenuUser

A la vista de menús se le ha añadido los usuarios a los que se ha restringido el uso del menú. Al final el fichero restrict_menu_user.xml es:

<odoo>
    <data>
        <record model="ir.ui.view" id="ir_ui_menu_users_list">
            <field name="name">restrict.menu</field>
            <field name="model">ir.ui.menu</field>
            <field name="inherit_id" ref="base.edit_menu"/>
            <field name="arch" type="xml">
                <xpath expr="//field[@name='complete_name']" position="after">
                    <field name="is_user_hidden"/>
                </xpath>
            </field>
        </record>

        <record model="ir.ui.view" id="ir_ui_menu_users_form" >
            <field name="name">restrict.menu</field>
            <field name="model">ir.ui.menu</field>
            <field name="inherit_id" ref="base.edit_menu_access"/>
            <field name="arch" type="xml">
                <xpath expr="//notebook" position="inside">
                    <page string="Restrict users" name="restrict_users">
                        <tree>
                            <field name="restrict_user_ids"/>
                        </tree>
                    </page>
                </xpath>
            </field>
        </record>
    </data>
</odoo>











  


Comentarios

Entradas populares de este blog

4. Desarrollando con Odoo (2). Introducción a modulos, vistas, modelos .. Añadir dependencias externas de Python a Odoo

8. Desarrollando con Odoo (6). Herencia de clase en modelos. Herencia de vistas. Operaciones numeradas en Odoo ??

26. Desarrollando con Odoo (21). IMPORTANTE: Entorno de desarrollo. Recapitulaciones del capítulo 8 (Development, Test and Debug).