建議觀看影片, 會更清楚:smile:
-
Youtube Tutorial - odoo 手把手教學 - One2many Editable Bottom and Top - part3-1 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - Search Filters - part4 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - 說明 noupdate 以及 domain_force - part5 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - 如何透過 button 呼叫 view, form - part6 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - 說明 name_get 和 _name_search - part7 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - 使用 python 增加取代 One2many M2X record - part8 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - tree create delete edit False - part9 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - 同一個 model 使用不同的 view_ids - part10 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - widget 介紹 handle 和 many2onebutton - part11 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - view 搭配 context - part12 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - view 搭配 active_test context - part13 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - view 搭配 domain - part14 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - 如何看到當下 view 繼承頁面 - part15 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - odoo rainbow - part16 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - tree decoration - part17 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - model _rec_name 說明 - part18 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - copy override 說明 - part19 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - move position 說明 - part20 - 文章快速連結
-
odoo 手把手教學 - ir.actions.act_url 說明 - part21 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - Smart Button 說明 - part22 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - options create_edit 說明 - part23 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - PostgreSQL ondelete cascade 說明 - part24 - 文章快速連結
-
Youtube Tutorial - odoo14 手把手教學 - auto_join 說明 - part25 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - view parent 說明 - part26 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - domain 搭配 fields 的三種用法 - part27 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - form_view_ref 以及 tree_view_ref 說明 - part28 - 文章快速連結
-
odoo 手把手教學 - Message Post 教學 - part29 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - groups 搭配 fields 用法 - part30 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - ACID transactions 說明 - part31 - 文章快速連結
-
Youtube Tutorial - odoo 手把手教學 - 特殊 groups 應用說明 - part32 - 文章快速連結
建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)
這篇主要介紹 Many2one, Many2many, One2many 這三個東西,
以下將介紹這個 addons 的結構
先來看 models/models.py
Many2one
......
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
name = fields.Char('Description', required=True)
employee_id = fields.Many2one('hr.employee', string="Employee", required=True)
user_id = fields.Many2one('res.users', default=lambda self: self.env.user)
......
一個 hr.employee
可以對到很多個 demo.expense.tutorial
,
所以是 多(demo.expense.tutorial
) 對 一(hr.employee
) 的關係,
來看 db 中的狀況
demo_expense_tutorial
會多出一個欄位 ( 對應 hr_employee
的 id )
user_id
field 中的 default=lambda self: self.env.user
代表預設的值會設定當前登入的 user
因為 One2many 比較特別, 所以我們先介紹 Many2many:laughing:
Many2many
要建立 Many2many 之前, 一定要先定義一個 model,
先定義 DemoTag (也請記得設定 security/ir.model.access.csv )
......
class DemoTag(models.Model):
_name = 'demo.tag'
_description = 'Demo Tags'
name = fields.Char(string='Tag Name', index=True, required=True)
active = fields.Boolean(default=True, help="Set active.")
......
然後接著到底下 models/models.py
......
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
......
# https://www.odoo.com/documentation/12.0/reference/orm.html#odoo.fields.Many2many
# Many2many(comodel_name=<object object>, relation=<object object>, column1=<object object>, column2=<object object>, string=<object object>, **kwargs)
#
# relation: database table name
#
# By default, the relationship table name is the two table names
# joined with an underscore and _rel appended at the end.
# In the case of our books or authors relationship, it should be named demo_expense_tutorial_demo_tag_rel.
### odoo 手把手教學 - Many2many - part2
tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id', string='Tges')
......
Many2many 比較多欄位, 我來說明一下,
comodel_name
為 demo.tag
(需要對應的 model)
relation
為 demo_expense_tag
(table 名稱),
Many2many 會多出一個 table, 這邊是針對 table 命名,
也就是 db 中的 table 名稱,
如果你沒填 relation
這個值, 預設的 table 名稱會是 model名稱 + comodel_name + _rel
,
所以也就會是 demo_expense_tutorial_demo_tag_rel
.
column1
為 demo_expense_id
, demo.expense.tutorial
table 中對應的 id.
column2
為 tag_id
, demo.tag
table 中對應的 id.
繼續看 models/models.py
......
# Related (Reference) fields (不會存在 db)
# readonly default 為 True
# store default 為 False
gender = fields.Selection('Gender', related='employee_id.gender')
......
fields.Selection
就只是下拉選單而已, 比較特別的是 related
這個,
related='employee_id.gender'
這邊的意思是, 會自己去找 employee_id 中的 gender,
到 employee 中找到 gender 為 Male
DemoExpenseTutorial 中的 gender
自然會是 Male,
但要注意幾件事情,
related
預設的 field 是不會儲存在 db 中的, store default 為 False,
你在 table 中是找不到 gender
field 的 (如下圖),
如果你想要儲存在 db 中的, 請另外設定 store=Ture
,
然後 readonly default 為 True, 也就是說你是不可以去修改的,
( 如果要修改請去 employee 中找到 gender 修改 )
接著來看最後一個
One2many
一個 demo.expense.sheet.tutorial
可以對應很多個 demo.expense.tutorial
所以是 一(demo.expense.sheet.tutorial
) 對 多(demo.expense.tutorial
) 的關係,
......
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'
name = fields.Char('Expense Demo Report Summary', required=True)
# One2many is a virtual relationship, there must be a Many2one field in the other_model,
# and its name must be related_field
expense_line_ids = fields.One2many(
'demo.expense.tutorial', # related model
'sheet_id', # field for "this" on related model
string='Expense Lines')
......
說明 expense_line_ids 裡面的參數意義,
demo.expense.tutorial
代表關連的 model (必填)
sheet_id
代表所關連 model 的 field (必填)
也就是說如果你要建立 One2many, 一定也要有一個 Many2one,
但如果建立 Many2one 則不一定要建立 One2many.
One2many 是一個虛擬的欄位, 你在資料庫中是看不到它的存在(如下圖)
你只會看到 Many2one 中的 sheet_id
models/models.py, demo.expense.tutorial
中的 sheet_id
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
......
sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report")
......
記得也要設定對應的 security/ir.model.access.csv 和 security/security.xml.
......
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial Form</field>
......
<!-- <field name="tag_ids"/> -->
<field name="tag_ids" widget="many2many_tags"/> <!-- widget -->
<field name="sheet_id"/>
......
在 odoo 中很有多 widget, 大家可以改成其他的 widget 試試看, 像是 many2many_tags 的 widget
......
<record id="view_form_demo_expense_sheet_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Sheet Tutorial Form</field>
<field name="model">demo.expense.sheet.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Sheet Tutorial">
<sheet>
<group>
<field name="name"/>
</group>
<notebook>
<page string="Expense">
<field name="expense_line_ids">
<tree>
<field name="name"/>
<field name="employee_id"/>
<field name="tag_ids" widget="many2many_tags"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
......
在 view_form_demo_expense_sheet_tutorial
裡的 One2many 中的 expense_line_ids fields,
就把需要的欄位填進去即可,
這邊補充一下 One2many 中的 Editable Bottom 和 Top
<record id="view_form_demo_expense_sheet_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Sheet Tutorial Form</field>
<field name="model">demo.expense.sheet.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Sheet Tutorial">
<sheet>
......
<notebook>
<page string="Expense">
<field name="expense_line_ids" >
<tree>
<!-- <tree editable="top"> --> <!-- <<<<<<<<<<<< -->
<!-- <tree editable="bottom"> --> <!-- <<<<<<<<<<<< -->
<field name="name"/>
<field name="employee_id"/>
<field name="tag_ids" widget="many2many_tags"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
如果你加上 editable
這個參數, 當你新增 record 的時候, 就不會整個跳出視窗, 可以直接在裡面輸入
(或許比較好看:smile:)
至於 editable="bottom"
和 editable="top"
的差別如下
editable="top"
一個新增的 record 會顯示在最上面
editable="bottom"
一個新增的 record 會顯示在最下面
接著來看 filter 的功能
......
<record id="view_filter_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial Filter</field>
<field name="model">demo.expense.tutorial</field>
<field name="arch" type="xml">
<search string="Demo Expense Tutorial Filter">
<field name="name" string="Name"/>
<field name="employee_id" filter_domain="['|', ('employee_id', 'ilike', self), ('user_id', 'ilike', self)]" string="User"/>
<filter name="filter_inactive" domain="[('active','=',False)]" string="Inactive"/>
<filter name="gender" domain="[('gender','=','male')]" string="Male"/>
<separator/>
<filter name="name" domain="[('name', 'ilike', 'a')]" string="Name_2"/>
<group expand="0" string="Group By">
<filter string="Sheet" name="sheet" domain="[]" context="{'group_by': 'sheet_id'}"/>
<filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/>
</group>
</search>
</field>
</record>
......
主要都是在 tree 中搜尋, 可參考上面的 code 去看對應的邏輯
<field name="employee_id" filter_domain="['|', ('employee_id', 'ilike', self), ('user_id', 'ilike', self)]" string="User"/>
特別說明一下這個, self
代表使用者輸入的內容.
<separator>
代表 and
, 如果沒寫則代表 or
.
and
or
<filter string="Sheet" name="sheet" domain="[]" context="{'group_by': 'sheet_id'}"/>
<filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/>
依照特定的 fields 分組
點選後的狀態
再來看看
......
<data noupdate="1">
<record id="ir_rule_demo_expense_user" model="ir.rule">
<field name="name">Demo Expense User</field>
<field name="model_id" ref="model_demo_expense_tutorial"/>
<field name="domain_force">[('employee_id.user_id.id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('demo_expense_tutorial_group_user'))]"/>
</record>
<record id="ir_rule_demo_expense_manager" model="ir.rule">
<field name="name">Demo Expense Manager</field>
<field name="model_id" ref="model_demo_expense_tutorial"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('demo_expense_tutorial_group_manager'))]"/>
</record>
</data>
......
noupdate="1"
(代表更新 addons 時, 不再更新, 除非你刪掉 record)
假如我們在安裝完 addons 之後, 已經存在 record 了, 這時候我們手動更新這些 record 的資料,
再去更新 addons, 這些 record 是不會有任何改變的.
(但是, 假如你刪掉 record, 再去更新 addons, record 會重新被安裝回來.)
noupdate="0"
(代表更新 addons 時, 會保持更新, 也就是會被還原)
假如我們在安裝完 addons 之後, 已經存在 record 了, 這時候我們手動更新這些 record 的資料,
再去更新 addons, 這些 record 是會被改回原本的.
(但是, 假如你刪掉 record, 再去更新 addons, record 會重新被安裝回來.)
id="ir_rule_demo_expense_user"
第一段為針對 demo_expense_tutorial_group_user
限制 domain_force
, 規則很簡單, 這類的 user 只能看到自己的單子, 也就是
[('employee_id.user_id.id', '=', user.id)]
.
id="ir_rule_demo_expense_manager"
針對 demo_expense_tutorial_group_manager
限制 domain_force
, 這邊比較特別 [(1, '=', 1)]
, 代表沒有限制, 也就是全部的單子都
可以看到.
demo
用戶為 User, 所以只能看到自己的單子
Admin
用戶為 Manager, 所以能看到全部的單子
接著補充說明一下, 在 security/ir_rule.xml 中可以設定更細的權限管理
<record id="ir_rule_demo_expense_user" model="ir.rule">
<field name="name">Demo Expense User</field>
<field name="model_id" ref="model_demo_expense_tutorial"/>
<field name="domain_force">[('employee_id.user_id.id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('demo_expense_tutorial_group_user'))]"/>
<!-- Groups (no group = global) -->
<!-- <field name="global" eval="True"/> -->
<field eval="0" name="perm_unlink"/>
<field eval="1" name="perm_write"/>
<field eval="1" name="perm_read"/>
<field eval="1" name="perm_create"/>
</record>
預設的 rule 如果沒有特別設定權限, CRUD 都會是 true,
但也可以去分別設定 (如上教學),
像這邊給了 read, write, create 的權限 (沒給 delete 權限)
<field name="global" eval="True"/>
則代表 global,
基本上, no group = global.
接下來介紹前面跳過的部份, 也就是透過 button 的方式呼叫 view, form,
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
......
@api.multi
def button_sheet_id(self):
return {
'view_mode': 'form',
'res_model': 'demo.expense.sheet.tutorial',
'res_id': self.sheet_id.id,
'type': 'ir.actions.act_window'
}
透過前端呼叫 button_sheet_id
, 會回傳屬於它的 sheet_id
點進去會直接進入 sheet 中的 form
前端的部份就是呼叫 button_sheet_id
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial Form</field>
<field name="model">demo.expense.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Tutorial">
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="button_sheet_id"
string="SHEET ID" type="object"
attrs="{'invisible':[('sheet_id','=', False)]}" icon="fa-bars"/>
</div>
......
</sheet>
</form>
</field>
</record>
既然找了 sheet_id, 也來做一個反查回來的, 也就是透過 sheet_id 找到 demo.expense.tutorial
,
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'
......
@api.multi
def button_line_ids(self):
return {
'name': 'Demo Expense Line IDs',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': 'demo.expense.tutorial',
'view_id': False,
'type': 'ir.actions.act_window',
'domain': [('sheet_id', '=', self.id)],
}
......
res_model
為目標的 model demo.expense.tutorial
.
domain
稍微說明一下 [('sheet_id', '=', self.id)],
,
sheet_id
是指目標 model demo.expense.tutorial
的 sheet_id,
self.id
是指當下 model demo.expense.sheet.tutorial
的 id.
點下去會帶出它的 demo.expense.tutorial
views/view.xml 的部份
......
<record id="view_form_demo_expense_sheet_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Sheet Tutorial Form</field>
<field name="model">demo.expense.sheet.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Sheet Tutorial">
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="button_line_ids"
string="SHEET IDs" type="object"
attrs="{'invisible':[('expense_line_ids','=', False)]}" icon="fa-bars"/>
</div>
......
</sheet>
</form>
</field>
</record>
最後來看 models/models.py 中比較特殊的部份,
分別是 name_get
和 _name_search
,
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'
......
@api.multi
def name_get(self):
names = []
for record in self:
name = '%s-%s' % (record.create_date.date(), record.name)
names.append((record.id, name))
return names
# odoo12/odoo/odoo/addons/base/models/ir_model.py
@api.model
def _name_search(self, name='', args=None, operator='ilike', limit=100):
if args is None:
args = []
domain = args + ['|', ('id', operator, name), ('name', operator, name)]
# domain = args + [ ('name', operator, name)]
# domain = args + [ ('id', operator, name)]
return super(DemoExpenseSheetTutorial, self).search(domain, limit=limit).name_get()
首先是 name_get
這個的功能主要是去修改 name 的名稱, 在這邊我們加上當下的時間
(可以依照自己的需求下去修改)
Many2one 時也會看到自己定義的 name_get
注意:exclamation: 這些增加的值是不會儲存進 db 中的, db 中還是儲存的是 name 的內容而已 (概念和 compute field 一樣:smile:)
再來要來說明 _name_search
,
如果沒有它, 假設我知道某個資料的 id 是 4, 在搜尋的地方打上 id,
你會發現找不到資料:joy:
但今天如果有了 _name_search
並實作它,
你會發現這次你打 id 會才成功找到需要的資料:satisfied:
我在 code 中有放幾個範例註解, 大家可以自行玩玩看:smile:
這邊只需要注意3個 function,
add_demo_expense_record
link_demo_expense_record
replace_demo_expense_record
分別對應的 button 為下圖
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'
name = fields.Char('Expense Demo Report Summary', required=True)
# One2many is a virtual relationship, there must be a Many2one field in the other_model,
# and its name must be related_field
expense_line_ids = fields.One2many(
'demo.expense.tutorial', # related model
'sheet_id', # field for "this" on related model
string='Expense Lines')
@api.multi
def add_demo_expense_record(self):
# (0, _ , {'field': value}) creates a new record and links it to this one.
data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
tag_data_1 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_1')
tag_data_2 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_2')
for record in self:
# creates a new record
val = {
'name': 'test_data',
'employee_id': data_1.employee_id,
'tag_ids': [(6, 0, [tag_data_1.id, tag_data_2.id])]
}
self.expense_line_ids = [(0, 0, val)]
@api.multi
def link_demo_expense_record(self):
# (4, id, _) links an already existing record.
data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
for record in self:
# link already existing record
self.expense_line_ids = [(4, data_1.id, 0)]
@api.multi
def replace_demo_expense_record(self):
# (6, _, [ids]) replaces the list of linked records with the provided list.
data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
data_2 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_2')
for record in self:
# replace multi record
self.expense_line_ids = [(6, 0, [data_1.id, data_2.id])]
說明 add_demo_expense_record
......
@api.multi
def add_demo_expense_record(self):
# (0, _ , {'field': value}) creates a new record and links it to this one.
data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
tag_data_1 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_1')
tag_data_2 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_2')
for record in self:
# creates a new record
val = {
'name': 'test_data',
'employee_id': data_1.employee_id,
'tag_ids': [(6, 0, [tag_data_1.id, tag_data_2.id])]
}
self.expense_line_ids = [(0, 0, val)]
......
(0, _ , {'field': value})
新建一筆 record 並且連接它.
self.env.ref(......)
這個的用法是去取得既有的資料, 路徑在 data/demo_expense_tutorial_data.xml.
當你點選按鈕, 下面就會一直新增資料
說明 link_demo_expense_record
......
@api.multi
def link_demo_expense_record(self):
# (4, id, _) links an already existing record.
data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
for record in self:
# link already existing record
self.expense_line_ids = [(4, data_1.id, 0)]
......
(4, id, _)
連接已經存在的 record.
當你點選按鈕, 下面會直接連接一比資料, 如果已經連接就不會有動作,
說明 replace_demo_expense_record
......
@api.multi
def replace_demo_expense_record(self):
# (6, _, [ids]) replaces the list of linked records with the provided list.
data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
data_2 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_2')
for record in self:
# replace multi record
self.expense_line_ids = [(6, 0, [data_1.id, data_2.id])]
......
(6, _, [ids])
使用 list 取代既有的 records.
當你點選按鈕, 會使用你定義的 list 取代全部的 records.
通常管理一個使用者可不可以建立 records, 是根據 security 資料夾裡面的檔案,
也就是 security.xml
ir_rule.xml
ir.model.access.csv
.
記住:exclamation: odoo 可以從 model 層(db層) 或權限下手, 也可以從 view 那層下手,
當然, 如果是從安全性的角度來看 從 model 層(db層) 或權限下手 是比較高全的:smile:
今天就是要來介紹 從 view 那層下手,
增加一個 tree views/view.xml
......
<record id="view_tree_demo_expense_tutorial_no_create" model="ir.ui.view">
<field name="name">Demo Expense Tutorial List No Create</field>
<field name="model">demo.expense.tutorial</field>
<field name="arch" type="xml">
<tree string="no_create_tree" create="0" delete="false" edit="1" editable="top">
<field name="name"/>
<field name="employee_id"/>
</tree>
</field>
</record>
......
重點在 <tree string="no_create_tree" create="0" delete="false" edit="1" editable="top">
這段, 裡面增加了一下 tag, 允許就是 1
或 True
, 不允許就是 0
或 False
.
儘管你有權限建立 records, 如果你設定了 create="0"
, 你還是沒辦法建立 records.
也記得在 views/menu.xml 增加 action,
並且要指定 view_id
(也就是剛剛建立出來的那個)
......
<!-- Action to open the demo_expense_tutorial_no_craete -->
<record id="action_expense_tutorial_no_craete" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action No Craete</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree</field>
<field name="view_id" ref="view_tree_demo_expense_tutorial_no_create"/>
</record>
......
你會發現 create delete 的按鈕都消失了
一般來說, 在定義一個 model 時, 通常會搭配一個 form 的 view 以及 tree 的 view, 或是特別指定一個 view,
像是前面介紹到的 view_id, 但有時候會有這種情況, 也就是一個 model, 在兩個不同的地方, 分別顯示不同的
form 的 view 以及 tree 的 view, 這時候就要使用 view_ids 分別下去定義.
現在 meun 上會多出 Demo Expense Tutorial View ids, 點下去分別有
Demo Expense Tutorial View id 1 以及 Demo Expense Tutorial View id 2
他們都是屬於 demo.expense.tutorial
model, 只不過使用了不同的 view 和 form,
為了方便區分不同的 form 和 view, 簡單用 fields 的排序不同
view 1
view 2
可參考 views/menu.xml
......
<!-- Action to open the menu_expense_tutorial_view_id_1 -->
<record id="action_expense_tutorial_view_id_1" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action View id 1</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.tree_expense_view_id_1')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.form_expense_view_id_1')})]"/>
</record>
......
<!-- Action to open the menu_expense_tutorial_view_id_2 -->
<record id="action_expense_tutorial_view_id_2" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action View id 2</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.tree_expense_view_id_2')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.form_expense_view_id_2')})]"/>
</record>
......
看起來雖然很複雜, 但其實不難, 設定都和之前的一樣, 只是將 view_id 換成了 view_ids, 然後分別設定不同的 view_id,
這邊只有分別設定 tree 和 form, 如果你想要定義新的 kanban 或其他的 view_mode 也都是可以的.
eval="[(5, 0, 0)
的意思是清除所有和它有關的 record (因為我們重新定義了需要的 view),
相關說明可參考
(0, 0, { values }) link to a new record that needs to be created with the given values dictionary
(1, ID, { values }) update the linked record with id = ID (write *values* on it)
(2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
(3, ID) cut the link to the linked record with id = ID (delete the relationship between the two objects but does not delete the target object itself)
(4, ID) link to existing record with id = ID (adds a relationship)
(5) unlink all (like using (3,ID) for all linked records)
(6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
你也可以在 Technical -> Actions -> Window Actions 看到你所設定的 view_ids
這些就是剛剛的設定
這邊補充一個小技巧, 在定義 view 時, 有一個參數是 <field name="priority" eval="1"/>
, 如果你只有一個 view 不需要特別設定,
但如果你有很多個, 你可以透過這個 priority 去決定顯示 view 的優先權.
在 odoo 中很非常多的 widget 可以使用, 除了像前面介紹的 widget="many2many_tags"
之外, 這邊再介紹另外兩個,
首先是 handle widget,
這個比較常和 sequence 搭配一起使用,
可參考 models/models.py
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
_order = "sequence, id desc"
......
sequence = fields.Integer(index=True, help="Gives the sequence order", default=1)
定義了一個 sequence fields, 然後排序使用 sequence.
在 tree view 中加入 widget="handle"
,
可參考 views/view.xml
<record id="view_tree_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial List</field>
<field name="model">demo.expense.tutorial</field>
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<tree>
<!-- <tree default_order="sequence, id desc"> -->
<field name="sequence" widget="handle"/>
......
</tree>
</field>
</record>
這邊補充一下, 除了在 model 中定義 order 之外, 也可以在 tree, kanban 上定義,
像是 <tree default_order="sequence, id desc">
.
這樣就完成了, 你會發現 tree 可以排序了:smile:
再來是 many2onebutton widget,
通常如果一個 tree view 上有 many2one 的 fields, 如果想看這個 fields 的資料,
必須要點進去 form, 再點進去 many2one 的 fields 才看的到資料,
如果在 tree 的 many2one fields 上加入 widget="many2onebutton"
,
就可以直接點進去觀看該 fields 的資料.
可參考 views/view.xml
<record id="view_tree_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial List</field>
<field name="model">demo.expense.tutorial</field>
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<tree>
......
<field name="sheet_id" widget="many2onebutton"/>
</tree>
</field>
</record>
你會發現 many2one fields 變藍色的了, 直接點選即可.
這部份將介紹 view 搭配 context 的使用,
可參考 views/menu.xml
<!-- Action to open the demo_expense_tutorial_context -->
<record id="action_expense_tutorial_context" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action Context</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<!-- init search default -->
<field name="context">{'search_default_name': 'test123'}</field>
<!-- init create default name 'test123'-->
<!-- <field name="context">{'default_name': 'test123'}</field> -->
</record>
來說明一下 <field name="context">{'search_default_name': 'test123'}</field>
這段程式碼, name
是我定義的 fields, 格式是 search_default + fields
,
也就是進入這個 view 的時候, 預設會幫你搜尋 name
吻合 test123
.
接著來看另一個, <field name="context">{'default_name': 'test123'}</field>
這段程式碼, 格式是 default + fields
, 注意哦, 這次沒有 search,
那這個和剛剛的有什麼不同呢:question:
當你建立一個 records 的時候, 他預設會幫你的 name
fields 自動帶入 test123
.
context 也可以在 developer mode 中的 Edit Action 看到,
這部份延續上一次的介紹, 來看看 active_test
這個東西,
這部份建議大家看影片會比較清楚:smile:
先來看 models/models.py
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
_order = "sequence, id desc"
......
active = fields.Boolean(default=True, help="Set active.")
通常如果有定義 active fields,
預設的情況下, 你的 view 就是會顯示只有 active=True
的 records,
如果你要顯示 active=False
的 records,
則必須另外去 filter 出來, 如下圖
這樣你可能會問我, 為什麼會這樣呢:question:
原因是 odoo 原始碼內的 odoo/models.py
這段
@api.model
def _where_calc(self, domain, active_test=True):
......
if 'active' in self._fields and active_test and self._context.get('active_test', True):
# the item[0] trick below works for domain items and '&'/'|'/'!'
# operators too
if not any(item[0] == 'active' for item in domain):
domain = [('active', '=', 1)] + domain
預設如果沒有特別指定, 邏輯就是會跑 active = 1
也就是 True.
那如果我今天希望預設顯示 active 為 True 和 False 同時都顯示, 這樣要如何實作:question:
搭配 <field name="context">{'active_test':False}</field>
這段程式碼,
可參考 views/menu.xml
<!-- Action to open the demo_expense_tutorial_test_active -->
<record id="action_expense_tutorial_test_active" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action Test Active</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<!-- init show all (active True False) record -->
<field name="context">{'active_test':False}</field>
<!-- init show only (active True) record -->
<!-- <field name="context">{}</field> -->
</record>
這樣子預設就會把全部的 records (不管 active 狀態) 都顯示出來.
context 同樣也可以在 developer mode 中的 Edit Action 看到,
這部份將介紹 view 搭配 domain 的使用,
使用方法和 context 差不多:smile:
可參考 views/menu.xml
<!-- Action to open the demo_expense_tutorial_domain -->
<record id="action_expense_tutorial_domain" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action Domain</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('name', 'like', 'test')]</field>
<field name="context">{}</field>
</record>
說明 <field name="domain">[('name', 'like', 'test')]</field>
這段程式碼,
只會顯示 name fields like test 的內容,
注意:exclamation: 和 search_default_name
不一樣的地方是, 他不會顯示 search 的東西,
使用者也不能自行修改
domain 同樣也可以在 developer mode 中的 Edit Action 看到,
有時候當我們寫了很多的繼承 ( tree 或 form), 在當下的頁面, 會不知道是否有被繼承過,
這時候推薦大家一個小技巧:smile:
使用 demo_class_inheritance 這個 addons 當作範例.
首先, 先進去你想要查看的頁面, 這邊進入 hr_expnese
debug developer mode 請打開, 可參考 odoo12 如何開啟 odoo developer mode
點選 Edit View: List
點選 Inherited Views 這個 tab
你就可以很清楚的看到這個頁面被 demo_class_inheritance
繼承:smile:
像是 form 或其他的 view_type 也都是同樣的方法哦:smirk:
在 odoo 中也有特效這個東西
@api.multi
def button_rainbow_man(self):
return {
'effect': {
'fadeout': 'slow',
'message': 'hello',
'type': 'rainbow_man',
}
}
在 odoo 中有很多的 decoration 可以使用, 通常是搭配 tree 顯示特殊的資料.
使用方法非常的簡單, 直接加上需要顯示的邏輯即可,
可參考 views/view.xml
......
<record id="view_tree_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial List</field>
<field name="model">demo.expense.tutorial</field>
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<tree decoration-info="'info' in name" decoration-muted="'muted' in name" decoration-danger="'danger' in name" decoration-bf="'bf' in name" decoration-warning="'warning' in name" decoration-success="'success' in name">
......
</tree>
</field>
</record>
......
關於 decoration-{$name}
的詳細說明, 可參考官方文件 Advanced Views
今天要和大家介紹在 model 中有時會看到的 _rec_name
,
......
class DemoTag(models.Model):
_name = 'demo.tag'
_description = 'Demo Tags'
_rec_name = 'complete_name'
name = fields.Char(string='Tag Name', index=True, required=True)
complete_name = fields.Char('Complete Name', compute='_compute_complete_name')
active = fields.Boolean(default=True, help="Set active.")
@api.depends('name')
def _compute_complete_name(self):
for record in self:
record.complete_name = 'hello world - {}'.format(record.name)
......
首先, 你需要知道一件事, 如果你建立一個 model, 該 model 沒有特別定義 name
, 且沒另外指定 _rec_name
(如果沒特別指定 _rec_name
, default 就是使用 name
)
通常這時候 odoo 的訊息會提醒你建議你設定 name
field 或是指定 _rec_name
.
但這只是 WARNING, 你也可以不要理他.
但我的建議是, 如果你不指定 name
, 就請特別去指定 _rec_name
.
當然, 如果你有設定了 name
, 你也可以特別去指定 _rec_name
為其他的 field.
這個 name
和 _rec_name
只是指定顯示的名稱而已.
詳細的 demo 差異可以看影片的說明.
在 odoo 中有幾個比較特殊的 function, 分別是 create
write
copy
unlink
,
create
建立一比 record 時.
write
更新一比 record 時.
copy
複製一比 record 時.
unlink
刪除一比 record 時.
今天來介紹 copy
當作範例, 其他的大家可以以此類推:smile:
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
_order = "sequence, id desc"
name = fields.Char('Description', required=True)
......
tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id', string='Tges', copy=False)
......
@api.multi
def copy(self, default=None):
default = dict(default or {})
if not default.get('name'):
default['name'] = '{} copy'.format(self.name)
return super(DemoExpenseTutorial, self).copy(default)
當點選 Duplicate 時, 會觸發這個 copy
(在這邊做的事情是 override, 和之前介紹的繼承觀念其實是差不多的)
你會發現 name 被加上 copy 了.
然後預設的 field 是 copy=True
, 如果不想要他被 copy, 可以直接在 fields
上設定 copy=False
.
最後要提醒大家, 在 odoo 中少用/小心使用 duplicate, 不然就是你要非常清楚裡面寫了甚麼,
因為我很常用到複製出來的 record 有問題, 可能是翻譯, 又可能是複製出來的這比很奇怪,
唯一的可能就是他的 copy 沒有寫好, 特殊的邏輯沒有補上去, 導致你複製出來的 record 行為很怪.
基本上在要修改 create
write
copy
unlink
時, 可以先想想有沒有比較簡單的方式能
改動你的需求, 如果真的沒有, 才選擇改他:smirk:
在 odoo 中常常容易使用到繼承的方式改寫 view, 最常見的 position 就是 after, before, replace,
但有時候會有一種狀況, 就是單純想要交換兩個既有的 fields 位置 (不使用直接在 odoo 上面改, 不推薦),
例如, 下面這個已經存在的 view, 希望交換 Employee 和 Description 的位置,
假設這個 view 只能使用繼承的方式修改, 這時候通常很麻煩, 因為有可能你的作法是把整個 tree 去 replace 掉,
再自己去排版, 但為了幾個 fields 就去 replace 掉整個 view, 真得有點麻煩:expressionless:
所以, 今天來認識 move position 這個東西, 寫法可參考 view.xml
<!-- change name, employee_id fields-->
<record id="view_tree_demo_expense_tutorial_move" model="ir.ui.view">
<field name="name">view_tree_demo_expense_tutorial_move</field>
<field name="model">demo.expense.tutorial</field>
<field name="inherit_id" ref="demo_expense_tutorial_v1.view_tree_demo_expense_tutorial"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="before">
<field name="employee_id" position="move"/>
</xpath>
</field>
</record>
基本上就是在要移動的 fields 上的後面加上 position="move"
即可, 效果如下
有了這個東西, 以後單純想要交換兩個 fields, 只需要使用 move 即可, 不需要再整個 replace 了:smile:
這部份很簡單, 只是要和大家說可以透過 ir.actions.act_url
來開始 url
可參考 models/models.py
@api.multi
def button_act_url(self):
self.ensure_one()
return {
'type': 'ir.actions.act_url',
'target': 'new',
# 'target': 'self',
'url': 'https://github.com/twtrubiks/odoo-demo-addons-tutorial',
}
甚麼是 Smart Button ❓ 如果你常用 odoo, 你一定常看到這個東西,
如下, 這就是所謂的 Smart Button
其實之前就有介紹過 Smart Button 了, 今天就再順便說明要如何設計底下的那個數字
首先, 你需要透過 compute 這個參數建立一個 fields (用來計算出這個數字)
......
demo_expenses_count = fields.Integer(
compute='_compute_demo_expenses_count',
string='Demo Expenses Count')
......
def _compute_demo_expenses_count(self):
# usually used read_group
for record in self:
record.demo_expenses_count = len(self.expense_line_ids)
......
在這邊只是簡單的算出數量而已(為了demo), 註解在這邊的意思是說, 通常都會使用 read_group
來做計算 (可自行參考 odoo source code)
最後, 自行將這個 fields 放到 view 中即可, 可參考 views/view.xml
......
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button"
name="button_line_ids"
type="object"
attrs="{'invisible':[('expense_line_ids','=', False)]}"
icon="fa-bars">
<field name="demo_expenses_count" widget="statinfo" string="Counts"/>
</button>
</div>
......
效果如下
今天要來介紹 options
這個參數,
請參考 view.xml
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial Form</field>
<field name="model">demo.expense.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Tutorial">
......
<field name="name"/>
<field name="employee_id"/>
<!-- <field name="employee_id" options="{'no_quick_create': True}"/> -->
<!-- <field name="employee_id" options="{'no_create_edit': True}"/> -->
<!-- <field name="employee_id" options="{'no_create': True}"/> -->
<!-- <field name="employee_id" options="{'no_open': True}"/> -->
......
</form>
</field>
</record>
在一般的情況下 (不設定任何的 option
), 顯示如下,
以下是每一種 option 呈現的效果, 大家可以自行玩玩看:smile:
no_quick_create
no_create_edit
no_create
no_open
當然, 如果你的需求是多個組合, 也可以多個一起使用.
今天要介紹 Many2one fields 中的 ondelete="cascade"
參數代表的意思,
這邊要先說明一下, ondelete='cascade'
這個東西並不是 odoo 的, 它是 PostgreSQL 的特性:exclamation:
使用方法很簡單, 如下, 可參考 models/models.py
......
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
......
sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report", ondelete='cascade')
......
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
......
expense_line_ids = fields.One2many(
'demo.expense.tutorial', # related model
'sheet_id', # field for "this" on related model
string='Expense Lines')
......
在開始介紹之前, 大致上除了 'cascade'
之外, 還有其他幾個選項, 如下分別是
說明 ondelete='set null'
這個是 default, 可參考 odoo fields.py
中的 Many2one 說明(如下).
......
class Many2one(_Relational):
""" The value of such a field is a recordset of size 0 (no
......
:param ondelete: what to do when the referred record is deleted;
possible values are: ``'set null'``, ``'restrict'``, ``'cascade'``
......
"""
......
_slots = {
'ondelete': 'set null', # what to do when value is deleted
'auto_join': False, # whether joins are generated upon search
'delegate': False, # whether self implements delegation
}
......
( 如果文章看的不是很清楚, 請參考影片中的說明, 我會一個一個說明 😎 )
說明 ondelete='set null'
(預設行為)
如果直接刪除 sheet_id
( 底下有很多expense_line_ids
), 可以成功刪除 sheet_id
, 但你會發現
expense_line_ids
並沒有被刪除 ( sheet_id
變為 null
).
說明 ondelete='cascade'
如果直接刪除 sheet_id
( 底下有很多expense_line_ids
), 可以成功刪除 sheet_id
, 且你會發現
expense_line_ids
也自動都被移除了.
說明 ondelete='restrict'
如果直接刪除 sheet_id
( 底下有很多expense_line_ids
), 無法刪除 sheet_id
,
你必需先移除 sheet_id
底下的 expense_line_ids
, 才可以刪除 sheet_id
.
其實, 你可以把他們想成是 child 和 parent 的關係即可:smile:
要如何知道 fields 有 ondelete='....'
之類的特性呢:question:
除了可以透過 code 或 odoo 的 model fields 中查看之外,
也可以利用查看 db table 的工具 (pgadmin4)
在 view 中可以透過 parent
這個值, 拿到 parent
的 fields
內容 (可能有點繞口:smile:)
不懂沒關係, 請看以下的說明:smile:
通常會使用在 view 中的 domain 或是 attrs,
首先, 先看一下 models/models.py
......
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
_order = "sequence, id desc"
name = fields.Char('Description', required=True)
......
sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report", ondelete='restrict')
......
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'
name = fields.Char('Expense Demo Report Summary', required=True)
# One2many is a virtual relationship, there must be a Many2one field in the other_model,
# and its name must be related_field
expense_line_ids = fields.One2many(
'demo.expense.tutorial', # related model
'sheet_id', # field for "this" on related model
string='Expense Lines')
......
接著看 views/view.xml
......
<record id="view_form_demo_expense_sheet_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Sheet Tutorial Form</field>
<field name="model">demo.expense.sheet.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Sheet Tutorial">
......
<group>
<field name="name"/>
</group>
<notebook>
<page string="Expense">
<field name="expense_line_ids" >
<!-- <tree> -->
<tree editable="top"> <!-- <<<<<<<<<<<< -->
<!-- <tree editable="bottom"> --> <!-- <<<<<<<<<<<< -->
<field name="name"/>
<field name="employee_id"/>
<field name="tag_ids" widget="many2many_tags" attrs="{'readonly': [('parent.name', '=', 'test-readonly')]}"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
......
主要請看 <field name="tag_ids" widget="many2many_tags" attrs="{'readonly': [('parent.name', '=', 'test-readonly')]}"/>
當 sheet 的 name
為 test-readonly
的時候, tag_ids
這個 fields 會變成 readonly
.
請注意:exclamation: 我們並沒有 parent
這個欄位, 但是在 view 中可以透過這種方式使用 parent (也就是 sheet ) 的東西.
當 sheet 的 name
不是 test-readonly
時, tag_ids
這個 fields 會變成可以 edit ( 不是readonly
).
另外一點要注意的是, 請搭配 <tree editable="top">
或 <tree editable="bottom">
, 單純使用 <tree>
不會生效:exclamation:
editable
的效果可參考之前的介紹 odoo 手把手教學 - One2many Editable Bottom and Top
在 odoo 中 domain 幾乎無所不在:smile: 今天和大家介紹三種 domain 搭配 fields 的用法,
第一種 - 直接在 model 中的 fileds 定義
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
......
employee_id = fields.Many2one('hr.employee', string="Employee", required=True,
domain=[('active', '=', True)] )
......
開啟 odoo12 如何開啟 odoo developer mode, 並且到 fields 上觀看, 會看到我們定義的 domain
第二種 - 直接在 view 中的 fileds 定義
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial Form</field>
<field name="model">demo.expense.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Tutorial">
......
<field name="employee_id" domain="[('user_id', '=', user_id)]"/>
......
</group>
</sheet>
</form>
</field>
</record>
開啟 odoo12 如何開啟 odoo developer mode, 並且到 fields 上觀看, 會看到我們定義的 domain
❗這邊要注意的是, 如果第一種和第二種同時寫, 以第二種在 view 上定義的為主❗
第三種 - 透過 onchange
的方法增加 domain
這種方法蠻酷的, 所以我留到最後來講:smile:
首先, 如果不了解 onchange
可參考 介紹 model.
請看下面的範例 model/models.py
......
@api.onchange('user_id')
def onchange_user_id(self):
# domain
result = dict()
result['domain'] = {
'employee_id': [('user_id', '=', self.user_id.id)]
}
# equal
# self.env['hr.employee'].search([('user_id', '=', self.user_id.id)])
return result
......
當改變 user_id
時, 會增加對應的 domain, 需要回傳一個 dict,
這個 dict 包含 fields, 也就是 employee_id
, 後面則是我們所需要的 domain.
還記得 odoo 手把手教學 - 同一個 model 使用不同的 view_ids - part10 這篇教學嗎:question:
那時候是使用 ir.actions.act_window
也就是 action 的方式定義不同的 view_ids,
今天如果想單獨針對 fields 定義 view 時, 就需要使用 form_view_ref
tree_view_ref
❗
使用方法也很簡單, 請參考 views/view.xml
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial Form</field>
<field name="model">demo.expense.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Tutorial">
......
<field name="sheet_id" context="{'form_view_ref':'demo_expense_tutorial_v1.view_form_demo_expense_sheet_tutorial'}"/>
<!-- <field name="sheet_id" context="{'form_view_ref':'demo_expense_tutorial_v1.custom_view_form_demo_sheet'}"/> -->
......
</group>
</sheet>
</form>
</field>
</record>
......
<record id="custom_view_form_demo_sheet" model="ir.ui.view">
<field name="name">Custim Demo Sheet Form</field>
<field name="model">demo.expense.sheet.tutorial</field>
<field name="arch" type="xml">
<form string="custom_view_form_demo_sheet">
<sheet>
......
</sheet>
</form>
</field>
</record>
在需要的 fields 上, 加上 context="{'form_view_ref':......}"
, 然後再定義你的 view 即可,
tree_view_ref
也是一樣的概念:smile:
注意:exclamation:, 在這裡只要你有定義一個以上的 demo.expense.sheet.tutorial
form view 時,
記得一定要使用 form_view_ref
( 否則它會自動選最後一個 ).
可參考 models/models.py
......
@api.multi
def btn_message_post(self):
for rec in self:
if rec.user_id:
rec.user_id.partner_id.message_post(body="test body", subject="test subject")
else:
raise UserError('請選擇使用者(user_id)')
......
透過 partner_id.message_post(....")
可以完成 Message Post, 資訊要到 Contacts (res.partner
) 底下看,
這邊的用法和 odoo 手把手教學 - domain 搭配 fields 的三種用法 - part27 是類似的,
只不過對象換成了 groups,
寫法如下, 請參考 models/models.py 中,
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
......
tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id',
string='Tges', copy=False,
groups='demo_expense_tutorial_v1.demo_expense_tutorial_group_manager'
)
......
tag_ids
field 增加了 groups='demo_expense_tutorial_v1.demo_expense_tutorial_group_manager'
,
代表的意思是只有 Manager 可以看到這個 field(擁有權限),
假如今天一個 User 權限的人, 不管在 tree 或是 form 都看不到 tag_ids
.
但這不只是隱藏起來, 也就是說如果你強制去取值, 還是無法拿到資料的(因為權限不夠),
建議可以用 shell 模式下去嘗試取值.
然後另一種寫法如下,
請參考 views/view.xml,
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
......
<field name="tag_ids" widget="many2many_tags" groups="demo_expense_tutorial_v1.demo_expense_tutorial_group_manager"/>
......
</record>
但這只是隱藏起來, 也就是說如果你強制去取值, 還是可以拿到資料的(沒有權限限制),
差別在於,
如果你是寫在 model, 會自動幫你產生到全部的 view 上(並且需要對應的權限).
如果你單獨寫在 view 上, 是針對個別的 view (像這邊就是只在 form 上) 生效,
(但不需要對應的權限).
這邊先說結論, 下面會再說明, 如果你遵守 odoo 的 ORM,
你是不需要另外去處理 ACID transactions 的問題.
相關的 odoo source code 可參考 odoo/odoo/sql_db.py
class Cursor(object):
"""Represents an open transaction to the PostgreSQL DB backend,
acting as a lightweight wrapper around psycopg2's
``cursor`` objects.
``Cursor`` is the object behind the ``cr`` variable used all
over the OpenERP code.
.. rubric:: Transaction Isolation
......
如果你不知道甚麼是 ACID, 可參考 Transaction 概念簡介.
這邊就用一個例子來說明 Atomicity (原子性),
可參考 models/models.py
......
@api.multi
def btn_test_acid_atomicity(self):
for index in range(3):
self.create({
'name': index,
'employee_id': 1
})
if index == 1:
raise UserError('error - auto rollback')
......
嘗試上面的 code.
假設今天有3筆資料,
只有兩種結果, 3筆全都寫入成功, 或是3筆全都失敗未寫入,
不會有1筆成功寫入, 2筆失敗的狀況.
這邊介紹幾個比較特殊的 groups 給大家,
首先是 base.group_no_one
,
它的 groups 定義在原始碼中的 /odoo/addons/base/security/base_groups.xml
......
<record model="res.groups" id="group_no_one">
<field name="name">Technical Features</field>
</record>
......
這個 groups 只有在你打開 odoo developer mode 的時候才看的到.
接著是 base.group_erp_manager
和 base.group_system
,
這些 groups 則是當你擁有 Access Rights
和 Settings
權限的時候你才看的到.
它的 groups 定義在原始碼中的 /odoo/addons/base/security/base_groups.xml
......
<record model="res.groups" id="group_erp_manager">
<field name="name">Access Rights</field>
</record>
<record model="res.groups" id="group_system">
<field name="name">Settings</field>
<field name="implied_ids" eval="[(4, ref('group_erp_manager'))]"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
......
當然除了使用 groups 定義之外, 也可以直接指定 user,
像是 base.user_admin
就是只有 admin user 才看的到.
它的定義在原始碼中的 /odoo/addons/base/data/res_users_data.xml
......
<!-- user 2 is the human admin user -->
<record id="user_admin" model="res.users">
<field name="login">admin</field>
<field name="password">admin</field>
<field name="partner_id" ref="base.partner_admin"/>
<field name="company_id" ref="main_company"/>
<field name="company_ids" eval="[(4, ref('main_company'))]"/>
<field name="groups_id" eval="[(6,0,[])]"/>
<field name="signature"><![CDATA[<span>-- <br/>
Administrator</span>]]></field>
</record>
......
使用方法其實之前都說明過了, 可參考 views/view.xml
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial Form</field>
<field name="model">demo.expense.tutorial</field>
......
<field name="debug_field" groups="base.group_no_one"/>
<field name="admin_field" groups="base.user_admin"/>
......
基本上原生的都是定義在原始碼中的 odoo/addons/base/security/base_groups.xml
像是基本的 user groups 種類,
......
<record model="res.groups" id="group_user">
<field name="name">Internal User</field>
</record>
......
<record id="group_portal" model="res.groups">
<field name="name">Portal</field>
......
</record>
......
<record id="group_public" model="res.groups">
<field name="name">Public</field>
......
</record>
......
透過定義 user 以及 groups, 可以組合出更靈活的架構.