

odoo wizard界面显示带复选框列表及勾选数据获取 - 授客
source link: https://www.cnblogs.com/shouke/p/17135887.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

odoo wizard界面显示带复选框列表及勾选数据获取
Odoo 14.0-20221212 (Community Edition)
如下图(非实际项目界面截图,仅用于介绍本文主题),打开记录详情页(form视图),点击某个按钮(图中的"选取ffers"按钮),弹出一个向导(wizard)界面,并将详情页中内联tree视图("Offers" Tab页)的列表记录展示到向导界面,且要支持复选框,用于选取目标记录,然执行目标操作。


详情页所属模型EstateProperty
class EstateProperty(models.Model):
_name = 'estate.property'
_description = 'estate property table'
# ... 略
offer_ids = fields.One2many("estate.property.offer", "property_id", string="PropertyOffer")
def action_do_something(self, args):
# do something
print(args)
Offers
Tab页Tree列表所属模型EstatePropertyOffer
class EstatePropertyOffer(models.Model):
_name = 'estate.property.offer'
_description = 'estate property offer'
# ... 略
property_id = fields.Many2one('estate.property', required=True)
代码组织结构
为了更好的介绍本文主题,下文给出了项目文件大致组织结构(为了让大家看得更清楚,仅保留关键文件)
odoo14
├─custom
│ ├─estate
│ │ │ __init__.py
│ │ │ __manifest__.py
│ │ │
│ │ ├─models
│ │ │ estate_property.py
│ │ │ estate_property_offer.py
│ │ │ __init__.py
│ │ │
│ │ ├─security
│ │ │ ir.model.access.csv
│ │ │
│ │ ├─static
│ │ │ │
│ │ │ └─src
│ │ │ │
│ │ │ └─js
│ │ │ list_renderer.js
│ │ │
│ │ ├─views
│ │ │ estate_property_offer_views.xml
│ │ │ estate_property_views.xml
│ │ │ webclient_templates.xml
│ │ │
│ │ └─wizards
│ │ demo_wizard.py
│ │ demo_wizard_views.xml
│ │ __init__.py
│ │
├─odoo
│ │ api.py
│ │ exceptions.py
│ │ ...略
│ │ __init__.py
│ │
│ ├─addons
│ │ │ __init__.py
│ ...略
...略
wizard简介
wizard(向导)通过动态表单描述与用户(或对话框)的交互会话。向导只是一个继承TransientModel
而非model
的模型。TransientModel
类扩展Model
并重用其所有现有机制,具有以下特殊性:
-
wizard记录不是永久的;它们在一定时间后自动从数据库中删除。这就是为什么它们被称为瞬态(transient)。
-
wizard可以通过关系字段(
many2one
或many2many
)引用常规记录或wizard记录,但常规记录不能通过many2one
字段引用wizard记录
注意:为了更清楚的表达本文主题,代码文件中部分代码已略去
wizard实现
odoo14\custom\estate\wizards\demo_wizard.py
实现版本1
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import logging
from odoo import models,fields,api
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class DemoWizard(models.TransientModel):
_name = 'demo.wizard'
_description = 'demo wizard'
property_id = fields.Many2one('estate.property', string='property')
offer_ids = fields.One2many(related='property_id.offer_ids')
def action_confirm(self):
'''选中记录后,点击确认按钮,执行的操作'''
#### 根据需要对获取的数据做相应处理
# ... 获取数据,代码略(假设获取的数据存放在 data 变量中)
record_ids = []
for id, value_dict in data.items():
record_ids.append(value_dict.get('data', {}).get('id'))
if not record_ids:
raise UserError('请选择记录')
self.property_id.action_do_something(record_ids)
return True
@api.model
def action_select_records_via_checkbox(self, args):
'''通过wizard窗口界面复选框选取记录时触发的操作
@params: args 为字典
'''
# ...存储收到的数据(假设仅存储data部分的数据),代码略
return True # 注意,执行成功则需要配合前端实现,返回True
@api.model
def default_get(self, fields_list):
'''获取wizard 窗口界面默认值,包括记录列表 #因为使用了@api.model修饰符,self为空记录集,所以不能通过self.fieldName = value 的方式赋值'''
res = super(DemoWizard, self).default_get(fields_list)
record_ids = self.env.context.get('active_ids') # 获取当前记录ID列表(当前记录详情页所属记录ID列表) # self.env.context.get('active_id') # 获取当前记录ID
property = self.env['estate.property'].browse(record_ids)
res['property_id'] = property.id
offer_ids = property.offer_ids.mapped('id')
res['offer_ids'] = [(6, 0, offer_ids)]
return res
-
注意,不能使用类属性来接收数据,因为类属性供所有对象共享,会相互影响,数据错乱。
-
action_select_records_via_checkbox
函数接收的args
参数,其类型为字典,形如以下,其中f412cde5-1e5b-408c-8fc0-1841b9f9e4de
为UUID,供web端使用,用于区分不同页面操作的数据,'estate.property.offer_3'
为供web端使用的记录ID,'data'
键值代表记录的数据,其id
键值代表记录在数据库中的主键id,context
键值代表记录的上下文。arg
数据格式为:{'uuid':{'recordID1':{'data': {}, 'context':{}}, 'recordID2': {'data': {}, 'context':{}}}}
{'f412cde5-1e5b-408c-8fc0-1841b9f9e4de': {'estate.property.offer_3': {'data': {'price': 30000, 'partner_id': {'context': {}, 'count': 0, 'data': {'display_name': 'Azure Interior, Brandon Freeman', 'id': 26}, 'domain': [], 'fields': {'display_name': {'type': 'char'}, 'id': {'type': 'integer'}}, 'id': 'res.partner_4', 'limit': 1, 'model': 'res.partner', 'offset': -1, 'ref': 26, 'res_ids': [], 'specialData': {}, 'type': 'record', 'res_id': 26}, 'validity': 7, 'date_deadline': '2022-12-30', 'status': 'Accepted', 'id': 21}, 'context': {'lang': 'en_US', 'tz': 'Europe/Brussels', 'uid': 2, 'allowed_company_ids': [1], 'params': {'action': 85, 'cids': 1, 'id': 41, 'menu_id': 70, 'model': 'estate.property', 'view_type': 'form'}, 'active_model': 'estate.property', 'active_id': 41, 'active_ids': [41], 'property_pk_id': 41}}}}
实现版本2
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import uuid
import logging
from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError, MissingError
_logger = logging.getLogger(__name__)
class DemoWizard(models.TransientModel):
_name = 'demo.wizard'
_description = 'demo wizard'
property_id = fields.Many2one('estate.property', string='property')
property_pk_id = fields.Integer(related='property_id.id') # 用于action_confirm中获取property
offer_ids = fields.One2many(related='property_id.offer_ids')
@api.model
def action_confirm(self, data:dict):
'''选中记录后,点击确认按钮,执行的操作'''
#### 根据需要对获取的数据做相应处理
record_ids = []
for id, value_dict in data.items():
record_ids.append(value_dict.get('data', {}).get('id'))
if not record_ids:
raise UserError('请选择记录')
property_pk_id = None
for id, value_dict in data.items():
property_pk_id = value_dict.get('context', {}).get('property_pk_id')
break
if not property_pk_id:
raise ValidationError('do something fail')
property = self.env['estate.property'].browse([property_pk_id]) # 注意,,所以,这里不能再通过self.property_id获取了
if property.exists():
property.action_do_something(record_ids)
else:
raise MissingError('do something fail:当前property记录(id=%s)不存在' % property_pk_id)
return True
@api.model
def default_get(self, fields_list):
'''获取wizard 窗口界面默认值,包括记录列表'''
res = super(DemoWizard, self).default_get(fields_list)
record_ids = self.env.context.get('active_ids')
property = self.env['estate.property'].browse(record_ids)
res['property_id'] = property.id
res['property_pk_id'] = property.id
offer_ids = property.offer_ids.mapped('id')
res['offer_ids'] = [(6, 0, offer_ids)]
return res
odoo14\custom\estate\wizards\__init__.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from . import demo_wizard
odoo14\custom\estate\__init__.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from . import models
from . import wizards
odoo14\custom\estate\wizards\demo_wizard_views.xml
实现版本1
对应demo_wizard.py
实现版本1
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<record id="demo_wizard_view_form" model="ir.ui.view">
<field name="name">demo.wizard.form</field>
<field name="model">demo.wizard</field>
<field name="arch" type="xml">
<form>
<field name="offer_ids">
<tree hasCheckBoxes="true" modelName="demo.wizard" modelMethod="action_select_records_via_checkbox" jsMethodOnModelMethodDone="enableActionConfirmButton()" jsMethodOnToggleCheckbox="disableActionConfirmButton()">
<field name="price" string="Price"/>
<field name="partner_id" string="partner ID"/>
<field name="validity" string="Validity(days)"/>
<field name="date_deadline" string="Deadline"/>
<button name="action_accept_offer" string="" type="object" icon="fa-check" attrs="{'invisible': [('status', 'in', ['Accepted','Refused'])]}"/>
<button name="action_refuse_offer" string="" type="object" icon="fa-times" attrs="{'invisible': [('status', 'in', ['Accepted','Refused'])]}"/>
<field name="status" string="Status"/>
</tree>
</field>
<footer>
<button name="action_confirm" type="object" string="确认(do something you want)" class="oe_highlight"/>
<button string="取消" class="oe_link" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_demo_wizard" model="ir.actions.act_window">
<field name="name">选取offers</field>
<field name="res_model">demo.wizard</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</odoo>
<tree hasCheckBoxes="true" modelName="demo.wizard" modelMethod="action_select_records_via_checkbox" jsMethodOnModelMethodDone="enableActionConfirmButton()" jsMethodOnToggleCheckbox="disableActionConfirmButton()">
hasCheckBoxes
设置"true"
,则显示复选框。以下属性皆在hasCheckBoxes
为"true"
的情况下起作用。modelName
点击列表复选框时,需要访问的模型名称,需要配合modelMethod
方法使用,缺一不可。可选modelMethod
点击列表复选框时,需要调用的模型方法,通过该方法收集列表勾选记录的数据。可选。jsMethodOnModelMethodDone
定义modelMethod
方法执行完成后,需要调用的javascript方法(注意,包括参数,如果没有参数则写成()
,形如jsMethod()
)。可选。jsMethodOnToggleCheckbox
定义点击列表复选框时需要调用的javascript方法,比modelMethod
优先执行(注意,包括参数,如果没有参数则写成()
,形如jsMethod()
)。可选。
以上参数同下文saveSelectionsToSessionStorage
参数可同时共存
如果需要将action绑定到指定模型指定视图的Action,可以在ir.actions.act_window
定义中添加binding_model_id
和binding_view_types
字段,如下:
<record id="action_demo_wizard" model="ir.actions.act_window">
<field name="name">选取offers</field>
<field name="res_model">demo.wizard</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<!-- 添加Action菜单 -->
<field name="binding_model_id" ref="estate.model_estate_property"/>
<field name="binding_view_types">form</field>
</record>

