Published: 2016-07-05

Odoo Tips or Tricks

记录一些odoo相关的知识或技巧,供查阅借鉴。

Table of Contents

1 Form 表单里的 Button显示与否

可以通过 attrs 属性来设置,如下:

<button name="delete_message_button"
    string="删除群发"
    type="object"
    class="oe_inline oe_stat_button"
    icon="fa-refresh"
    attrs="{'invisible':['|',('state','not in',('send','finished')),('msgtype','not in',('mpnews', 'mpvideo'))]}"/>

但会跟 根据 state 来显示与否冲突

<button name="delete_message_button"
    string="删除群发"
    type="object"
    class="oe_inline oe_stat_button"
    states='draft'       <-- 差异
    icon="fa-refresh"
    attrs="{'invisible':['|',('state','not in',('send','finished')),('msgtype','not in',('mpnews', 'mpvideo'))]}"/>

上面例子里的 state 为表中的一个字段, 通常的模式是放到顶补做状态栏,然后按钮可以根据状态来是否显示。 当我需要同时根据状态和表中其它一个字段来控制是否显示时,就不能用 states='draft,send' 这种模式,会冲突导致 attrs 设置的属性无效。 Fuck so much patterns!

2 在列表视图的“动作按钮”添加动作, 可以单个或者批量操作

模式如下, 参考res.users 修改密码的模式 通常动作是弹出一个向导,所以需要先建立一个向导model

class change_password_wizard(osv.TransientModel):
    """
        A wizard to manage the change of users' passwords
    """

    _name = "change.password.wizard"
    _description = "Change Password Wizard"
    _columns = {
        'user_ids': fields.one2many('change.password.user', 'wizard_id', string='Users'),
    }

    def _default_user_ids(self, cr, uid, context=None):
        if context is None:
            context = {}
        user_model = self.pool['res.users']
        user_ids = context.get('active_model') == 'res.users' and context.get('active_ids') or []
        return [
            (0, 0, {'user_id': user.id, 'user_login': user.login})
            for user in user_model.browse(cr, uid, user_ids, context=context)
        ]

    _defaults = {
        'user_ids': _default_user_ids,
    }

根据实际需要建立几个模型(源代码处建立了两个,而我自己用的地方一个就够了)。 上述代码 _default_user_ids 方法得到选择的那些记录

XML 视图如下:

<!-- change password wizard -->
        <record id="change_password_wizard_view" model="ir.ui.view">
            <field name="name">Change Password</field>
            <field name="model">change.password.wizard</field>
            <field name="arch" type="xml">
                <form string="Change Password">
                    <field name="user_ids"/>
                    <footer>
                        <button string="Change Password" name="change_password_button" type="object" class="btn-primary"/>
                        <button string="Cancel" class="btn-default" special="cancel" />
                    </footer>
                </form>
            </field>
        </record>
        <record id="change_password_wizard_user_tree_view" model="ir.ui.view">
            <field name="name">Change Password Users</field>
            <field name="model">change.password.user</field>
            <field name="arch" type="xml">
                <!-- the user list is editable, but one cannot add or delete rows -->
                <tree string="Users" editable="bottom" create="false" delete="false">
                    <field name="user_login"/>
                    <field name="new_passwd" required="True" password="True"/>
                </tree>
            </field>
        </record>
        <act_window id="change_password_wizard_action"
            name="Change Password"
            src_model="res.users"
            res_model="change.password.wizard"
            view_type="form" view_mode="form"
            key2="client_action_multi" target="new"
            groups="base.group_erp_manager"/>

前两个record根据自己需要创建,主要是第三个, src_mode 指定在哪个mode上见示出来, res_model 指定弹出的wizard是哪个model的

在列表视图上需要钩选某些记录才能显示更多操作的按钮, 这时候往往需要将钩选记录的一些信息传入到弹出到Form中,需么从context中获得,模式如下:

