Odoo開發(fā)入門課程的學生資料, 參考書點擊這里.
利用繼承機制, Odoo可以在不直接修改底層對象的情況下為應用增加特性, 不需要修改已存在的模塊, 而是通過創(chuàng)建新的模塊來實現(xiàn)對模塊的修改. 繼承可以應用在所有的級別, 例如模型, 視圖, 業(yè)務邏輯.
本文通過繼承機制為todo_app增加社交和消息特性.
創(chuàng)建一個名為todo_user的odoo模塊, 創(chuàng)建__init__.py文件以及__openerp__.py, 修改__openerp__.py文件內(nèi)容為:
{ 'name': 'Multiuser To-Do', 'description': 'Extend the To-Do app to multiuser.', 'author': 'Jeff Zhang', 'depends': ['todo_app'],}
設置->更新模塊列表->本地模塊查找”todo_user”, 安裝該模塊.
Odoo中實體模型使用Python類來定義, 擴展模型時也是使用Python類, 但是需要使用Odoo的一個特殊機制.
為了擴展一個已有的模型, 需要使用一個具有_inherit屬性的Python類來定義. _inherit屬性指明擴展/繼承哪個模型. 新的模型繼承了父模型的所有特性, 在新類中定義我們期望的特性即可.
在Odoo中, 模型是獨立于特定模塊的, 可以通過self.env[
當服務啟動后模塊載入時, 任何擴展的模型修改只會影響之前載入的模塊, 所以載入順序很重要需要確認模塊依賴關系是正確的.
擴展todo.task模型, 增加以下字段: 任務的責任人, 最后期限日期.
創(chuàng)建models子目錄, 并在其中創(chuàng)建todo_task.py文件, 內(nèi)容如下:
# -*- coding: utf-8 -*-#!/usr/bin/env pythonfrom openerp import models, fields, apiclass TodoTask(models.Model): _inherit = 'todo.task' user_id = fields.Many2one('res.users', 'Responsible') date_deadline = fields.Date('Deadline')
注意_inherit屬性, 此處其值為’todo.task’, 說明這個類繼承自todo.task模型. 使用了_inherit屬性, 那么_name屬性不需要了.
在models下創(chuàng)建__init__.py文件, 內(nèi)容如下:
# -*- coding: utf-8 -*-#!/usr/bin/env pythonimport todo_task
在模塊根目錄下的__init__.py中增加:
from models import todo_task
更新模塊, 在技術特性中查看模型, 可以看到模型todo.task中增加了的新字段.
除了通過繼承增加新的字段, 自odoo8.0開始, 還可以修改已存在的字段的屬性, 只需簡單地添加相同名稱的字段并設置字段屬性值即可.
例如, 為了改變name字段的help tooltip, 在todo_task.py中增加一句即可:
name = fields.Char(help="What needs to be done?")
升級模塊, 查看模型, 可以發(fā)現(xiàn)修改后的變化.
繼承還可以應用在業(yè)務邏輯上, 例如向模型添加新的方法, 簡單地在派生類中定義相應的函數(shù)即可.
如果希望擴展已存在的業(yè)務邏輯, 可以定義一個同名的方法覆蓋已有模型中的方法, 并且在這個派生類的方法中還可以使用Python的super關鍵字來訪問其覆蓋的父方法.
示例: 覆蓋To-Do應用模型中的do_clean_done方法, 該方法之前的業(yè)務邏輯是將所有is_done為True的記錄的active字段賦值為False, 原來的代碼如下:
@api.multidef do_clear_done(self): done_recs = self.search([('is_done', '=', True)]) done_recs.write({'active': False}) return True
由于現(xiàn)在希望每個用戶只能操作他自己的任務, 所以需要修改do_clean_done的業(yè)務邏輯, 修改后的代碼如下:
@api.multidef do_clear_done(self): domain = [('is_done', '=', True), '|', ('user_id', '=', self.env.uid), ('user_id', '=', False)] done_recs = self.search(domain) done_recs.write({'active': False}) return True
發(fā)生的修改很簡單, 就是將之前的記錄過濾條件由僅僅考慮is_done為True改為is_done為True并且user_id為當前用戶或user_id為空(False).
odoo中過濾條件使用一個domain來描述, 一個domain用包含多個條件的列表來描述. 每個條件用一個元組來表示, 各個條件之間為’與’的關系. 如果希望表達’或’的關系, 先使用’|’, 在’|’后面的兩個元組為’或’關系的兩個條件表達式.
這樣, 新的方法覆蓋了父方法. 更新并升級模塊, 還看不出效果, 因為現(xiàn)在所有任務的user_id還都是False.
除了上例中完整的替換某個業(yè)務邏輯外, 更多的情況是在不破壞已有業(yè)務邏輯并利用它的情況下擴展它, 比如增加預處理或后處理等. 下面的代碼在現(xiàn)有的標記任務完成的業(yè)務邏輯基礎上, 增加一個用戶身份的判斷:
@api.onedef do_toggle_done(self): if self.user_id != self.env.user: raise Exception('Only the responsible can do this!') else: return super(TodoTask, self).do_toggle_done()
使用Python的super()方法可以調用父類中的方法, super相關信息如下:
Help on class super in module __builtin__:class super(object) | super(type) -> unbound super object | super(type, obj) -> bound super object; requires isinstance(obj, type) | super(type, type2) -> bound super object; requires issubclass(type2, type) | Typical use to call a cooperative superclass method: | class C(B): | def meth(self, arg): | super(C, self).meth(arg)
在odoo中, 視圖繼承的形式如下:
<record id="view_form_todo_task_inherited" model="ir.ui.view"> <field name="name">Todo Task form – User extension</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.
外部ID跨模塊地唯一標識一個資源, 更多的信息在后續(xù)的內(nèi)容中介紹.
Form, Tree/List以及Search視圖都是使用arch的XML結構來定義的, 為了修改這個XML, 需要定位XML元素. 在XML中定位元素最直接的方法是使用XPath表達式, 例如
//field[@name]='is_done'
在此基礎上, 如果希望在is_done之前增加date_deadline字段, 可以使用下面的XML實現(xiàn):
<xpath expr="http://field[@name]='is_done'" position="before"> <field name="date_deadline" /></xpath>
除了XPath外, odoo還提供了更加方便的標記方法, 可以避免使用XPath, 上面的XML可以用簡化為下面的內(nèi)容:
<field name="is_done" position="before"> <field name="date_deadline" /></field>
<field>用于確定新字段顯示位置的定位, 除此之外, <sheet>, <group>, <div>等都有類似特性.
相應地, name屬性是經(jīng)常用到的資源查找方式, 但是也可以使用其他的方式, 例如如果需要查找顯示某些特定字符串的label或者CSS類名, 就可以使用string.
position屬性用于指定新增元素的位置, 可以有以下的選擇:
1.after: 在匹配元素之后, 與匹配元素具有相同的父元素;2.befor: 在匹配元素之前, 與匹配元素具有相同的父元素;3.inside(默認值): 作為匹配元素內(nèi)容;4.replace: 替換匹配元素的內(nèi)容, 如果值為空將會刪除匹配的元素;5.attributes: 修改匹配元素的屬性.
position屬性的這幾種可選值大部分容易理解, 只對attributes進行說明. 如果position屬性設定為attributes, 那么需要使用<attribute name=”attr-name”>value</attribute>設定該屬性的新值. 例如下面的代碼將invisible屬性設置為1:
<field name="active" position="attributes"> <attribute name="invisible">1</attribute></field>
新建一個views文件夾, 在其中新增一個todo_view.xml文件, 將基本視圖擴展代碼寫入其中:
<?xml version="1.0"?><openerp> <data> <record id="view_form_todo_task_inherited" model="ir.ui.view"> <field name="name">Todo Task form – User extension</field> <field name="model">todo.task</field> <field name="inherit_id" ref="todo_app.view_form_todo_task"/> <field name="arch" type="xml"> <field name="name" position="after"> <field name="user_id" /> </field> <field name="is_done" position="before"> <field name="date_deadline" /> </field> <field name="name" position="attributes"> <attribute name="string">I have to...</attribute> </field> </field> </record> </data></openerp>
最后, 在__openerp__.py中增加data描述:
'data': ['views/todo_view.xml'],
更新模塊, 創(chuàng)建新任務, 發(fā)現(xiàn)新增特性已經(jīng)應用了, 如下圖.
新增一個任務, 然后設定為完成, 切換到管理員角色可以看到包括已完成任務在內(nèi)的所有任務, 但是現(xiàn)在點擊清除已完成任務, 不會將非admin用戶的任務的activity設置為False, 滿足預期的業(yè)務邏輯要求.
但現(xiàn)在存在一個問題: 如果使用admin用戶創(chuàng)建一個任務, 并將Responsible設定為另一個用戶(例如aaa), 以aaa登錄并不能看到這個由admin分配的任務. 這是由于之前設定了行級訪問規(guī)則, 其中的下面這一句設定導致不能顯示, 這個問題在本文最后一部分解決.
<field name="domain_force"> [('create_uid','=',user.id)]</field>
向todo_view.xml中增加下面的內(nèi)容擴展List/Tree視圖:
<record id="view_tree_todo_task_inherited" model="ir.ui.view"> <field name="name">Todo Task tree – User extension</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>
向todo_view.xml中增加下面的內(nèi)容擴展搜索視圖:
<record id="view_filter_todo_task_inherited" model="ir.ui.view"> <field name="name">Todo Task tree – User extension</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>
更新模塊, 看效果.
前面介紹的模型繼承是最基本的模型繼承方式, 但是還有更強力的混合類繼承和代理繼承方式. 前者通過繼承將某個模型的所有功能特性加入另一個模型中, 后者通過一種透明的方式訪問另一個模型中的數(shù)據(jù).
之前使用_inherit屬性來擴展一個模型, 定義一個繼承todo.task模型的類, 然后向該其添加了一些特性, 這種方法中類的_name屬性沒有進行設定, 仍然是todo.task.
如果使用模型的_name屬性, 可以創(chuàng)建混合類(mixin class).
from openerp import modelsclass TodoTask(models.Model): _name = 'todo.task' _inherit = 'mail.thread'
以上代碼將模型mail.thread的特性拷貝到模型todo.task, 使todo.task具有了mail.thread的功能特性. mail.thread實現(xiàn)了消息和關注功能, 這個功能是可重用的, 所以可以非常方便地將該特性添加到其他模型上.
上面提到的拷貝意味著繼承的新模型中可以獲得被繼承的所有字段和方法, 也即被繼承模型中的字段也將在繼承的新模型對應的數(shù)據(jù)庫表中創(chuàng)建, 并且新模型中也包括未繼承前的所有字段. 舊模型和繼承后的新模型的數(shù)據(jù)被分別保存, 不會產(chǎn)生沖突. 下圖是一個示意圖.
上圖中最上方可以看到一個名為”gogogo”的任務中, 用戶aaa發(fā)送了兩條消息給所有關注該任務的用戶. 在數(shù)據(jù)庫的todo_task表(舊模型)中, 只保存舊的字段, 而社交網(wǎng)絡數(shù)據(jù)則保存在mail_message表(新模型中包括)中并與todo_task中的記錄關聯(lián)起來.
混合模型一般用于抽象模型, 例如mail.thread. 抽象模型與通常的模型類似, 只是抽象模型不在數(shù)據(jù)庫中創(chuàng)建相應的數(shù)據(jù)表, 所以抽象模型更像是一個模板, 描述重用于普通模型時的字段和業(yè)務邏輯, 抽象模型中定義的字段只會在繼承自它的普通模型中創(chuàng)建.
一般使用混合(mixin)繼承時, 很少繼承自普通模型, 因為這種繼承機制將會復制相同的數(shù)據(jù)結構. 如果必須繼承普通模型, 那么應該使用odoo提供你的代理繼承機制, 這種機制能夠避免數(shù)據(jù)結構的復制.
代理繼承使用_inherits屬性(多一個s)將繼承的模型映射到字段.
from openerp import models, fieldsclass User(models.Model): _name = 'res.users' _inherits = {'res.partner': 'partner_id'} partner_id = fields.Many2one('res.partner')
例如以上的代碼是標準的Users模型(res.users), 其中嵌入了了一個Partner模型. 當創(chuàng)建一個新的User時, 一個Partner將會同時創(chuàng)建, 并且partner_id中將會保存這個Partner的引用, 這種機制類似OO中的多態(tài)概念.
嵌入的模型(Partner)中的所有字段就像是User的字段一樣, 可以以相同的形式直接使用. 例如, partner的name和address字段使用起來與使用User本身的字段一樣, 只不過它們的數(shù)據(jù)實際上保存在關聯(lián)的Partner模型中. 這樣沒有數(shù)據(jù)結構的復制.
注意, 代理繼承中只有字段被繼承, 方法是不能繼承的!
社交網(wǎng)絡模塊(技術名稱mail)提供的消息板可以在很多頁面的底部看到, 也被稱作Open Chatter.
社交網(wǎng)絡消息功能通過mail模塊的mail.thread模型提供, 這是一個抽象模型, 可以用以下步驟將其添加到其他模型:
Step1, todo_task依賴于todo_app, 而todo_app依賴于mail, 所以不需要再做;
Step2, 之前我們的模型已經(jīng)使用_inherit繼承了todo.task模型, 因為odoo的_inherit可以接受多個模型繼承, 使用列表即可, 所以對todo_task.py進行如下修改:
由:_inherit = 'todo.task'改為:_name = 'todo.task'_inherit = ['todo.task', 'mail.thread']
注意: 創(chuàng)建抽象模型: 派生自models.AbstractModel而不是models.Model即可.
Step3, 使用視圖繼承將社交網(wǎng)絡小組件(widgets)擺放在form底部, 打開todo_view.xml, 添加下面的內(nèi)容到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>
message_follower_ids和message_ids這兩個字段在mail.thread模型中定義.
更新模塊, 看效果. 關注一個任務, 然后登錄其他用戶打開這個任務, 在下方的社交組件處向關注這個任務的用戶發(fā)送消息, 其他所有關注該任務的用戶都可以收到這條消息了.
與視圖不同, 一般數(shù)據(jù)記錄沒有XML的arch結構, 所以不能通過XPath表達式擴展和修改. 想要修改這些數(shù)據(jù), 需要使用另一種方式.
利用<record id=”x” model=”y”>可以完成模型上的插入或更新操作: 如果x不存在, 插入記錄; 否則, 對其進行更新.
例如下面的代碼修改菜單項, 將其添加到todo_user模塊的todo_view.xml中:
<!-- Modify menu item --><record id="todo_app.menu_todo_task" model="ir.ui.menu"> <field name="name">My To-Do</field></record><!-- Action to open To-Do Task list --><record model="ir.actions.act_window" id="todo_app.action_todo_task"> <field name="context"> {'search_default_filter_my_tasks': True} </field></record>
在之前的To-Do引用中, 應用了一條行級訪問規(guī)則, 使得任務只對創(chuàng)建它的用戶可見, 但是現(xiàn)在增建了社交網(wǎng)絡, 用戶之間可以加入?yún)f(xié)作, 任務需要對其他的用戶可見, 例如被分配為任務責任人的讓用戶或關注任務的用戶.
這里需要做的工作于上一節(jié)修改菜單項類似: 重寫todo_app.todo_task_user_rule, 修改domain_force字段為新的值(現(xiàn)在是任務創(chuàng)建用戶ID與當前用戶相同). 但是由于todo_app中設定了<data no_update=”1”>, 所以不能進行寫操作, 這里需要做的是: 刪除之前的行級訪問規(guī)則并且創(chuàng)建一個新的.
為了保持模塊組織性, 新建一個security子目錄并在其中創(chuàng)建一個名為todo_access_rules.xml的文件, 內(nèi)容如下:
<?xml version="1.0" encoding="utf-8"?><openerp> <data noupdate="1"> <delete model="ir.rule" search="[('id', '=', ref('todo_app.todo_task_user_rule'))]" /> <record id="todo_task_per_user_rule" model="ir.rule"> <field name="name">ToDo Tasks only for owner</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></openerp>
以上代碼首先刪除已有的todo_task_user_rule記錄, 然后創(chuàng)建一條新的記錄.需要注意的是對于Followers的處理, 因為followers是partners, 并不是User對象, 所以需要使用user.partner_id.id而不是user.id.
還可以在開發(fā)時將<data noupdate="1">改為<data noupdate="0">, 正式發(fā)布時再改回來.
最后, 修改__openerp__.py中的data屬性:
'data': [ 'views/todo_view.xml', 'security/todo_access_rules.xml', ],
更新模塊, 看效果吧, 之前提到的由于行級訪問控制規(guī)則引起的現(xiàn)象沒有了, 一切都如計劃運行.