参考连接:https://www.odoo.com/documentation/14.0/zh_CN/developer/reference/addons/actions.html
实现版本2
对应demo_wizard.py
实现版本2
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<record id="demo_wizard_view_form" model="ir.ui.view">
<field name="name">demo.wizard.form</field>
<field name="model">demo.wizard</field>
<field name="arch" type="xml">
<form>
<field name="property_pk_id" invisible="1"/>
<field name="offer_ids" context="{'property_pk_id': property_pk_id}">
<tree string="List" hasCheckBoxes="true" saveSelectionsToSessionStorage="true">
<field name="price" string="Price"/>
<field name="partner_id" string="partner ID"/>
<field name="validity" string="Validity(days)"/>
<field name="date_deadline" string="Deadline"/>
<button name="action_accept_offer" string="" type="object" icon="fa-check" attrs="{'invisible': [('status', 'in', ['Accepted','Refused'])]}"/>
<button name="action_refuse_offer" string="" type="object" icon="fa-times" attrs="{'invisible': [('status', 'in', ['Accepted','Refused'])]}"/>
<field name="status" string="Status"/>
</tree>
</field>
<footer>
<button name="action_confirm" onclick="do_confirm_action('demo.wizard','action_confirm')" string="确认(do something you want)" class="oe_highlight"/>
<button string="取消" class="oe_link" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_demo_wizard" model="ir.actions.act_window">
<field name="name">选取offers</field>
<field name="res_model">demo.wizard</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</odoo>
saveSelectionsToSessionStorage
为"true"
则表示点击复选框时,将当前选取的记录存到浏览器sessionStorage
中,可选
odoo14\custom\estate\security\ir.model.access.csv
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
# ...略
access_demo_wizard_model,access_demo_wizard_model,model_demo_wizard,base.group_user,1,1,1,1
注意:wizard
模型也是需要添加模型访问权限配置的
复选框及勾选数据获取实现
大致思路通过继承web.ListRenderer
实现自定义ListRenderer,进而实现复选框展示及勾选数据获取。
odoo14\custom\estate\static\src\js\list_renderer.js
注意:之所以将uuid
函数定义在list_renderer.js
中,是为了避免因为js顺序加载问题,可能导致加载list_renderer.js
时找不到uuid
函数定义问题。
function uuid() {
var s = [];
var hexDigits = "0123456789abcdef";
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
s[8] = s[13] = s[18] = s[23] = "-";
var uuid = s.join("");
return uuid;
}
odoo.define('estate.ListRenderer', function (require) {
"use strict";
var ListRenderer = require('web.ListRenderer');
ListRenderer = ListRenderer.extend({
init: function (parent, state, params) {
this._super.apply(this, arguments);
this.hasCheckBoxes = false;
if ('hasCheckBoxes' in params.arch.attrs && params.arch.attrs['hasCheckBoxes']) {
this.objectID = uuid();
$(this).attr('id', this.objectID);
this.hasCheckBoxes = true;
this.hasSelectors = true;
this.records = {}; // 存放当前界面记录
this.recordsSelected = {}; // 存放选取的记录
this.modelName = undefined; // 定义点击列表复选框时需要访问的模型
this.modelMethod = undefined; // 定义点击列表复选框时需要调用的模型方法
this.jsMethodOnModelMethodDone = undefined; // 定义modelMethod方法执行完成后,需要调用的javascript方法
this.jsMethodOnToggleCheckbox = undefined; // 定义点击列表复选框时需要调用的javascript方法,比modelMethod优先执行
if ('modelName' in params.arch.attrs && params.arch.attrs['modelName']) {
this.modelName = params.arch.attrs['modelName'];
}
if ('modelMethod' in params.arch.attrs && params.arch.attrs['modelMethod']) {
this.modelMethod = params.arch.attrs['modelMethod'];
}
if ('jsMethodOnModelMethodDone' in params.arch.attrs && params.arch.attrs['jsMethodOnModelMethodDone']){
this.jsMethodOnModelMethodDone = params.arch.attrs['jsMethodOnModelMethodDone'];
}
if ('jsMethodOnToggleCheckbox' in params.arch.attrs && params.arch.attrs['jsMethodOnToggleCheckbox']) {
this.jsMethodOnToggleCheckbox = params.arch.attrs['jsMethodOnToggleCheckbox'];
}
if ('saveSelectionsToSessionStorage' in params.arch.attrs && params.arch.attrs['saveSelectionsToSessionStorage']) {
this.saveSelectionsToSessionStorage = params.arch.attrs['saveSelectionsToSessionStorage'];
}
}
},
// _onToggleSelection: function (ev) {
// 点击列表表头的全选/取消全选复选框时会调用该函数
// this._super.apply(this, arguments);
// },
_onToggleCheckbox: function (ev) {
if (this.hasCheckBoxes) {
var classOfEvTarget = $(ev.target).attr('class');
/* cstom-control-input 刚好点中复选框input,
custom-control custom-checkbox 刚好点中复选框input的父元素div
o_list_record_selector 点击到复选框外上述div的父元素*/
if (['custom-control custom-checkbox', 'custom-control-input', 'o_list_record_selector'].includes(classOfEvTarget)){
if (this.jsMethodOnToggleCheckbox) {
eval(this.jsMethodOnToggleCheckbox)
}
var id = $(ev.currentTarget).closest('tr').data('id'); // 'custom-control-input' == classOfEvTarget
var checked = !this.$(ev.currentTarget).find('input').prop('checked') // 获取复选框是否框选 'custom-control-input' != classOfEvTarget
if ('custom-control-input' == classOfEvTarget) {
checked = this.$(ev.currentTarget).find('input').prop('checked')
}
if (id == undefined) {
if (checked == true) { // 全选
this.recordsSelected = JSON.parse(JSON.stringify(this.records));
} else { // 取消全选
this.recordsSelected = {};
}
} else {
if (checked == true) { // 勾选单条记录
this.recordsSelected[id] = this.records[id];
} else { // 取消勾选单条记录
delete this.recordsSelected[id];
}
}
if (this.saveSelectionsToSessionStorage) {
window.sessionStorage[this.objectID] = JSON.stringify(this.recordsSelected);
}
// 通过rpc请求模型方法,用于传输界面勾选的记录数据
if (this.modelName && this.modelMethod) {
self = this;
this._rpc({
model: this.modelName,
method: this.modelMethod,
args: [this.recordsSelected],
}).then(function (res) {
if (self.jsMethodOnModelMethodDone) {
eval(self.jsMethodOnModelMethodDone);
}
});
}
}
}
this._super.apply(this, arguments);
},
_renderRow: function (record) {
// 打开列表页时会渲染行,此时存储渲染的记录
if (this.hasCheckBoxes) {
this.records[record.id] = {'data': record.data, 'context': record.context};
}
return this._super.apply(this, arguments);
}
});
odoo.__DEBUG__['services']['web.ListRenderer'] = ListRenderer; //覆盖原有的ListRender服务
});
实践过程中,有尝试过以下实现方案,视图通过指定相同服务ID web.ListRenderer
来覆盖框架自带的web.ListRenderer
定义,这种实现方案只能在非Debug
模式下正常工作,且会导致无法开启Debug
模式,odoo.define
实现中会对服务是否重复定义做判断,如果重复定义则会抛出JavaScript异常。
odoo.define('web.ListRenderer', function (require) {
"use strict";
//...略,同上述代码
// odoo.__DEBUG__['services']['web.ListRenderer'] = ListRenderer;
return ListRenderer;
});
笔者后面发现,可以使用include
替代extend
方法修改现有的web.ListRenderer
,如下
odoo.define('estate.ListRenderer', function (require) {
"use strict";
var ListRenderer = require('web.ListRenderer');
ListRenderer = ListRenderer.include({//...略,同上述代码});
// odoo.__DEBUG__['services']['web.ListRenderer'] = ListRenderer; //不需要添加这行代码了
});
odoo14\custom\estate\static\src\js\demo_wizard_views.js
实现版本1
供demo_wizard_views.xml
实现版本1使用
function disableActionConfirmButton(){ // 禁用按钮
$("button[name='action_confirm']").attr("disabled", true);
}
function enableActionConfirmButton(){ // 启用按钮
$("button[name='action_confirm']").attr("disabled", false);
}
这里的设计是,执行复选框操作时,先禁用按钮,不允许执行确认操作,因为执行复选框触发的请求可能没那么快执行完成,前端数据可能没完全传递给后端,此时去执行操作,可能会导致预期之外的结果。所以,等请求完成再启用按钮。
实现版本2
供demo_wizard_views.xml
实现版本2使用
function do_confirm_action(modelName, modelMethod, context){
$("button[name='action_confirm']").attr("disabled", true); // 点击按钮后,禁用按钮状态,比较重复点击导致重复发送请求
var wizard_dialog = $(event.currentTarget.offsetParent.parentElement.parentElement);
var dataUUID = $(event.currentTarget.parentElement.parentElement.parentElement.parentElement).find('div.o_list_view').prop('id');
var rpc = odoo.__DEBUG__.services['web.rpc'];
rpc.query({
model: modelName,
method: modelMethod,
args: [JSON.parse(window.sessionStorage.getItem(dataUUID) || '{}')]
}).then(function (res) if (res == true) {
wizard_dialog.css('display', 'none'); // 隐藏对话框
window.sessionStorage.removeItem(dataUUID);
} else {
$("button[name='action_confirm']").attr("disabled", false);
}
}).catch(function (err) {
$("button[name='action_confirm']").attr("disabled", false);
});
}
odoo14\odoo\addons\base\rng\tree_view.rng
可选操作。如果希望hasCheckBoxes
,modelName
,modelMethod
等也可作用于非内联tree视图,则需要编辑该文件,添加hasCheckBoxes
,modelName
,modelMethod
等属性,否则,更新应用的时候会报错。
<?xml version="1.0" encoding="UTF-8"?>
<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0"
xmlns:a="http://relaxng.org/ns/annotation/1.0"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<!-- ...此处内容已省略 -->
<rng:define name="tree">
<rng:element name="tree">
<!-- ...此处内容已省略 -->
<rng:optional><rng:attribute name="decoration-warning"/></rng:optional>
<rng:optional><rng:attribute name="banner_route"/></rng:optional>
<rng:optional><rng:attribute name="sample"/></rng:optional>
<!--在此处添加新属性>
<rng:optional><rng:attribute name="hasCheckBoxes"/></rng:optional>
<rng:optional><rng:attribute name="modelName"/></rng:optional>
<rng:optional><rng:attribute name="modelMethod"/></rng:optional>
<rng:optional><rng:attribute name="jsMethodOnModelMethodDone"/></rng:optional>
<rng:optional><rng:attribute name="jsMethodOnToggleCheckbox"/></rng:optional>
<rng:optional><rng:attribute name="saveSelectionsToSessionStorage"/></rng:optional>
<!-- ...此处内容已省略 -->
</rng:element>
</rng:define>
<!-- ...此处内容已省略 -->
</rng:grammar>
odoo14\custom\estate\views\webclient_templates.xml
用于加载自定义js
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_common" inherit_id="web.assets_common" name="Backend Assets (used in backend interface)">
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/estate/static/src/js/list_renderer.js"></script>
<script type="text/javascript" src="/estate/static/src/js/demo_wizard_views.js"></script>
</xpath>
</template>
</odoo>
odoo14\custom\estate\__manifest__.py
加载自定义模板文件,进而实现自定义js文件的加载
#!/usr/bin/env python
# -*- coding:utf-8 -*-
{
'name': 'estate',
'depends': ['base'],
'data':[
'views/webclient_templates.xml',
'security/ir.model.access.csv',
#...略
'wizards/demo_wizard_views.xml'
'views/estate_property_views.xml',
'views/estate_property_offer_views.xml',
]
}
记录详情页视图实现
odoo14\custom\estate\views\estate_property_views.xml
<?xml version="1.0"?>
<odoo>
<!--...略-->
<record id="estate_property_view_form" model="ir.ui.view">
<field name="name">estate.property.form</field>
<field name="model">estate.property</field>
<field name="arch" type="xml">
<form string="estate property form">
<header>
<button name="%(action_demo_wizard)d"
type="action"
string="选取offers" class="oe_highlight"/>
<!--...略-->
</header>
<sheet>
<!--...略-->
<notebook>
<!--...略-->
<page string="Offers">
<field name="offer_ids" attrs="{'readonly': [('state', 'in', ['Offer Accepted','Sold','Canceled'])]}"/>
</page>
<!--...略-->
</notebook>
</sheet>
</form>
</field>
</record>
</odoo>
说明:class="oe_highlight"
设置按钮高亮显示
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK