本章包含如下內容:
- 定義模型方法和使用api裝飾器
- 向用戶反饋錯誤信息
- 針對不同的對象獲取空數據集
- 創建新紀錄
- 更新數據集數據
- 搜索數據
- 組合數據集
- 過濾數據集
- 遍歷記錄集
- 排序數據集
- 重寫已有業務邏輯
- 重寫write()和create()
- 定義如何搜索數據
- 通過read_group()以組為單位數據
定義模型方法和使用api裝飾器
在odoo的模型中,類是字段及業務邏輯的組合。第四章我們了解了添加字段。本節我們將學習如何添加業務邏輯。
我們來學習如何創建一個可由用戶點擊激活或者由其他業務函數代用的方法。
表現在LibraryBook類上,我們會創建一個改變圖書狀態的函數。
准備
在前面LibraryBook類的基礎上,新增state字段
from odoo import models, fields, api
class LibraryBook(models.Model):
# [...]
state = fields.Selection([
('draft', 'Unavailable'),
('available', 'Available'),
('borrowed', 'Borrowed'),
('lost', 'Lost')],
'State', default="draft")
步驟
創建業務邏輯
- 添加一個檢測state狀態變化是否被允許的幫助函數
@api.model
def is_allowed_transition(self, old_state, new_ state):
allowed = [('draft', 'available'),
('available', 'borrowed'),
('borrowed', 'available'),
('available', 'lost'),
('borrowed', 'lost'),
('lost', 'available')]
return (old_state, new_state) in allowed
- 添加改變圖書狀態的函數
def change_state(self, new_state):
for book in self:
if book.is_allowed_transition(book.state,new_state):
book.state = new_state
else:
continue
- 添加不同狀態下改變圖書的函數
def make_available(self):
self.change_state('available')
def make_borrowed(self):
self.change_state('borrowed')
def make_lost(self):
self.change_state('lost')
- 在form視圖下添加由用戶觸發的按鈕
<form>
...
<button name="make_available" string="Make Available" type="object"/>
<button name="make_borrowed" string="Make Borrowed" type="object"/>
<button name="make_lost" string="Make Lost" type="object"/>
<field name="state" widget="statusbar"/>
...
</form>
原理
本節定義了幾個函數。他們都是帶有self參數的普通python函數。有幾個函數帶有了odoo.api裝飾器。
小貼士
api裝飾器是在odoo9引入的,以支持新舊框架。在odoo10中,不再支持老的api了,但是一寫裝飾器還可以使用。
當一個新的函數,沒有使用裝飾器,那么該函數式作用在數據集層面的。在這類函數中,self是任意數量記錄的數據集(可以是空數據集),通常通過循環獲取每一個數據進行處理。
@api.model裝飾器類似,但是他被用作針對模型而不是記錄集內容的函數。 他與python的@classmethod裝飾器類似。
步驟1,我們創建了監測圖書狀態狀態是否符合要求的函數is_allowed_transition()
步驟2,我們創建了改變圖書狀態的函數change_state()。
步驟3,我們通過調用change_state()實現圖書狀態的改變。
步驟4,我們在form視圖下創建了幾個按鈕,用戶可通過點擊按鈕實現改變圖書的狀態。其中,statusbar的組件(widget)將直觀的展示圖書的狀態。
向用戶反饋錯誤信息
在函數執行的過程中,有時需要中斷執行,因為用戶沒有權限或者其他的異常情況發生。本節將介紹如何想用戶展示錯誤信息。
准備
步驟
當我們通過change_state函數改變圖書的狀態的時候,若某些轉變是不允許的。
- 添加引用
from odoo.exceptions import UserError
from odoo.tools.translate import _
- 修改change_state函數並觸發UserError異常
def change_state(self, new_state):
for book in self:
if book.is_allowed_transition(book.state, new_ state):
book.state = new_state
else:
msg = _('Moving from %s to %s is not allowed') % (book.state, new_state)
raise UserError(msg)
原理
在python中,若發生異常,他將逐級調用堆棧,直到他被處理。在odoo中,RPC(remote procedure call遠程過程調用)層將對客戶端捕獲到的異常行為進行處理。
如果異常在odoo.exceptions沒有被定義,那么將會報服務器錯誤(HTTP status500)。UserError將會展示一個錯誤提醒彈框,並且當前數據庫事務將會回滾。
_()定義在odoo.tools.translate的函數。用於標記字符串是可翻譯的。
重要提醒
在使用_()時,要確保括號中只有字符串。比如,('Warning: could not find %s') % value是正確的。('Warning: could not find %s' % value) 就是錯誤的。
更多
有時,您正在處理容易出錯的代碼,這意味着您正在執行的操作可能會生成錯誤。Odoo將捕獲此錯誤並向用戶顯示一個回溯信息。如果您不想向用戶顯示完整的錯誤日志,您可以捕獲錯誤並返回自定義的異常。在提供的示例中,我們通過try捕獲異常並返回錯誤,而不是顯示一個完整的錯誤日志,Odoo現在將顯示一個有意義的消息警告:
def post_to_webservice(self, data):
try:
req = requests.post('http://my-test-service.com', data=data, timeout=10)
content = req.json()
except IOError:
error_msg = _("Something went wrong during data submission")
raise UserError(error_msg)
return content
在odoo.exceptions中定義了幾個異常,這些都繼承自except_orm異常類。它們中的大多數只在內部使用,除了以下幾種:
- ValidationError: 當python 的字段約束未滿足時觸發該異常。
- AccessError: 當用戶嘗試訪問未授權的數據時觸發該異常。
- RedirectWarning: 在這個錯誤中,可以展示帶有錯誤提醒的重定向按鈕。我們需要傳遞兩個參數,一個是動作ID(aciton ID),一個是錯誤提醒信息。
- Warning: 在odoo8中,odoo.exceptions.warning與odoo9及之后版本中的UserError相同。它現在已棄用,因為它的名稱具有欺騙性(它是一個錯誤,而不是一個警告),並且它與Python內置的warning類相撞。你應該在你的代碼中使用UserError。
針對不同的對象獲取空數據集
當在寫odoo模塊的時候,我們可以通過self獲取當前模型。如果想同時操作幾個模型,我們沒有辦法直接實例化相關模型。我們需要獲取相應模型的數據集。
本節將介紹獲取odoo中定義的odoo模型的數據集。
准備
步驟
- 在LibraryBook中,新增get_all_library_members函數
class LibraryBook(models.Model):
# ...
def log_all_library_members(self):
# This is an empty recordset of model library. member
library_member_model = self.env['library.member']
all_members = library_member_model.search([])
print("ALL MEMBERS:", all_members)
return True
2. 在form視圖添加按鈕
```xml
<button name="log_all_library_members" string="Log Members" type="object"/>
原理
在啟動時,Odoo加載所有模塊,並組合從Model派生的各種類,還定義或擴展給定的模型。這些類存儲在Odoo注冊表中,按名稱建立索引。任何記錄集的env屬性,可通過self.env獲取,他是定義在odoo.api模塊中的Environment類的一個實例。
在odoo開發中,Environment扮演着重要的角色。
- env屬性,它通過模擬Python字典提供了訪問注冊表的快捷方式。如果你知道你要找的模特的名字,self.env[model_name]將為該模型獲取一個空記錄集。此外,記錄集將傳遞self的上下文環境。
- cr屬性,是數據庫的游標。可傳遞SQL的原生查詢。
- user屬性,是當前登錄的賬戶的引用。
- context屬性,當前調用的上下文,是一個python字典。
創建新紀錄
准備
你需要知道你創建記錄的模型的數據結構,特別是他們的名字和數據類型,以及字段間的約束。
創建library.book.category模型
class BookCategory(models.Model):
_name = 'library.book.category'
name = fields.Char('Category')
description = fields.Text('Description')
parent_id = fields.Many2one(
'library.book.category',
string='Parent Category',
ondelete='restrict',
index=True
)
child_ids = fields.One2many(
'library.book.category', 'parent_id',
string='Child Categories')
步驟
- 創建名為create_categories:
def create_categories(self):
...
- 函數中創建一個字典
categ1 = {
'name': 'Child category 1',
'description': 'Description for child 1'
}
- 第二個字段
categ2 = {
'name': 'Child category 2',
'description': 'Description for child 2'
}
- 創建父種類
parent_category_val = {
'name': 'Parent category',
'email': 'Description for parent category',
'child_ids': [
(0, 0, categ1),
(0, 0, categ2),
]
}
- 創建新記錄
record = self.env['library.book.category'].create(parent_ category_val)
- form視圖添加動作的button
<button name="create_categories" string="Create Categories" type="object"/>
原理
通過create(values)函數創建記錄,返回值為長度為1的新創建的記錄集。
在values中,需傳遞正確的類型,odoo中的類型與python中的類型對應如下:
- Text: python字符串
- Float: python的float或整型
- Boolean: python的Booleans或者整型
- Date: python的datetime.date
- Datetime: python的datetime.datetime
- Binary: 是Base64-encoding的字符串。通過python的標准包base64的encodebytes(bytestring)函數以Base64格式進行編碼。
- Many2one: 整型,是關聯對象的數據庫ID
- One2many和Many2many使用特殊的語法。值是包含三個元素的元組,如下:
| 元組 | 影響 |
| (0, 0, dict_val) | 關聯到主記錄的新紀錄 |
| (6, 0, id_list) | 創建新舊記錄的關聯管理。IDs是名為id_list的python列表 |
更多
如果模型中定義了默認值,那么create()將自動處理默認值。
create()函數支持批量創建記錄,可在傳參時傳遞列表。
更新數據集數據
准備
在library.book模型中有個名為date_release的字段。
步驟
- 更新圖書的date_update字段,我們創建一個名為change_update_date()的函數
def change_release_date(self):
self.ensure_one()
self.date_release = fields.Date.today()
- form視圖中添加button
<button name="change_release_date" string="Update Date" type="object"/>
原理
首先我們先調用ensuer_one()函數監測傳遞的self記錄集是否只要一條記錄。若記錄集中數據超過一條,那么調用將停止。這是必要的,因為我們不想更改多個記錄的日期。如果希望更新多個值,可以刪除ensure_one()並使用記錄集上的循環更新屬性。
更多
有三種方式實現數據更新
- 一,如上所述
- 二,通過對數據集調用update({'key':value,'key1':value1..})更新數據集
- 三,調用write函數,與update類似,傳遞字典。
| 元組 | 影響 |
| (0, 0, dict_val) | 將新創建的記錄對象關聯到主記錄上 |
| (1, id, dict_val) | 更新特定(ID)關聯記錄的值 |
| (2, id) | 移除ID的記錄,並從數據庫刪除ID的記錄 |
| (3, id) | 取消記錄ID的關聯,但並不從數據庫刪除記錄 |
| (4, id) | 添加關聯新的已經存在的紀錄 |
| (5, ) | 移除所有的記錄,類似於對每一個ID調用(3, id) |
| (6, 0, id_list) | 這將在正在更新的記錄和現有記錄之間創建一個關系,現有記錄的id在名為id list的Python列表中。 |

搜索數據
准備
我們將在library.book中創建一個名為find_book(self)的函數
步驟
- 定義 find_book函數
def find_book(self):
...
- 寫過濾
domain = [
'|',
'&', ('name', 'ilike', 'Book Name'),
('category_id.name', 'ilike', 'CategoryName'),
'&', ('name', 'ilike', 'Book Name 2'),
('category_id.name', 'ilike', 'Category Name2')
]
- 調用search()
books = self.search(domain)
原理
步驟2,創建搜索條件。我們將在第九章“定義過濾條件”章節詳細介紹。
步驟3,調用search()。支持的參數:
- offset=N: 過濾前N條記錄。可以和limit同時使用,以降低內存占用。默認為0
- Limit=N: 限制單次返回的數量。默認無限制。
- count=boolean: 如果為真,則顯示搜索的數量。默認為False。
重要提醒
search_count(domain)同樣可以搜索數量,這是也官方推薦的方法。
有時我們需要搜索另一個模型的數據,因此我們首先要獲取該模型的空數據集。然后再調用search()函數。如下
def find_partner(self):
PartnerObj = self.env['res.partner']
domain = [
'&', ('name', 'ilike', 'Parth Gajjar'),
('company_id.name', '=', 'Odoo')
]
partner = PartnerObj.search(domain)
在過濾條件中"&"是默認的。
更多
通過search()獲取的內容其實是首先要經過權限驗證的。並且對於擁有active屬性的模型,在active=False時,默認也是不進行搜索的。
關於不添加隱式的active=True條件的方法,請參閱第8章“高級服務器端開發技術”中使用不同的上下文配方調用方法。關於記錄級別訪問規則的更多信息,請參閱第10章“安全訪問”中的“使用記錄規則限制記錄訪問”配方。
如果由於某種原因,你需要通過SQL查詢來查找記錄id,
確保你使用了self.env['record.model'].search(((“id”,“in”,tuple(ids)))).ids應用了安全規則。這在多公司的Odoo案例中尤其重要,因為記錄規則被用來確保公司之間的隔離。
組合數據集
准備
兩個以上的數據集
步驟
- 要合並兩個記錄集為一個,同時保留它們的順序,使用以下操作:
result = recordset1 + recordset2
- 若要將兩個記錄集合並為一個記錄集,同時確保結果中沒有重復項,請使用以下操作:
result = recordset1 | recordset2
3.要找到兩個記錄集共有的記錄,使用以下操作:
result = recordset1 & recordset2
原理
記錄集的組合是通過對python運算符的重定義實現的。

還有 +=, -=, &=, and |=, 運算符,將得出的結果賦值給左側。這些在更新記錄的One2many或Many2many字段時非常有用。
過濾數據集
有時,我們已經得到了數據集,但我們只需要其中一部分數據。我們可以迭代數據集並通過判斷條件實現,但更為高效的是通過filter()函數實現。
准備
步驟
- 過濾有多個作者的圖書
@api.model
def books_with_multiple_authors(self, all_books):
- 定義過濾規則函數
def predicate(book):
if len(book.author_ids) > 1:
return True
return False
- 調用filter()函數
return all_books.filter(predicate)
原理
filter()函數創建了一個空數據集,所有的符合過濾條件的記錄都會被添加到數據集中。最后將返回新的數據集。
以上可以簡寫成
@api.model
def books_with_multiple_authors(self, all_books):
return all_books.filter(lambda b: len(b.author_ids) > 1)
其實我們過濾數據集是根據記錄中字段的值需符合python意義上的真(非空字符串、非0數字、非空容器等)。所以我們可以簡單的過濾如下: all_books.filter('category_id')。
更多
filter()過濾是在內存中實現的。如果我們需要在一個特別重要的函數中優化性能,那么通過搜索域或者直接原生SQL是不錯的選擇。
遍歷關聯字段的記錄集
當數據集的長度為1時,那么模型的屬性可直接被recordsets使用。
本章節我們將使用mapped()函數遍歷數據集關系。
准備
步驟
- 定義get_author_names()函數
@api.model
def get_author_names(self, books):
- 調用mapped()獲取關聯對象的郵件地址
return books.mapped('author_ids.name')
原理
步驟2,我們調用mapped(path)方法獲取數據集的字段值。path是以"."分隔的字段名。在path中的每個字段,mapped()都將獲取每個字段的的數據集,並接着獲取后續字段的值。比如上面,我們獲取到一本書的所有的作者(author_ids)數據集,然后在這個新的數據集上獲取所有記錄中name的值,並以列表的形式返回。若最后一個字段是關聯字段的話,那么返回的將是數據集。
mapped()有兩個非常有用的屬性:
- 若path是單個標量字段名,將返回經過計算的相同順序的列表。
- 若path中包含關聯字段,將不會保留順序,並且重復的數據會去掉。
重要提醒(感覺翻譯就變味了)
This second property is very useful when you want to perform an operation on all the records that are pointed to by a Many2many field for all the records in self, but you need to ensure that the action is performed only once (even if two records of self share the same target record).
更多
mapped()是在內容中實現的,因此如果需要優化性能,可通過search()及原生SQL實現。
排序數據集
當在通過search()查找數據集的時候,可傳遞參數以進行排序。但是當我們進行數據集的組合時,有可能導致順序錯亂。
本章節將介紹通過sorted()函數對數據集進行排序。
准備
步驟
- 定義函數sort_books_by_date():
@api.model
def sort_books_by_date(self, books):
- 排序
return books.sorted(key='release_date')
原理
步驟2進行排序,sorted有個可選參數reverse,決定排序的方式。如下
books.sorted(key='release_date', reverse=True)
更多
sorted()函數將對數據集進行排序。若沒有參數,那么模型的_order屬性將會被使用。
重要提醒
當使用模型的_order時,排序將通過數據庫實現。否則將通過odoo內部實現。兩者存在一定的性能不同。
重寫已有業務邏輯
在Odoo中,將應用程序特性划分為不同的模塊是一種非常常見的做法。通過這樣做,您可以通過安裝/卸載應用程序來啟用/禁用特性。當你向現有的應用程序添加新功能時,你就需要自定義在原始應用程序中定義的一些方法的行為。有時,你還想向現有的模型添加新字段。這在Odoo中是一項非常簡單的任務,也是底層框架最強大的特性之一。
在此菜譜中,我們將看到如何從另一個模塊中的方法擴展一個方法的業務邏輯。我們還將從新模塊向現有模塊添加新字段。
准備
我們創建一個新的模塊my_library_return,這個模塊依賴於my_library模塊。在這個模塊中,我們返回所借圖書的日期並計算歸回日期。
在第4章“應用程序模型”的“使用繼承配方向模型添加特性”中,我們看到了如何向現有模型添加字段。在這個模塊中,擴展這個庫。圖書模型如下:
class LibraryBook(models.Model):
_inherit = 'library.book'
date_return = fields.Date('Date to return')
擴展library.book.category模型
class LibraryBookCategory(models.Model):
_inherit = 'library.book.category'
max_borrow_days = fields.Integer(
'Maximum borrow days',help="For how many days book can be borrowed",default=10)
步驟
- 在my_library_return模型,當我們將圖書狀態更改為已借時,我們想在books記錄中設置date_return。為此,我們將覆蓋my_module_return模塊中的make_borrowed方法:
def make_borrowed(self):
day_to_borrow = self.category_id.max_borrow_days or 10
self.date_return = fields.Date.today() + timedelta(days=day_to_borrow)
return super(LibraryBook, self).make_borrowed()
- 我們還希望在圖書返回並可借出時重置date_return,因此我們將重寫make_available方法來重置日期:
def make_available(self):
self.date_return = False
return super(LibraryBook, self).make_available()
原理
在Odoo模型的情況下,父類不是您從Python類定義中所期望的那樣。框架為我們的記錄集動態地生成了一個類層次結構,父類是我們依賴的模塊中的模型定義。因此,對super()的調用帶回了library的實現。從my_module書。在這個實現中,make_borrowed()將book的狀態更改為Borrowed。因此,調用super()將調用父方法,並將圖書狀態設置為Borrowed。
更多
一般我們在重寫函數后會去調用父函數,否則父函數將不會被執行。
重寫函數中,在調用父函數前后我們可以:
- 修改傳遞給原始實現的參數(before)
- 修改傳遞給原始實現的上下文(before)
- 修改原始實現返回的結果(after)
- 調用另一個方法(before和after)
- 創建記錄(前后)
- 在禁止的情況下(之前和之后)拋出UserError錯誤來取消執行
- 將self分成更小的記錄集,並在每個子集上以不同的方式調用原始實現(以前)
重寫write()和create()
和上一些內容類似,需要注意的如下:
- 在create及write中,調用self.user_has_groups('組名稱')可以判斷用戶是否輸入某個權限組。
- 在write函數中調用super()的write之后,對數據進行修改后在此觸發write方法,這可能導致write的遞歸。可通過寫上下文參數的形式避免,如下:
class MyModel(models.Model):
def write(self, values):
sup = super(MyModel, self).write(values)
if self.env.context.get('MyModelLoopBreaker'):
return
self = self.with_context(MyModelLoopBreaker=True)
self.compute_things() # can cause calls to writes
return sup
定義如何搜索數據
本章節將重新定義name_search,以按書名、作者或書號在Many2one小部件中搜索一本書。
准備
class LibraryBook(models.Model):
_name = 'library.book'
name = fields.Char('Title')
isbn = fields.Char('ISBN')
author_ids = fields.Many2many('res.partner', 'Authors')
def name_get(self):
result = []
for book in self:
authors = book.author_ids.mapped('name')
name = '%s (%s)' % (book.name, ', '.join(authors))
result.append((book.id, name))
return result
當使用這個模型時,Many2one小部件中的一本書將顯示為書名(Author1, Author2…)。用戶希望能夠輸入作者的名字,並找到根據這個名字過濾的列表,但是這不會起作用,因為name_search的默認實現只使用的_rec_name屬性引用的屬性。我們還希望允許按ISBN號進行過濾。
步驟
- 重定義_name_search()函數
@api.model
def _name_search(self, name='', args=None, operator='ilike',limit=100, name_get_uid=None):
args = [] if args is None else args.copy()
if not(name == '' and operator == 'ilike'):
args += ['|', '|',('name', operator, name),('isbn', operator, name),('author_ids.name', operator, name)]
return super(LibraryBook, self)._name_search(name=name, args=args, operator=operator,limit=limit, name_get_uid=name_get_uid)
- 在庫中添加old_editions Many2one字段。book模型測試_name_search實現:
old_edition = fields.Many2one('library.book', string='Old Edition')
<field name="old_edition" />
原理
name_search()默認的實現方式只是調用了_name_search()函數。_name_search()有一個額外的參數, name_get_uid, 是搜索主題的用戶,可以是sudo()或者其他的用戶。
我們一般只是將參數進行傳遞而已,並不做修改:
- name: 是用戶輸入的內容。
- args: 既可以是None,也可以的domain過濾條件。
- operator: 字符串,一般是'ilike'或者'='。
- limit: 返回的最大行數。
- name_get_uid: 使用不同用戶的權限實現展示記錄。
通過read_group()以組為單位數據
在前面的章節中,我們看到了如何從數據庫中搜索和獲取數據。但有時,您希望通過匯總記錄得到結果,例如上月銷售訂單的平均成本。通常,我們在SQL查詢中使用group by和aggregate函數來得到這樣的結果。幸運的是,在Odoo中,我們有read_group()方法。在本食譜中,您將學習如何使用read_group()方法來獲取聚合結果。
准備
library.book模型
class LibraryBook(models.Model):
_name = 'library.book'
name = fields.Char('Title', required=True)
date_release = fields.Date('Release Date')
pages = fields.Integer('Number of Pages')
cost_price = fields.Float('Book Cost')
category_id = fields.Many2one('library.book.category')
author_ids = fields.Many2many('res.partner', string='Authors')
添加library.book.category模型。簡單起見,直接添加在library_book.py文件中。
class BookCategory(models.Model):
_name = 'library.book.category'
name = fields.Char('Category')
description = fields.Text('Description')
我們將計算每個品類圖書的平均值。
步驟
要提取分組的結果,我們將向library.book模型添加_get_average_cost方法。它將使用read_group()方法獲取組中的數據:
@api.model
def _get_average_cost(self):
grouped_result = self.read_group(
[('cost_price', "!=", False)], # Domain
['category_id', 'cost_price:avg'], # Fields to access
['category_id'] # group_by
)
return grouped_result
原理
read_group()函數其實是通過SQL的groupby和aggregate函數實現的。常用的參數如下:
- domain: 過濾條件。
- fields: 傳遞給group的字段。
field_name: 如果寫了字段名,那么在后面的group_by中也必須有。否則會報錯。
field_name:agg:這是對field_name字段進行統計。比如cost_price:avg是計算花費的平均值。
name:agg(field_name): 類似於上一個,name是給計算的字段起了一個昵稱。 - groupby: 分組的字段。對於date和datetime列,可以通過groupby_function來應用基於不同時間段的日期分組,例如date_release:month。這將適用於基於月份的分組。
read_group()支持一些可選參數,如下:
- offset: 跳過的數據量。
- limit: 限制返回的記錄數量。
- orderby: 排序字段
- lazy: True/False。如果為True,結果集將通過第一個groupby進行分組,其余的放入_context的key中。如果為False,那么將在一次中完成排序。
性能提醒
read_group()性能優於獲取記錄后再進行計算的方式。
