本章代碼位於作為GITHUB庫 https://github.com/PacktPublishing/Odoo-14-Development-Cookbook-Fourth-Edition
在第五章(服務側開發-基礎篇)中,我們了解了如何在類中創建函數,如何從繼承的類擴展函數以及如何處理數據集。本章將會討論一些更進一步的內容,比如處理數據集的上下文,通過按鈕點擊觸發函數,處理onchange函數。本章將包含如下內容:
-
更改執行動作的用戶
-
通過編輯過的上下文執行方法
-
執行原生SQL查詢
-
為用戶編寫操作向導
-
定義onchange方法
-
在服務器端調用onchange方法
-
通過計算方法定義onchange
-
基於SQL視圖定義模型
-
添加用戶配置選項
-
實現在模塊安裝時的函數(個人叫它鈎子函數)
更改執行動作的用戶
當我們寫一些業務邏輯的時候,你可能需要通過不同的上下文執行動作。典型的場景是通過superuser用戶越過權限控制。有這樣一些場景,我們需要操作一些我們並沒有權限的數據。
本節將介紹普通用戶如何通過sudo()實現修改圖書的狀態。
准備
為了理解容易,我們新建一個管理圖書借閱的模型,library.book.rent。
class LibraryBookRent(models.Model):
_name = 'library.book.rent'
book_id = fields.Many2one('library.book', 'Book', required=True)
borrower_id = fields.Many2one('res.partner', 'Borrower', required=True)
state = fields.Selection([('ongoing', 'Ongoing'), ('returned', 'Returned')],required=True)
rent_date = fields.Date(default=fields.Date.today)
return_date = fields.Date()
你需要添加form視圖、菜單、動作等以可以在前台UI看到相關內容,實現交互。
步驟
如果您測試了該模塊,您將發現只有擁有圖書管理員訪問權限的用戶才能將圖書標記為借閱。非圖書管理員用戶不能自己借書;他們需要問圖書管理員用戶。假設我們想添加一個新功能,這樣非圖書管理員用戶就可以自己借書了。我們將在不給他們訪問權限的情況下執行此操作library.book.rent模型。
- 添加book_rent()
class LibraryBook(models.Model):
_name = 'library.book'
...
def book_rent(self):
- 確保數據為一
self.ensure_one()
- 如果一本書無法借閱,則發出警告(請確保在頂部導入了UserError)
if self.state != 'available':
raise UserError(_('Book is not available for renting'))
- 獲取的空記錄:
rent_as_superuser = self.env['library.book.rent'].sudo()
- 使用適當的值創建新的圖書借閱記錄:
rent_as_superuser.create({
'book_id': self.id,
'borrower_id': self.env.user.partner_id.id,
})
- 要從用戶界面觸發此方法,請將按鈕添加到書本的窗體視圖:
<button name="book_rent" string="Rent this book" type="object" class="btn-primary"/>
重新啟動服務器並更新給定的更改。更新后,您將在“書本窗體”視圖上看到“租用本書”按鈕。當你點擊它,將創建新的租金記錄。這也適用於非圖書管理員用戶。您可以作為演示用戶訪問Odoo來測試這一點。
原理
在前三個步驟中,我們添加了名為book_rent()的新方法。當用戶單擊“書本窗體”視圖上的“租用本書”按鈕時,將調用此方法。
在步驟4中,我們使用sudo()。此方法返回一個新的記錄集,其中包含用戶具有超級用戶權限的已修改環境。調用記錄集時使用sudo(),環境會將環境屬性修改為su,它指示環境的超級用戶狀態。您可以通過記錄集.env.su. 通過此sudo記錄集的所有方法調用都是使用超級用戶權限進行的。要更好地了解這一點,請從方法中刪除.sudo(),然后單擊“租這本書”按鈕。它將引發訪問錯誤,用戶將無法再訪問模型。簡單地使用sudo()將繞過所有安全規則。
如果需要特定用戶,可以傳遞包含該用戶或該用戶的數據庫ID的記錄集,如下所示:
public_user = self.env.ref('base.public_user')
public_book = self.env['library.book'].with_user(public_user)
public_book.search([('name', 'ilike', 'cookbook')])
更多
使用sudo(),可以繞過訪問權限和安全記錄規則。有時您可以訪問要隔離的多個記錄,例如多公司環境中來自不同公司的記錄。sudo()記錄集繞過Odoo的所有安全規則。
如果不小心,在此環境中搜索的記錄可能會鏈接到數據庫中的任何公司,這意味着您可能正在向用戶泄漏信息;更糟的是,您可能會通過鏈接屬於不同公司的記錄而悄悄地損壞數據庫。
重要提醒
使用sudo()時,請始終仔細檢查以確保對search()的調用不依賴標准記錄規則來篩選結果。
通過編輯過的上下文執行方法
上下文是記錄集環境的一部分。它用於從用戶界面傳遞額外的信息,例如時區和用戶的語言。還可以使用上下文傳遞操作中指定的參數。標准Odoo附加組件中的許多方法使用上下文來根據這些上下文值調整其業務邏輯。有時需要修改記錄集上的上下文,以便從方法調用中獲得所需的結果或計算字段的所需值。
這個配方將展示如何根據環境上下文中的值更改方法的行為。
准備
對於本節,我們將使用上一節中的my_library模塊。在library.book.rent模型視圖中,我們將添加一個按鈕來標記該書為丟失,以防普通用戶丟失一本書。注意,我們在書的表單視圖中已經有了相同的按鈕,但是在這里,我們將有一個稍微不同的行為來理解Odoo中上下文的使用。
步驟
- 更新狀態字段的定義,使其具有丟失狀態:
state = fields.Selection([ ('ongoing', 'Ongoing'),
('returned', 'Returned'),
('lost', 'Lost')],
'State', default='ongoing', required=True)
- 在窗體視圖中添加“標記為丟失”按鈕:
<button name="book_lost" string="Lost the Book"
states="ongoing" type="object"/>
- 添加book_lost()方法
def book_lost(self):
···
- 在方法中,確保我們對單個記錄執行操作,然后更改狀態:
self.ensure_one() self.sudo().state = 'lost'
- 在方法中添加以下代碼以更改環境的上下文,並調用該方法將書本的狀態更改為lost:
book_with_different_context = self.book_id.with_context(avoid_deactivate=True) book_with_different_context.sudo().make_lost()
- 更改library.book模型具有不同的行為:
def make_lost(self):
self.ensure_one()
self.state = 'lost'
if not self.env.context.get('avoid_deactivate'):
self.active = False
原理
在步驟1中,我們為該書添加了一個新狀態。這個新狀態將顯示丟失的書。在步驟2中,我們添加了一個新按鈕,markaslost。用戶將使用此按鈕報告丟失的書。
在第3步和第4步中,我們添加了一個方法,當用戶單擊markaslost時將調用該方法。
第5步調用帶有某些關鍵字參數的self.book_id.with_context()。這將返回具有更新上下文的book_id記錄集的新版本。我們在這里的上下文中添加了一個鍵,avoid_deactivate=True,但是如果需要,可以添加多個鍵。我們在這里使用了sudo(),因此非圖書管理員用戶可以將書籍報告為丟失。
在第6步中,我們檢查了上下文中的avoid_deactivate鍵是否為正值。我們避免停用圖書,這樣圖書管理員即使丟了也能看到。
現在,當圖書管理員在“圖書窗體”視圖中報告丟失的圖書時,圖書記錄狀態將更改為“丟失”,圖書將被存檔。但是,當非圖書管理員用戶在其租金記錄中報告丟失的圖書時,圖書記錄狀態將更改為“丟失”;圖書將不會存檔,以便圖書管理員以后查看。
這只是一個修改上下文的簡單示例,但是您可以根據自己的需求在ORM中的任何地方使用它,即對象關系映射(Object Relational Mapping)的縮寫。
更多
也可以通過上下文()將字典傳遞給。在本例中,字典用作新上下文,它將覆蓋當前上下文。因此,步驟5也可以寫為:
new_context = self.env.context.copy()
new_context.update({'avoid_deactivate': True})
book_with_different_context = self.book_id.with_context(new_context)
book_with_different_context.make_lost()
執行原生SQL查詢
大多數時候,您可以通過使用Odoo的ORM來執行您想要的操作。例如,您可以使用search()方法來獲取記錄。然而,有時,你需要更多;要么您無法使用域語法表達您想要的內容(對於域語法,有些操作是棘手的,如果不是完全不可能的話),要么您的查詢需要多次調用search(),這最終導致效率低下。
准備
對於本節,我們將使用上節中的my_library模塊。為簡單起見,我們將只在日志中打印結果,但在實際場景中,您將需要在業務邏輯中使用查詢結果。在第9章“后端視圖”中,我們將在用戶界面中顯示該查詢的結果。
步驟
要獲得關於用戶保存特定圖書的平均天數的信息,您需要執行以下步驟:
- 將average_book_occupation()方法添加到library.book:
def average_book_occupation(self): ...
- 將尚未提交到數據庫的計算提交
self.flush()
- SQL代碼如下
sql_query = """ SELECT
lb.name,
avg((EXTRACT(epoch from age(return_date, rent_ date)) / 86400))::int
FROM
library_book_rent AS lbr
JOIN
library_book as lb ON lb.id = lbr.book_id
WHERE lbr.state = 'returned' GROUP BY lb.name;"""
- 執行查詢
self.env.cr.execute(sql_query)
- 獲取數據並打印到日志
result = self.env.cr.fetchall()
logger.info("Average book occupation: %s", result)
- 添加按鈕
<button name="average_book_occupation" string="Log Average Occ." type="object" />
原理
步驟1,我們添加了average_book_occupation()方法,當用戶單擊Log Average OCc. 按鈕時調用。
步驟2,我們使用了flush()方法。從Odoo13開始,ORM多度使用內容。在每一個事務中,ORM使用一個全局緩存。所以,有可能數據庫中的記錄與緩存中的記錄時不同的。通過flush()函數,可以確保所有在緩存中的更改同步到數據庫中。
步驟3,我們聲明一個SQL SELECT查詢。這將返回用戶持有某本書的平均天數。如果您在PostgreSQL CLI中運行這個查詢,您將得到一個基於圖書數據的結果。如下:
步驟4,對存儲在self.env.cr中的數據庫游標調用execute()方法。這會將查詢發送給PostgreSQL並執行它。
步驟5,使用游標的fetchall()方法來檢索所選行的列表的查詢。這個方法返回一個行列表。在我的例子中,這是[('Odoo 12 Development Cookbook', 33), ('PostgreSQL 10 Administration Cookbook', 81)]。從我們執行的查詢的形式中,我們知道每一行正好有兩個值,第一個是name,另一個是用戶持有某本書的平均天數。然后,我們簡單地記錄它。
步驟6,添加按鈕。
重要提醒
當在使用UPDATE語句進行更新的時候,我們需要手動將緩存功能置為無效。可通過self.invalidate_cache()。
更多
self.env.cr是對psycopg2游標的簡單封裝。如下是常用的一些函數:
- execute(query, params): 這將執行SQL查詢,查詢中標記為%s的參數將被params中的值替換,params是一個元組。
警告
不要嘗試自己通過%s的方式拼接SQL,這可能導致SQL注入的風險。
- fetchone(): 返回一行數據,元組格式。
- fetchall(): 返回所有的行,元組的序列。
- dictfetchall(): 返回所有的行,以列名和值的字典列表。
在使用原生SQL的時候需要特別小心
- 通過原生SQL將會跳過應用的權限驗證。一定要確保在search([('id','in',tuple(ids))])中的IDs列表是過濾掉用戶無權訪問的記錄。
- 你所做的任何修改都繞過了附加模塊設置的約束,除了NOT NULL, UNIQUE和FOREIGN KEY約束,這些約束在數據庫級別強制執行。對於任何計算字段重新計算觸發器,也都是如此,所以最終可能會破壞數據庫。
- 避免INSERT/UPDATE查詢,因為通過查詢插入或更新記錄將不會運行通過覆蓋create()和write()方法編寫的任何業務邏輯。它不會更新存儲的計算字段,ORM約束也會被繞過。
為用戶編寫操作向導
在第4章,應用模型,模型中使用可重用模型的抽象模型特性配方。引入了TransientModel基類。這個類與普通模型有很多共享,除了在數據庫中定期清理暫態模型的記錄之外,因此命名為transient。它們用於創建向導或對話框,這些向導或對話框由用戶在用戶界面中填充,通常用於對數據庫的持久記錄執行操作。
准備
對於本節,我們將使用前面食譜中的my_library模塊。這個配方將添加一個新的向導。有了這個向導,圖書管理員將能夠同時發行多本書。
步驟
- 向模塊中添加一個新的瞬態模型,定義如下:
class LibraryRentWizard(models.TransientModel):
_name = 'library.rent.wizard'
borrower_id = fields.Many2one('res.partner', string='Borrower')
book_ids = fields.Many2many('library.book', string='Books')
- 添加對瞬態模型執行操作的回調方法。將以下代碼添加到LibraryRentWizard類中:
def add_book_rents(self):
rentModel = self.env['library.book.rent']
for wiz in self:
for book in wiz.book_ids:
rentModel.create({
'borrower_id': wiz.borrower_id.id,
'book_id': book.id
})
- 為模型創建一個表單視圖。將以下視圖定義添加到模塊視圖中:
<record id='library_rent_wizard_form' model='ir. ui.view'>
<field name='name'>library rent wizard form view</ field>
<field name='model'>library.rent.wizard</field>
<field name='arch' type='xml'>
<form string="Borrow books">
<sheet>
<group>
<field name='borrower_id'/>
</group>
<group>
<field name='book_ids'/>
</group>
</sheet>
<footer>
<button string='Rent' type='object' name='add_book_rents' class='btn-primary'/>
<button string='Cancel' class='btn-default' special='cancel'/>
</footer>
</form>
</field>
- 創建向導的動作及菜單。
<act_window id="action_wizard_rent_books" name="Give on Rent"
res_model="library.rent.wizard"
view_mode="form" target="new" />
<menuitem id="menu_wizard_rent_books"
parent="library_base_menu" action="action_wizard_rent_books" sequence="20" />
- 添加訪問權限控制
acl_library_rent_wizard,library.library_rent_ wizard,model_library_rent_wizard,group_librarian,1,1,1,1
原理
步驟1,定義了一個新的模型。它與其他的模型沒有什么區別。除了繼承的基類,該模型繼承自TransientModel。TransientModel和Model都繼承自BaseModel。如果查看Odoo的源碼,可看到99%的工作都是基於BaseModel的。
在TransientModel記錄中唯一改變的事情如下:
- 記錄定期從數據庫中刪除,以便瞬態模型的表不會隨着時間而增長。
- 您不允許在引用普通模型的TransientModel實例上定義one2many字段,因為這會在鏈接到臨時數據的持久模型上添加一個列。在這種情況下使用many2many關系。當然,如果one2many中的相關模型也是瞬態模型,你可以使用one2many字段。
我們在模型中定義了兩個字段:一個存儲借書的成員,另一個存儲借書的列表。例如,我們可以添加其他標量字段來記錄預定的返回日期。
步驟2,將代碼添加到向導類中,單擊步驟3中定義的按鈕時將調用該類。此代碼從向導中讀取值並創建library.book.rent的記錄。
步驟3,為向導定義一個視圖。有關詳細信息,請參閱第9章“后端視圖”中的文檔樣式表單配方。這里的重點是頁腳中的按鈕;type屬性設置為“object”,這意味着當用戶單擊按鈕時,將調用按鈕的name屬性指定名稱的方法。
步驟4,確保在應用程序的菜單中有向導的入口點。我們在操作中使用target='new',以便窗體視圖在當前窗體上顯示為對話框。有關詳細信息,請參閱第9章“后端視圖”中的添加菜單項和窗口操作方法。
步驟5,我們已經為library.rent.wizard向導模型。這樣,圖書管理員用戶將擁有library.rent.wizard向導模型的所有訪問權。
注意
在odoo14之前的版本中,TransientModel並不需要特定的訪問權限。任何人都可以創建並訪問自己創建的內容。但在odoo14中TransientModel也必須具有訪問權限才可以使用。
更多
使用上下文去計算默認值
我們的向導需要用戶輸入成員的名稱。根據網頁客戶端的特性,我們可以保存一些輸入的內容。當窗體動作被執行的時候,上下文將攜帶這些值,供向導使用。
- active_model: 這是關聯到動作的模型的名稱。
- active_id: 這表示form視圖下單個記錄處於活動狀態,並提供該記錄的ID。
- active_ids: 有多個記錄被選擇,是一組ID列表。這表示在動作被觸發的時候,有tree視圖下多條記錄被選擇。
- active_domain: 這是在向導運行時額外的過濾。
這些值可以用來計算模型的默認值,甚至可以直接在按鈕調用的方法中使用。為了提升本節中的例子,我們在res.partner視圖下有一個按鈕,可觸發向導動作。其上下文中將包含{'active_model': 'res.partner', 'active_id': <partner_id>}。我們可以定義member_id字段以通過如下函數計算默認值:
def _default_member(self):
if self.context.get('active_model') == 'res.partner':
return self.context.get('active_id', False)
向導和代碼復用
步驟2,我們可以移除for循環。並假設len(self)為1,我們可添加self.ensure_one()方法。
def add_book_rents(self):
self.ensure_one()
rentModel = self.env['library.book.rent']
for book in self.book_ids:
rentModel.create({
'borrower_id': self.borrower_id.id, 'book_id': book.id
})
在函數開通添加self.ensure_one()將確保記錄的數量為1。如果超過1,那么將會報錯。
我們建議使用這個版本。因為這可以讓我們能夠復用創建記錄的代碼。
重定向
步驟2中並沒有返回任何內容。這會在向導視圖完成相關操作后直接關閉。還有一個方式是返回一個ir.action對象的字典。這時,頁面將會捕獲到該信息並進行響應,就像用戶點擊了菜單一樣。在BaseModel模型中的get_formview_action()將會動作。在這個實例中,我們計划展示那些人借了書的form視圖。代碼如下:
def add_book_rents(self):
rentModel = self.env['library.book.rent']
for wiz in self:
for book in wiz.book_ids:
rentModel.create({
'borrower_id': wiz.borrower_id.id,
'book_id': book.id
})
borrowers = self.mapped('borrower_id')
action = borrowers.get_formview_action()
if len(borrowers.ids) > 1:
action['domain'] = [('id', 'in', tuple(borrowers.ids))]
action['view_mode'] = 'tree,form'
return action
這將列出通過向導借閱了圖書的人(實際上將會只有一個借閱者),並且創建了一個動態的行為(將展示特定ID的用戶)。
參考
- 第九章,文檔格式的form視圖,將了解關於向導的更多知識
- 第九章,添加菜單及窗體工作,將會加深我們對服務器側開發的理解。
- 第五章,組合數據集,可以更好的理解為向導創建記錄並將其組合成一個數據集的情況。
定義onchange方法
在編寫業務邏輯時,一些字段經常是相互關聯的。我們在第4章“應用程序模型”的“向模型配方添加約束驗證”中了解了如何在字段之間指定約束。這個食譜說明了一個稍微不同的概念。在這里,當在用戶界面中修改字段時,調用onchange方法來更新web客戶機中記錄的其他字段的值。
舉例說明,本節我們創建一個與”引導用戶的向導“一節中類似的向導,但是它能用於記錄圖書的歸還情況。當向導中一個成員被設置了,該成員的借書清單也將更新。我們將在TransientModel舉例說明onchange函數,這些特性在普通模型中也是可用的。
准備
本節,我們將”引導用戶的向導“一節中的my_library模塊。我們創建了一個用於歸還圖書的向導。我們將添加onchange函數,用於當管理員選擇成員字段的時候,實現自動填充圖書字段。
我們新增一個向導的虛擬模型:
class LibraryReturnWizard(models.TransientModel): _name = 'library.return.wizard'
borrower_id = fields.Many2one('res.partner', string='Member')
book_ids = fields.Many2many('library.book', string='Books')
def books_returns(self):
loanModal = self.env['library.book.rent']
for rec in self:
loans = loanModal .search( [('state', '=', 'ongoing'),
('book_id', 'in', rec.book_ids.ids),
('borrower_id', '=', rec.borrower_id.id)] )
for loan in loans:
loan.book_return()
最后,我們還需要定義視圖、動作及菜單。
步驟
為了當用戶改變的時候,待歸還的圖書列表能夠實現自動填充,我們需在LibraryReturnWizard中實現Onchange函數:
@api.onchange('borrower_id')
def onchange_member(self):
rentModel = self.env['library.book.rent']
books_on_rent = rentModel.search(
[('state', '=', 'ongoing'),
('borrower_id', '=', self.borrower_id.id)])
self.book_ids = books_on_rent.mapped('book_id')
原理
onchange方法使用@api.onchange裝飾器,通過傳遞目標字段實現在目標字段變化后自動觸發該函數。在我們的例子中,我們可以看到在borrowser_id變化后,該函數將被觸發。
在方法體內部,我們查找用戶最新借閱的圖書列表,並將其賦值給book_ids屬性。
更多
onchange函數的基本用法是在目標變化后自動計算相應字段的值。
在方法體的內部,我們可以訪問當前記錄的當前視圖下的展示的字段,而並不一定能夠訪問該模型所有的字段。這是因為記錄在被創建但尚未存儲到數據庫的時候,onchange就可以被調用。在onchange的內部,self是一個特殊的狀態,實際上,self.id並不是一個整數,而是odoo.model.NewId的一個實例。因此,在Onchange中你不可以修改數據庫的內容,因為用戶可能會取消創建記錄,而在用戶取消創建記錄后,數據庫的修改並不會回滾。
此外,onchange函數可以返回一個Python的字典。字典中可以有如下key:
- warning: 值必須是另一個帶有title、message鍵的字典。這將返回前端一個彈框信息,用於告知用戶可能存在的錯誤。
- domain: 值必須是另一個將字段名映射到域的字典。當你改變one2many字段的域時非常有用。
例如,在libaray.book.rent模型中的expected_return_date字段是一個固定的值,我們想當用戶有一些書過期尚未歸還的時候展示警告信息。我們還希望將書籍的選擇限制為用戶當前所借的書籍。我們重寫onchange函數如下:
@api.onchange('member_id')
def onchange_member(self):
rentModel = self.env['library.book.rent']
books_on_rent = rentModel.search( [('state', '=', 'ongoing'),
('borrower_id', '=', self.borrower_id.id)] )
self.book_ids = books_on_rent.mapped('book_id')
result = {
'domain': {'book_ids': [
('id', 'in', self.book_ids.ids)]
}
}
late_domain = [
('id', 'in', books_on_rent.ids),
('expected_return_date', '<', fields.Date.today())
]
late_books = loans.search(late_domain)
if late_books:
message = ('Warn the member that the following books are late:\n')
titles = late_books.mapped('book_id.name')
result['warning'] = {
'title': 'Late books',
'message': message + '\n'.join(titles)
}
return result
在服務器端調用onchange方法
onchange函數由一個限制: 當你在服務器側動作的時候是不會被自動觸發的。但是在一些場景中,onchange函數又是特別重要。因此,我們需要自己計算相關字段,但是,在我們修改第三方的模塊時,我們並不了解其內部邏輯的情況下,是無法實現。
本節將介紹如果在創建記錄前,手動觸發onchange。
准備
在"改變執行動作的用戶"一節中,我們添加了Rent this book按鈕,這可以讓非管理員自己實現借書。我們也想實現自動還書的功能,但是又不想去寫還書的業務邏輯。我們將直接使用”定義onchange方法“中的還書向導。
步驟
在本節,我們將手動創建一個library.return.wizard模型記錄。我們想通過onchange函數為我們自動計算歸還的圖書。
- 將tests中的Form導入library_book.py
from odoo.tests.common import Form
- 在library.book模型中創建return_this_books方法
def return_this_books(self):
self.ensure_one()
- 獲取library.return.wizard模型的空數據集:
wizard = self.env['library.return.wizard']
- 創建向導Form塊:
with Form(wizard) as return_form:
- 通過對borrower_id賦值觸發onchange函數:
return_form.borrower_id = self.env.user.partner_id
record = return_from.save()
record.books_returns()
步驟
步驟1-3是前面幾章的內容。
步驟4,創建一個虛擬的Form視圖,類似於GUI。
步驟5,包含了返回所有圖書的全部邏輯。我們首先對borrower_id進行賦值。這回觸發onchange函數。然后通過save()函數,可返回向導記錄。然后我們調用books_returns()放執行返回所有圖書的函數。
通過計算方法定義onchange
在前兩節中,我們看到了如何定義和調用onchange方法。我們也看到了它的局限性,即只能從用戶界面自動調用它。作為這個問題的解決方案,Odoo v13引入了一種新的方法來定義onchange行為。在這篇文章中,我們將看到如何使用compute方法來產生類似onchange方法的行為。
准備
本節,我們將使用上節中的my_library模塊。我們將用compute方法替換library.return.wizard的onchange方法。
步驟
- 替代api.onchange_member()方法中的onchange:
@api.depends('borrower_id') def onchange_member(self):
...
- 在字段的定義中添加計算參數,如下所示:
book_ids = fields.Many2many('library.book', string='Books',compute="onchange_member", readonly=False)
原理
在功能上,我們計算的onchange工作方式與普通的onchange方法類似。唯一的區別是現在onchange也會在后端更改時觸發。
在步驟1中,我們替換了@api.onchange @api.compute。當字段值改變時,需要重新計算方法。
在步驟2中,我們將計算方法注冊到字段中。如果您注意到,我們在計算字段定義中使用了readonly=False。默認情況下,計算方法是只讀的,但是通過設置readonly=False,我們可以確保該字段是可編輯和存儲的。
更多
由於計算的onchange也在后端工作,我們不再需要在return_all_books()方法中使用Form類。您可以替換代碼如下:
def return_all_books(self):
self.ensure_one()
wizard = self.env['library.return.wizard']
wizard.create({
'borrower_id': self.env.user.partner_id.id
}).books_returns()
基於SQL視圖定義模型
在設計附加模塊時,我們對類中的數據進行建模,然后通過Odoo的ORM將這些數據映射到數據庫表。我們應用了一些眾所周知的設計原則,例如關注點分離和數據規范化。但是,在模塊設計的后期階段,將來自多個模型的數據聚合到一個表中,並在途中對它們執行一些操作,特別是用於報告或生成儀表板,可能會很有用。為了使這更容易,並充分利用Odoo中底層PostgreSQL數據庫引擎的功能,可以定義一個由PostgreSQL視圖支持的只讀模型,而不是表。
在本節,我們將重用“編寫向導”中的租用模型,並且我們將創建一個新模型,以便更容易地收集有關書籍和作者的統計信息。
准備
我們創建一個新的模型,library.book.rent.statistics以展示靜態數據。
步驟
基於PostgreSQL視圖創建新的方法
- 創建帶有_auto=False的模型
class LibraryBookRentStatistics(models.Model):
_name = 'library.book.rent.statistics'
_auto = False
- 設置只讀字段
book_id = fields.Many2one('library.book',string='Book',readonly=True)
rent_count = fields.Integer(string="Timesborrowe" ,readonly=True)
average_occupation = fields.Integer(string="Average Occupation (DAYS)",readonly=True)
- 定義init()創建視圖
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
quert = """
CREATE OR REPLACE VIEW library_book_rent_ statistics AS (
SELECT min(lbr.id) as id,
lbr.book_id as book_id,
count(lbr.id) as rent_count,
avg(
(
EXTRACT(
epoch
from age(return_date, rent_date)
) / 86400
)
)::int as average_occupation
FROM library_book_rent AS lbr
JOIN library_book as lb ON lb.id = lbr.book_id
WHERE lbr.state = 'returned'
GROUP BY lbr.book_id
);
- 我們現在可以為新模型定義視圖了。pivot視圖在瀏覽數據方面將會非常方便。
- 定義訪問權限控制。
原理
通常,Odoo將使用列的字段定義為您正在定義的模型創建一個新表。這是因為在BaseModel類中,\u auto屬性默認為True。在步驟1中,通過將這個class屬性定位為False,我們告訴Odoo我們將自己管理它。
步驟2中,我們定義了一些字段,Odoo將使用這些字段生成一個表。我們注意將它們標記為readonly=True,這樣視圖就不會啟用您無法保存的修改,因為PostgreSQL視圖是只讀的。
步驟3定義init()方法。此方法通常不執行任何操作;它在_auto_init()之后調用(當_auto=True時,它負責創建表,但在其他情況下不執行任何操作),我們使用它來創建新的SQL視圖(或者在模塊升級時更新現有視圖)。視圖創建查詢必須創建列名與模型字段名匹配的視圖。
重要提醒
在這種情況下,忘記重命名視圖定義查詢中的列是一個常見的錯誤,當Odoo找不到該列時,這將導致錯誤消息。
注意,我們還需要提供一個名為ID的整數列,該列包含唯一的值。
更多
在這樣的模型上也可以有一些計算的和相關的字段。唯一的限制是不能存儲這些字段(因此,不能使用它們對記錄進行分組或搜索)。但是,在前面的示例中,我們可以通過添加一個列來提供該書的編輯器,定義如下:
publisher_id = fields.Many2one('res.partner', related='book_ id.publisher_id', readonly=True)
如果需要按發布者分組,則需要通過在視圖定義中添加字段來存儲字段,而不是使用相關字段。
添加用戶配置選項
在Odoo中,您可以通過Settings選項提供可選功能。用戶可以隨時啟用或禁用此選項。我們將演示如何在此配方中創建設置選項。
准備
在前面幾節中,我們添加了按鈕,以便非圖書管理員用戶可以借書和還書。並非每個庫都是這樣;但是,我們將創建一個設置選項來啟用和禁用此功能。我們要把這些按鈕藏起來。在本節,我們將使用與前面配方相同的my_library庫模塊。
步驟
- 添加權限組group:
<record id="group_self_borrow" model="res.groups">
<field name="name">Self borrow</field>
<field name="users" eval="[(4,ref('base.user_admin'))]"/>
</record>
- 繼承res.config.setttins模型並添加字段
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
group_self_borrow = fields.Boolean(string="Self borrow",implied_group='my_library.group_self_borrow')
- 通過xpath添加字段到settings視圖中:
<record id="res_config_settings_view_form" model="ir. ui.view">
<field name="name">res.config.settings.view.form. inherit.library</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="5"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Library" string="Library" data-key="my_library" groups="my_library.group_librarian">
<h2>Library</h2>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_ setting_box" id="library">
<div class="o_setting_left_pane">
<field name="group_self_borrow"/>
</div>
<div class="o_setting_right_pane">
<label for="group_self_borrow"/>
<div class="text-muted"> Allow users to borrow and return books by themself
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
- 添加動作及菜單
<record id="library_config_settings_action" model="ir. actions.act_window">
<field name="name">Settings</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.config.settings</field>
<field name="view_id" ref="res_config_settings_view_form"/>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{'module' : 'my_library'}</field>
</record>
<menuitem name="Settings" id="library_book_setting_menu" parent="library_base_menu" action="library_config_settings_action" sequence="50"/>
- 修改書的窗體視圖中的按鈕並添加“my_library.group“以實現自助借閱:
<button name="book_rent" string="Rent this book" type="object" class="btn-primary" groups="my_library.group_self_borrow"/>
<button name="return_all_books" string="Return all book" type="object" class="btn-primary" groups="my_library.group_self_borrow"/>
原理
在Odoo中,所有設置選項都添加在資源配置設置模型。物件。配置設置是一個瞬態模型。在步驟1中,我們創建了一個新的安全組。我們將使用這個組來創建隱藏和顯示按鈕。
在步驟2中,我們在資源配置設置通過繼承模型。我們添加了一個implied_group屬性和my_library.group_self_borrow。當管理員使用布爾字段啟用或禁用選項時,此組將分配給所有odoo用戶。
Odoo設置使用窗體視圖在用戶界面上顯示設置選項。所有這些選項都添加到具有外部ID的單個窗體視圖中,base.res_config_settings_view_form。在步驟3中,我們通過繼承這個設置表單視圖在用戶界面中添加了我們的選項。我們使用xpath添加了設置選項。在第9章“后端視圖”中,我們將詳細了解這一點。在表單定義中,您會發現這個選項的屬性數據鍵值將是您的模塊名。只有在“設置”中添加全新選項卡時,才需要此選項。否則,您可以使用xpath在現有模塊的Settings選項卡中添加您的選項。
在步驟4中,我們添加了一個操作和一個菜單來從用戶界面訪問配置選項。您需要從操作中傳遞{module':'my_library}上下文,以便在單擊菜單時默認打開my_library模塊的設置選項卡。
在步驟5中,我們添加了my_library.group_self_borrow按鈕。由於此組,將根據設置選項隱藏或顯示“借用”和“歸還”按鈕。
之后,您將看到一個單獨的庫設置選項卡,在該選項卡中,您將看到一個布爾字段,用於啟用或禁用自借選項。當您啟用或禁用此選項時,在后台,Odoo將向所有Odoo用戶應用或從中刪除組。因為我們在按鈕上添加了組,所以如果用戶有組,按鈕將顯示;如果用戶沒有組,按鈕將隱藏。在第10章“安全訪問”中,我們將詳細介紹安全組。
更多
還有其他一些方法可以管理設置選項。其中之一是分離新模塊中的功能,並通過選項安裝或卸載它們。為此,您需要添加一個以模塊名稱為前綴的布爾字段。例如,如果我們創建了一個名為my_library_extras的新模塊,則需要添加一個布爾字段,如下所示:
module_my_library_extras = fields.Boolean( string='Library Extra Features')
啟用或禁用此選項時,odoo將安裝或卸載my_libarary_extras模塊。
管理設置的另一種方法是使用系統參數。這些數據存儲在ir.config_parameter參數模型。下面介紹如何創建系統范圍的全局參數:
digest_emails = fields.Boolean(string="Digest Emails",config_parameter='digest.default_digest_emails')
字段中的config_parameter屬性將確保用戶數據存儲在 設置|技術|參數|系統參數菜單的系統參數中。數據將與digest.default_digest_emails。
設置選項用於使應用程序通用。這些選項為用戶提供了自由,允許他們動態地啟用或禁用功能。當您將功能轉換為選項時,您可以使用一個模塊為更多客戶提供服務,並且您的客戶可以隨時啟用該功能。
實現在模塊安裝時的函數(個人叫它鈎子函數)
在第6章“管理模塊數據”中,您了解了如何從XML或CSV文件中添加、更新和刪除記錄。然而,有時業務案例很復雜,無法使用數據文件來解決。在這種情況下,可以使用清單文件中的init鈎子來執行所需的操作。
准備
我們將使用與上節my_library模塊。為了簡單起見,在本節,我們將通過post_init_hook創建一些圖書記錄。
步驟
- 在__manifest__.py中添加post_init_hook的鍵。
'post_init_hook': 'add_book_hook',
- 在__init__.py中添加add_book_hook():
from odoo import api, fields, SUPERUSER_ID
def add_book_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
book_data1 = {'name': 'Book 1', 'date_release':fields.Date.today()}
book_data2 = {'name': 'Book 2', 'date_release':fields.Date.today()}
env['library.book'].create([book_data1, book_data2])
原理
步驟1,我們在manifest文件中添加post_init_hook:add_book_hook的鍵值。
步驟2,我們聲明了add_book_hook()方法,該方法將在安裝模塊后調用。我們用這個方法創建了兩個記錄。在實際情況中,您可以在這里編寫復雜的業務邏輯。
odoo中還有兩種hooks方法:
- pre_init_hook: 當您開始安裝模塊時,將調用此鈎子。它與post_init_hook相反;它將在安裝當前模塊之前被調用。
- uninstall_hook:卸載模塊時將調用此鈎子。當您的模塊需要垃圾回收機制時,通常會使用此方法。