實踐環境
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"
設定按鈕高亮顯示