# model file
# 移动选定到用户到其它组
class change_users_groups_wizard(osv.TransientModel):

    _name = "change.users.groups.wizard"
    _description = u"更改用户组的向导"

    @api.multi
    def _get_default_user_ids(self):

        # 新api获得context的写法
        context = dict(self._context or {})

        # 从环境中去除当前model和当前钩选的ids
        user_ids = context.get('active_model') == 'my.users' and context.get('active_ids') or []

        # 因为user_ids是many2many格式,所以这样返回, 如果是many2one 返回id
        return [(6, 0, user_ids)]

    user_ids = fields.Many2many('my.users',
                                string=u'要移动的用户',default= _get_default_user_ids)
    groups_id = fields.Many2one('my.users.groups', string=u'移动到组', help=u'将用户移动到哪个组')


3 上传图片时遇到报错 IOError: decoder jpeg not available

4 在Form表单上弹出向导

首先表单视图上添加动作按钮

<button name="get_user_data" string="获取用户数据" type="object" class="oe_inline oe_stat_button"
                                    icon="fa-strikethrough"/>

然后对应的model 下写这个按钮触发的方法

@api.multi
def get_user_data(self):

    return {
        'name': _(u'获取用户数据'),
        'view_type': 'form',
        'view_mode': 'form',
        'res_model': 'get.user.data.wizard',
        'view_id': False,
        'type': 'ir.actions.act_window',
        'target': 'new'
    }


这样会弹出定义的向导(此处是"get.user.data.wizard")定义好的form视图。

向导xml可以这样写

<record id="view_get_user_data_wizard_form" model="ir.ui.view">
            <field name="name">获取用户数据向导</field>
            <field name="model">get.user.data.wizard</field>
            <field name="arch" type="xml">
                <form string="自定义菜单">
                    <sheet>
                        <div class="oe_button_box" name="button_box">
                        </div>
                        <div class="oe_title">
                            <label for="app_id" class="oe_edit_only"/>
                            <h1>
                                <field name="app_id" options="{'no_create': True,'no_open': True}"/>
                            </h1>
                        </div>
                        <group>
                            <group>
                                <field name="company_id" groups="base.group_multi_company"/>
                                <field name="begin_date" />
                                <field name="end_date" />
                            </group>
                            <group>
                            </group>
                        </group>
                        <footer>
                            <button string="确认" name="get_userdata" type="object" class="btn-primary"/>
                            <button string="取消" class="btn-default" special="cancel" />
                        </footer>
                    </sheet>
                </form>
            </field>
        </record>

然后在该向导model中定义按钮方法写对应的逻辑就可以了

PS: 如果要在向导上获得点击向导前form的model或者id ,可以参照2

5 New API 根据一个(或者一些)字段值的改变,改变另外一些值

很简单,用~@api.onchange~

# 如果选择的type为url, 自动给content属性加上url前缀
@api.onchange('type')
def _onchange_app_id(self):
    if self.type == 'url':
        self.content = 'http://'
    return

有些时候,会发现这样自动生成的内容在完成表单后没有保存上, 可以在字段上加上~compute=onchangeappid~来告诉model保存这个值

6 New API 根据一个(或者一些)字段值的改变,筛选另外一些值(给另外一些值加上domain)

# 选择app_id筛选user_id
@api.onchange('app_id')
def _onchange_app_id(self):
    if self.app_id:

        users = self.env['my.users'].search([('app_id', '=', self.app_id.id)])
        user_ids = [user.id for user in users]

        return {'domain':{'user_id':[('id','in',user_ids)]}}
        # 如果是筛选多个字段,那么domain后多加字段筛选

7 字段fields.Date和fields.Datetime的读取和存储

可以参考这篇文章,讲述的很全: http://blog.sina.com.cn/s/blog_b09d460201018o0v.html

### 假设self.begin_date_time取出的是默认的字符串格式,DEFAULT_DATETIME_FORMAT是"Y-%m-%d %H:%M:%S"

# 字符串转化为time.struct_time
time.strptime(self.begin_date_time, DEFAULT_DATETIME_FORMAT)
struct_time类型可以用[0],[1]这样的index取出对应的year, month等等

# time.struct_time转化为浮点型的时间戳
time.mktime(time.struct_time)

# 时间戳转化成dateime.datetime类型
datetime.fromtimestamp(timestamp)

# struct_time 到字符串时间
time.strftime(DEFAULT_DATETIME_FORMAT, strcut_time)


8 Form表单里的提示性文字,在xml上的写法

可以用seperator

<separator style="font-size:13px;" colspan="4" string="注意: 如果不选择'发送的用户'将会默认发送给所有用户"/>

9 XML 设置字段的一些属性, 以及一些常用视图的写法

<!-- 设置对象不能创建,不能打开,一般用在many2one字段上 -->
<field name="company_id" options="{'no_create': True,'no_open': True}"/>




<!-- 设置在一定情况下隐藏或显示 -->
<field name="state" />
<field name="result_ids" string="发送的消息返回的结果"
                                 attrs="{'invisible':[('state','=','draft')]}" />





<!-- 在一定情况下必填 -->
<field name="send_type" />
<field name="group_id"  attrs="{'required':[('send_type','=','group')]}" />





<!-- 当用户属于某个组当时候才可见 -->
<field name="company_id" groups="base.group_multi_company"/>





<!-- 新建记录前, 页面当提示信息, 比如新建要注意当地方 -->
<!-- 写在action视图当最后一个字段 -->
<field name="help" type="html">
   <p>在公众平台网站上,为订阅号提供了每天一条的群发权限,为服务号提供每月(自然月)4条的群发权限。</p>
   <p>请注意</p>
</field>




<!-- 密码星号表示 -->
<field name="ldap_password" password="True"/>


<!-- default_focus 新开窗口光标位置 -->
<field name="name" default_focus="1"/>



<!-- digits 直接格式化浮点字段 -->
<field digits="(14, 3)" name="volume" />


<!-- 新建一个空行 -->
<newline/>



<!-- 常用的的Form格式 -->
<sheet>

  <!-- 如果页面上部分右边有些动作按钮,则向下面这样写 -->
  <div class="oe_button_box" name="button_box">
    <button name="test_button" string="测试" type="object" class="oe_inline oe_stat_button"
                                  icon="fa-refresh"/>
    <button name="sync_button" string="同步" type="object" class="oe_inline oe_stat_button"
                                  icon="fa-strikethrough"/>
  </div>

  <!-- 如果有图片 -->
  <field name="image" widget='image' class="oe_avatar"/>

  <!-- 为了美观Form视图标题栏,可以将标题放大,可以参考下面格式 -->
  <div class="oe_title">
    <label for="app_id" class="oe_edit_only"/>
       <h1>
         <field name="app_id" options="{'no_create': True,'no_open': True}"/>
       </h1>
  </div>
  <!-- other parts below-->
</sheet>





<!-- 一个常用的表单顶部有状态栏,和按钮改动状态的写法 -->
<form string="发送消息表单">
  <header>
    <button name="send_message_button" style="float:left;" string="发送信息"
      states="draft,failed" type="object" class="oe_highlight" />
    <field name="state" widget="statusbar" statusbar_visible="draft,send,failed,cancel" />
  </header>

   <sheet>
   <!-- other part -->
   </sheet>
</form>

<!-- One2Many 在one的一端显示many的记录-->
<field name="result_ids" string="发送的消息返回的结果" attrs="{'invisible':[('state','=','draft')]}">
  <form>
     <group>
       <field name="user_id"/>
       <field name="msgid"/>
       <field name="errmsg"/>
       <field name="errcode"/>
     </group>
  </form>
  <tree>
    <field name="user_id"/>
    <field name="msgid"/>
    <field name="errmsg"/>
    <field name="errcode"/>
  </tree>
</field>


10 XML one2many many2many类型字段widget格式

可以选用下面这些: one2onelist,one2manylist,many2onelist,many2many,url,email,image,floattime,reference,many2manytags, selection,handle

11 Form视图的Group属性

  • colspan 说明该控件占用父容器多少列 openerp form 为顶级容器,约定为 4 列
  • rowspan 行数
  • col 用于容器控件,如 group,它表示这个容器内部分几列
  • string 组的名称

