繼承-擴展原有的應用
Odoo中,有一個非常重要的特色,不用直接修改底層對象就能為我們的模塊添加新的功能。這個特色就是Odoo中的繼承(inheritance)機制.繼承能夠在不同的層面上(models,views,business logic)來進行對原有模塊的修改。
為我們的To-Do app增加分享功能
我們的To-Do應用允許用戶管理他們自己的to-do 任務,我們接下來需要創建一個新
的模塊來擴展前一章的To-Do應用,運用繼承機制來添加分享任務和討論功能。
我們的工作計劃如下:
- 添加task的負責人
- 修改業務邏輯,用戶只能修改自己的tasks.
- 擴展view視圖,添加必須的字段
- 添加社交網絡功能:一個信息墻跟關注者。
效果圖
首先根據前一篇文章中創建todo_app時的方法創建todo_user模塊的骨架,還是在我們的'custom-addons'模塊中新建'todo_user'文件夾。創建'custom-addons/todo_user/manifest.py',添加如下代碼
{
'name': 'Multiuser To-Do',
'description': 'Extend the To-Do app to multiuser.',
'author': 'Daniel Reis',
'depends': ['todo_app'],
}
注意到我們添加了對原先的'todo_app'模塊的依賴'depends':['todo_app']
.這對我們的繼承機制是十分重要的.當我們添加了'depend'后,每次更新我們的'todo_user'模塊,它對應的依賴'todo_app'會自動更新.
擴展我們的模型(models)
python的classes能夠定義新的models.我們對models擴展需要使用Odoo的繼承機制來編寫classes.
- 如果要擴展一個已經存在的模型(model),我們使用python的類屬性
'_inherit'
.它指定了被擴展的模型.通過'_inherit'指定被繼承模型后我們新創建的類能獲得父類模型的所有功能.因此我們只要對需要修改的地方進行重構即可. - 事實上,Odoo的模型存在于我們Python模塊的外部,在一個集中的注冊處理區,這個注冊處理區,能夠被我們的模型方法self.env[<model name>]所獲取到,舉例來說,我們可以得到res.partner這個模型的對象通過使用self.env['res.partner']
- 為了修改Odoo模型,我們獲取到這個模型在注冊處理區的具體注冊類,然后執行修改在這個注冊類上。這意味著所有調用該模型的地方都會得到我們修改過后的新的注冊類。
- 還有一個問題需要注意,當我們的Odoo服務啟動時,我們的模塊加載順序是密切相關的,所以要必須保障我們的依賴模塊是正確的并被順利加載到addons路徑中.
-
為我們的模型添加新的字段
我們來擴展我們的todo.task模型, 添加2個新的字段:1.task的負責人 2.task的截止日期
創建'/todo_user/models'文件夾,其中創建'todo_task.py'文件,添加以下代碼
from odoo import models,fields,api
class TodoTask(models.Model):
_inherit = 'todo.task'
user_id = fields.Many2one('res.users', string='Responsible')
date_deadline = fields.Date('Deadline')
- 上面的代碼中,
_inherit
作為關鍵屬性:告訴了Odoo我們建立的'TodoTask'這個class是繼承并自'todo.task'. 注意點:_name
屬性在這里沒有出現,因為它已經從'todo.task'中被繼承了. - 最后的兩行代碼就是最為常見的字段聲明.
user_id
字段代表了來自'res.users'這個模型的用戶,它是一個Many2one
字段,從數據庫角度來說,就是一個外鍵的作用.date_deadline
是一個簡單的日期字段.我們更新我們的'todo_user'模塊后進入Technical | Database Structure | Models菜單.搜索todo.task模型會發現其中新增了我們剛添加的2個字段.
修改已經存在的字段
就像我們看到的,添加一個新的字段到我們已經存在的模型是相當直接的.從Odoo 8開始,修改模塊中已經存在的字段的屬性也是可以實現的,做法很簡單,添加一個與存在的字段名稱相同的字段,然后只修改字段的屬性的值就可以了.舉例來說,我們添加下列字段到我們的'todo_task.py'中:
name = fields.Char(help="What needs to be done?")
- 這行代碼在我們以前定義在'todo.task'模型中的name字段上添加了一個help屬性.我們更新我們的'todo_user'模塊,把鼠標懸停在'Description'上會有一段help的文字顯示.
修改模型的方法
繼承機制對業務邏輯層面同樣適用,添加新的方法跟添加字段一樣簡單:直接在class類中添加即可.
跟python的繼承類似,當我們適用Odoo的繼承機制時,我們在新的繼承類中定義與父類相同名字的函數同樣可以實現重寫.而且在繼承類中使用super()
函數來調用父類的方法.
注意點:重寫方法時,盡量不要修改傳入方法的參數列表,如果實在需要,可以使用關鍵字參數來實現.
- 我們的老的'Clear All Done'動作不適合我們現在的共享任務模塊,因為這個方法會改變所有用戶的任務(task)的'active'.因為我們需要修改這個方法為只能修改自己負責的任務的'active'屬性.
@api.multi
def do_clear_done(self):
domain = [('is_done', '=', True),
'|', ('user_id', '=', self.env.uid),
('user_id', '=', False)]
dones = self.search(domain)
dones.write({'active': False})
return True
- 上面的代碼很好理解,重寫了'do_clear_done'方法,在domain規則中,加入了判斷:
任務已經完成and(任務的負責人是當前用戶or任務的負責人沒有指定)
domain 規則是一系列代表判斷的tuple的list,中間的邏輯鏈接and
為'&'
,or
為'|'
.寫在tuple之前,默認的邏輯是'and' - 在上面的代碼里,我們完全重寫了父類中的方法,但是,我們平時在Odoo中不會這么做.我們應該使用額外的操作代碼來擴展父類中的方法,而不是對其原有結構直接更改.所以,我們通常需要使用super()方法來處理. 舉個例子:我們需要提高我們的'do_toggle_done()'方法,讓這個方法只能由task的負責人調用
from odoo.exceptions import ValidationError
@api.multi
def do_toggle_done(self):
for r in self:
if r.user_id != self.env.user:
raise ValidationError(
'Only the responsible can do this!'
)
return super(TodoTask, self).do_toggle_done()
- 上面代碼中,我們使用了Odoo自定義的異常類ValidationError.當用戶不是task負責人時,用戶點擊頁面上的'Do_Toggle_Done'就會直接把該異常拋出.頁面就會彈出一個警告框.
擴展views視圖
表單(forms),列表(list),搜索(search)視圖都是被'arch'所定義的XML結構.為了擴展視圖,我們需要修改這些xml,這就意味著要定位xml元素然后對它們進行修改.
一個繼承的視圖代碼:
<record id="view_form_todo_task_inherited" model="ir.ui.view">
<field name="name">Todo Task form Inherited</field>
<field name="model">todo.task</field>
<field name="inherit_id"
ref="todo_app.view_form_todo_task"/>
<field name="arch" type="xml">
<! -- ..match and extend elements here! ..-->
</field>
</record>
-
inherit_id
字段定義了需要被繼承的視圖.通過ref
屬性傳入被繼承的視圖的外部ID(External identifiers) - 使用Xpath來定位XML元素是最合適的,舉例來說,定位到
<field name="is_done">
這個元素可以使用表達式//field[@name] = 'is_done'
來實現. - 如果一個Xpath表達式匹配了多個元素,只有第一個匹配到的元素會被修改,所以最好使用獨一無二的元素屬性去匹配.通常在Odoo中我們使用
name
屬性去進行匹配,因此,為元素加上name
屬性是非常重要的. - 一旦找到我們需要定位的元素,我們可以修改或者直接添加XML元素來把我們新增的字段放入其中.以我們的繼承視圖為例,可以把'date_deadline'字段添加在'is_done'前面
<xpath expr="http://field[@name]='is_done'" position="before"/>
<field name="date_deadline"/>
</xpath>
- 幸運的是,Odoo提供了縮寫形式,大多數時候,我們可以避免每次都使用<xpath expr=...>這樣的表達式,我們可以使用與元素類型相關的信息和它的獨特屬性來進行定位,上述代碼可以改寫為:
<field name="is_done" position="before">
<field name="date_deadline"/>
</field>
- 注意,當一個字段多次出現在同一個view中,我們還是需要使用Xpath 表達式,因為縮寫形式在查找到第一個元素后就停止繼續定位了。
position
屬性通常有下面的幾個值:- after 添加到匹配節點的后面
- before 添加到匹配節點之前
- inside(默認值) 添加在節點里(一般與<group>之類的一起使用)
- replace 代替匹配到的節點,如果使用空內容,相當于刪除了匹配到的元素。
- attributes 修改匹配元素的屬性。使用
<attribute name="attr-name">
來進行新屬性的設置舉例:
<field name="active" position="attributes">
<attribute name='invisible'>1</attribute>
</field>
這段代碼表示把'acitve'這個字段隱藏起來
- 實際的開發中,我們使用添加'invisible'這個屬性來讓字段在頁面隱藏而盡量避免使用'replace',因為'replace'會刪除我們定位到的節點(有時候這些節點只是作為一個占位符,當replace刪除后會改變整個視圖的結構)
擴展form視圖
編寫views/todo_task.xml,
添加的完整的form視圖代碼如下:
<record id="view_form_todo_task_inherited" model="ir.ui.view">
<field name="name">Todo Task form Inherited</field>
<field name="model">todo.task</field>
<field name="inherit_id"
ref="todo_app.view_form_todo_task"/>
<field name="arch" type="xml">
<xpath expr="http://field[@name='is_done']" position="after">
<field name="date_deadline"></field>
</xpath>
<xpath expr="http://field[@name='name']" position="after">
<field name="user_id"></field>
</xpath>
<xpath expr="http://field[@name='active']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
最后在__manifest__.py
中添加todo_task.xml
到data屬性
'data':['views/todo_task.xml'],
擴展tree跟search視圖
與form視圖一樣,運用inherit_id
這個field來實現繼承,在tree視圖中,我們添加user_id字段。
<record id="view_tree_todo_task_inherited" model="ir.ui.view">
<field name="name">Todo Task tree Inherited</field>
<field name="model">todo.task</field>
<field name="inherit_id" ref="todo_app.view_tree_todo_task"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="user_id"/>
</field>
</field>
</record>
在search視圖我們添加2個新的過濾條件:1.用戶自己的任務。2. 那些沒有負責人的任務
<record id="view_filter_todo_task_inherited" model="ir.ui.view">
<field name="name">Todo Task filter Inherited</field>
<field name="model">todo.task</field>
<field name="inherit_id" ref="todo_app.view_filter_todo_task"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="user_id"/>
<filter name="filter_my_tasks" string="My Tasks"
domain="[('user_id','in',[uid,False])]"/>
<filter name="filter_not_assigned" string="Not Assigned"
domain="[('user_id','=',False)]"/>
</field>
</field>
</record>
模型繼承機制的更多介紹
- 我們剛才看到的是最為基礎的模塊擴展,在Odoo官方文檔中我們稱為class inheritance.這是繼承最為頻繁的使用,也是最好理解的,即直接對現有模塊進行擴展(in-place extension).當我們增加新字段,新功能的同時,這些新功能也被增加到原來已經存在的模型中.并沒有創建一個新的模型.
- 我們能夠繼承多個父類模型通過使用
_inherit
屬性._inherit
能夠接收一個列表來置入我們所要繼承的父類的名稱.通過這種方式,我們能夠創建一個混合類(mixin classes)。混合類可以看作是那些能夠提高基礎功能的模型集合。它們不是直接被使用的,而是像一個容器,里面有許多功能,當你想要添加這些功能時就能進行相應的擴展。 - 假如我們在繼承的子模型中使用了不同于父模型的
_name
,我們就會在數據庫中創建一個與父模型功能完全一樣的模型,但是這個新模型會有自己的數據庫表結構跟數據,官方文檔稱為原型繼承(prototype inheritance)。當你在新的模型中添加功能時,新功能只會添加到新模塊中。原來的父類模塊并不會發生改變。 - 還有一種我們稱為delegation inheritance的方法(也成實例繼承,instance inheritance),允許將模型的每條記錄鏈接到父模型的記錄,并且提供對父記錄的透明訪問。使用了
_inherits
這個屬性 - 這個繼承方式可以這么理解:我們現在需要有一個'teacher'模型跟一個'student'模型,這兩個模型都需要被繼承自'res.partner'這個模型.我們使用
_inherites
讓我們新創建的兩個模型擁有自己的數據庫表結構,分別在兩個模塊中添加新字段或者功能時只會寫入對應模塊的數據庫中,而不會改變'res.partner'模塊.
我們下面舉例子來進行說明
使用prototype inheritance來復制功能
我們在前面的擴展模型只使用了_inherite
屬性,我們定義了一個模型去繼承'todo.tasl'模型,添加了一些功能,但我們沒有使用_name
.所以繼承模塊還是使用todo.task的數據庫表結構.
- 現在,我們使用
_name
屬性,這會讓我們創建一個新的數據庫來復制被繼承模型的所用功能.例如:
from odoo import models
class TodoTask(models.Model):
_name = 'todo.task'
_inherit = 'mail.thread'
這個例子很好理解,我們通過_inherit
把'mail.thread'這個模塊的所有信息復制到了'todo.task'模型的數據庫表結構中.
復制意味著父模塊中的所有方法跟字段都被添加到子模塊中.我們的例子里就是把'mail.thread'定義的方法跟字段都添加到了'todo.task'.但是'mail.thread'跟'todo.task'都擁有自己獨立的數據庫表結構,它們之間沒有任何聯系.只是共享了fields的定義而已.
通過委托繼承(delegation inheritance)植入模型
委托不是那么常用,但是它能提供很多方便的繼承解決方法。它使用_inherites
屬性通過字典映射繼承模塊與字段之間的關系.一個很好的例子就是我們的標準用戶模型,'res.users',它植入了一個Partner模型.
from odoo import models, fields
class User(models.Model):
_name = 'res.users'
_inherits = {'res.partner': 'partner_id'}
partner_id = fields.Many2one('res.partner')
- 使用委托繼承,'res.users'模型中植入了繼承模型'res.partner'.當一個新的User被創建時,一個新的partner也被創建了,然后通過'partner_id'這個字段把partner關聯到了user類中,這與面向對象編程的多態有點類似。
- 通過委托繼承,所有的繼承模型中跟Partner中的字段都是可以被獲取的因為它們就存在于User的字段中。舉例來說,Partner的name跟address字段都顯示為User中的字段,實際上它們被存儲在相關聯的Partner模型中。并不會發生數據重復
- 相比于原型繼承,委托繼承的優勢在于它不會產生數據重復。當一個新的模型需要添加地址字段時就可以直接使用委托繼承來植入Partner模型。而當Partner模型中的地址發生改變時,所有與之有關聯的模型中的地址都會改變。
- 注意,使用委托繼承時,字段是可以繼承的,但是方法不能。
添加社交網絡功能
社交網絡模塊(mail)在form視圖的底部提供了消息板跟關注者功能。我們經常需要添加這些消息傳遞邏輯到我們的模塊,下面就開始操作
- 添加模型依賴到
__manifest__.py
的depned
中
'depends':['todo_app','mail']
- 繼承mail.thread模型,mail.thread模型是一個抽象模型,它沒有實際的數據庫表結構,是用來作為混合類來添加想要的功能來使用的
_name = 'todo.task'
_inherit = ['todo.task','mail.thread']
- 添加關注者窗口化部件到form視圖中,把下面的xml代碼插入到我們前面編寫的form繼承視圖
view_form_todo_task_inherited
的'arch'元素下
<sheet position="after">
<div class="oe_chatter">
<field name="message_follower_ids"
widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</sheet>
- 為關注者設置記錄規則
修改數據記錄
不同于views中的xml,普通的數據記錄沒有XML arch結構,無法被Xpath定位到,但我們任然可以通過<record id='x' model='y'>來插入或者修改普通數據記錄。(x不存在就是插入,存在即為修改)
修改菜單,動作記錄。
<!--modify menu item-->
<record id="todo_app.menu_todo_task" model="ir.ui.menu">
<field name="name">My To-Do</field>
</record>
<record model="ir.actions.act_window"
id="todo_app.action_todo_task">
<field name="context">
{'search_default_filter_my_tasks':True}
</field>
</record>
- 我們既然使用了新的繼承類,順便使用上面的代碼來修改以前在todo_app模塊的菜單的名稱。
- 動作視圖中有一個可選參數為
context
,這個參數可以為視圖中的字段跟過濾器提供默認值。在這里,我們使用它來為設定默認的過濾條件為以前設置過的My Tasks過濾器。注意這里以search_default_
作為前綴.
修改安全記錄規則
前一個章節中,我們的todo_app的記錄規則在于創立task的用戶才能看到對應的task,現在由于社交功能的加入,只要是task的關注者,都能看到該task.
我們創建/todo_user/security/todo_access_rules.xml文件,添加以下代碼。
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="1">
<record id="todo_app.todo_task_per_user_rule"
model="ir.rule">
<field name="name">ToDo Tasks for owner and followers</field>
<field name="model_id" ref="model_todo_task"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="domain_force">
['|',('user_id','in',[user.id,False]),
('message_follower_ids','in',[user.partner_id.id])]
</field>
</record>
</data>
</odoo>
- 規則記錄運行在當前用戶的上下文中,因為task的關注者是partners,所以需要使用user.partner_id來代替user.id
- groups字段是一個一對多的關系, 4在這里代表把base.group_user這個模型中的所有記錄添加到一個list里
- 我們重新添加了domain規則。
task只在以下情況顯示:1.task的負責人不存在或者是當前用戶
2.當前用戶是關注者其中一個('message_follower_ids','in',[user.partner_id.id])這里表示我們的