12 Form表单上的不同按钮触发同一个向导,但向导但视图需要根据不同按钮做些调整

在触发向导的按钮上传入环境变量用来区分是哪个按钮。 Button 按钮视图

<button name="get_data1" string="获取数据1" type="object"
class="oe_inline oe_stat_button"  icon="fa-strikethrough"/>

<button name="get_data2" string="获取数据2" type="object"
class="oe_inline oe_stat_button" icon="fa-strikethrough"/>

对应model里的按钮方法

@api.multi
def get_data1(self):

    return {
        'name': _(u'获取数据1'),
        'view_type': 'form',
        'view_mode': 'form',
        'res_model': 'my.data.wizard', #向导的model
        'view_id': '',
        'type': 'ir.actions.act_window',
        'target': 'new',
        'context': {'wizard_code': '1'}
    }


# 获取图文群发每日数据
@api.multi
def get_data2(self):

    return {
        'name': _(u'获取数据2'),
        'view_type': 'form',
        'view_mode': 'form',
        'res_model': 'my.data.wizard',
        'view_id': '',
        'type': 'ir.actions.act_window',
        'target': 'new',
        'context': {'wizard_code': '2'}
    }

向导的model用一个额外的字段(比如叫'wizardcode')保存从环境变量获得的wizardcode

@api.model
def default_get(self, fields_list):
    res = super(my_data_wizard, self).default_get(fields_list)
    context = self._context or {}
    if context.get('wizard_code', False):
        res.update({"wizard_code": context.get('wizard_code')})

    return res

然后在视图将字段"wizardcode"隐藏,其它部分根据这个字段作相应变化

Done!

13 一个Bug,fields.Selection

ODOO的ORM模型有个转换bug,当用fields.Seletion字段是时,要避免元组的第一个不为0. 具体来说, 比如有两个字段

user_source = fields.Selection([('0', u'会话'), ('1', u'好友'), ('2', u'朋友圈'),('3', u'腾讯微博'),('4', u'历史消息'), ('5', u'其它')],
                                   string=u'用户的渠道',help=u'代表用户从哪里进入来阅读该图文')

user_source1 = fields.Selection([(0, u'会话'), (1, u'好友'), (2, u'朋友圈'),(3, u'腾讯微博'),(4, u'历史消息'), (5, u'其它')],
                                   string=u'用户的渠道',help=u'代表用户从哪里进入来阅读该图文')

当我将某条记录的usersource1 设为数字0时, 是无法存上“会话”这个选项的。 当我查看具体执行的sql语句时,发现insert对应的项是"NULL", ORM模型将我给它的值数字0转化成了NULL存储(我在one2many用(0,0,vals)插入时发现的)。 相反的,当我在数据库里,手动将这个记录这个值设为数字0时,在后台界面上也显示不出具体的值。

14 列表视图(Tree View)根据字段显示不同颜色

<record id="acade_activity_tree" model="ir.ui.view">
     <field name="name">acade_activity_tree</field>
     <field name="model">acade.activity</field>
     <field name="type">tree</field>
     <field name="arch" type="xml">
         <tree string="acade.activity.tree" colors="green:is_over==False;red:is_over==True;">
             <field name="title"/>
                 <field name="start_time"/>
             <field name="end_time"/>
             <field name="address"/>
             <field name="view_times"/>
             <field name="apply_doctors"/>
             <field name="chat_created"/>
             <field name="is_over"/>
         </tree>
     </field>
 </record>

重点在"colors"这个属性

15 复杂domain解析求值顺序

从右往左读取,读取到一个三元结构求值,再向左读取

16 Field 设置domain

is_permanent = fields.Boolean(u'是否是永久素材')

# domain 里跟常量比较的写法
field_b = fields.Many2one('a.model', domain=[('is_permanent', '=', True)])

# domain 里跟本model的字段比较
field_c = fields.Many2one('a.model', domain="[('is_permanent', '=', is_permanent)]")

Author: Nisen

Email: imnisen@163.com