Odoo 12 開發手冊指南(八)—— 業務邏輯 – 業務流程的支持

在前面的文章中,我們學習了模型層、如何創建應用數據結構以及如何使用 ORM API 來存儲查看數據。本文中我們將利用前面所學的模型和記錄集知識實現應用中常用的業務邏輯模式。

本文的主要內容有:

  • 以文件爲中心工作流的階段(stage)
  • ORM 方法裝飾器:@api.multi, @api.one和@api.model
  • onchange方法,與用戶即時交互
  • 使用 ORM 內置方法,如create, write 和 unlink
  • Mail 插件提供的消息和活動功能
  • 創建嚮導來幫助用戶執行復雜操作
  • 使用日誌消息優化系統監測
  • 拋出異常以在出錯時給用戶反饋
  • 使用單元測試來進行代碼質量檢查
  • 開發工具,調試器等開發者工具

 

開發準備

本文中我們將創建一個依賴於之前文章創建的library_app和library_member模塊的library_checkout插件模塊。這些模塊的代碼請參見 GitHub 倉庫。這兩個插件模塊都應放置在add-ons路徑中(參見命令行–addons-path或~/.odoorc 配置文件中的addons_path),這樣我們才能安裝和使用。本文完成後的代碼請見 GitHub 倉庫

學習項目 – library_checkout模塊

在前面章節的學習中,我們爲圖書應用搭建了主數據結構。現在需要爲圖書會員添加借書的功能了。也就是說需要追蹤圖書是否可藉以及歸還的記錄。每本書的借閱都有一個生命週期,從圖書登記草稿到圖書被歸還。這是一個可通過看板視圖表示的簡單工作流,看板視圖中每個階段(stage)可展現爲一列,工作項和借閱請求流從左側列到右側列,直至完成爲止。

在本文中,我們集中學習實現這一功能的數據模型和業務邏輯。用戶界面部分的詳情將在第十章 Odoo 12開發之後臺視圖 – 設計用戶界面和第十一章 Odoo 12開發之看板視圖和用戶端 QWeb中討論。

圖書借閱模型包含:

  • 借閱圖書的會員(必填)
  • 借閱請求日期(默認爲當天)
  • 負責借閱請求的圖書管理員(默認爲當前用戶)
  • 借閱路線,包含請求借閱的一本或多本圖書

要支持並存檔借閱生命週期,需要添加如下內容:

  • 請求的階段:草稿、開放、借出、歸還或取消
  • 借閱日期,圖書借出的日期
  • 關閉日期,圖書歸還的日期

我們將開始創建一個新的模塊library_checkout並實現圖書借閱模型的初始版本。與此前章節相比此處並沒有引入新的知識,用於提供一個基礎供本文後續創建新功能。

在其它圖書插件模塊的同級路徑下創建一個library_checkout目錄:

1、首先添加__manifest__.py文件並加入如下內容:

2、在模塊目錄下創建__init__.py文件,並添加如下代碼:

3、創建models/__init__.py文件並添加:

4、在models/library_checkout.py中添加如下代碼:

下面就要添加數據文件了,添加訪問規則、菜單項和一些基礎視圖,這樣模塊可以最小化的運行起來。

5、添加security/ir.model.access.csv文件:

6、菜項項通過views/library_menu.xml實現:

7、視圖通過views/checkout_view.xml文件實現:

現在就可以在我們的 Odoo 工作數據庫中安裝這個模塊,並準備開始添加更多功能了。

 

以文檔爲中心工作流的階段(stage)

在 Odoo 中,我們可以實現以文檔(document)爲中心的工作流。我們這裏說的文檔包括銷售訂單、項目任務或人事申請。所有這些都遵循一個特定的生命週期,它們都在完成時才被創建。它們都被記錄在一個文檔中,按照一系列可能的階段推進,直至完成。

如果把各階段以列展示在面板中,把文檔作爲這些列中的工作項,就可以得到一個看板(Kanban),一個快速查看工作進度的視圖。實現這些進度步驟有兩種方法,通常稱爲狀態和階段。

狀態通過預定義的閉合選項列表來實現。它便於實現業務規則,並且模型和視圖對 state 字段有特別的支持,根據當前狀態來帶有必填和隱藏屬性集。狀態列表有一個劣勢,就是它是預定義並且閉合的,因此無法按具體流程需求來做調整。

階段通過關聯模型實現,階段列表是開放的,可被配置來滿足當前流程需求。可以輕易地修改引用階段列表:刪除、添加或渲染這些階段。它的劣勢是對流程自動化不可靠,因爲階段列表可被修改,自動化規則就無法依賴於具體的階段 ID 或描述。

獲取兩種方法優勢的方式是將階段映射到狀態中。文檔組織到可配置的階段中,然後間接關聯到對於自動化業務邏輯可靠的狀態碼中。我們將在library_checkout/models/library_checkout_stage.py文件中實現library.checkout.stage模型,代碼如下:

這裏我們可以看到 state 字段,允許每個階段與四個基本狀態映射。sequence字段很重要,要配置順序,階段應在看板和階段選擇列表中展示。fold 布爾字段是看板用於將一些列默認摺疊,這樣其內容就不會馬上顯示出來。摺疊通常用於已完成或取消的階段。新的代碼一定不要忘記加入到models/__init__.py文件中,當前內容爲:

下一步,我們需要向圖書借閱模型添加階段字段stage。編輯library_checkout/models/library_checkout.py文件,在 Checkout 類的最後面(line_ids 字段後)添加如下代碼:

stage_id是一個與階段模型的 many-to-one關聯。我們還添加了 state 字段,這是一個讓階段的 state 字段在當前模型中可用的關聯字段,這樣才能在視圖中使用。階段的默認值由_default_stage() 函數來計算,它返回階段模型的第一條記錄。因爲階段模型已通過 sequence 排序,所以返回的是 sequence 值最小的一條記錄。

group_expand參數重載字段的分組方式,默認的分組操作行爲是僅能看到使用過的階段,而不帶有借閱文檔的階段不會顯示。在我們的例子中,我們想要不同的效果:我們要看到所有的階段,哪怕它沒有文檔。_group_expand_stage_id() 幫助函數返回分組操作需使用組記錄列表。本例中返回所有已有階段,不論其中是否包含圖書借閱記錄。

ℹ️Odoo 10中的修改
group_expand字段在Odoo 10中引入,但在官方文檔中沒有介紹。使用示例在 Odoo 的源代碼中可以找到,比如在 Project 應用中:GitHub 倉庫

既然我們添加了新模塊,就應該在security/ir.model.access.csv文件中加入對應的安全權限,代碼如下:

我們需要一組階段來進行操作,所以下面來爲模塊添加默認數據。創建data/library_checkout_stage.xml文件並加入如下代碼:

要使文件生效,需先在library_checkout/__manifest__.py文件中添加該文件:

Odoo 12開發之業務邏輯 - 業務流程的支持

備註:上圖爲通過開發者菜單中Edit View: Form編輯添加了 stage_id 後的效果。

ORM 方法裝飾器

就我們目前碰到的 Odoo 中 Python 代碼,裝飾器,如@api.multi通常用於模型方法中。這對 ORM 非常重要,允許它給這些方法特殊用法。下面就來看看有哪些 ORM 裝飾器以及如何使用。

記錄集方法:@api.multi

大多數情況下,我們需要一個自定義方法來對記錄集執行一些操作。此時就需要使用@api.multi,並且此處self參數就是要操作的記錄集。方法的邏輯通常會包含對 self 的遍歷。@api.multi是最常用的裝飾器。

小貼士:如果模型方法沒有添加裝飾器,默認就使用@api.multi。

單例記錄方法:@api.one

有些情況下方法用於操作單條記錄(單例),此時可使用@api.one裝飾器。現在仍可使用@api.one,但在 Odoo 9中已聲明爲棄用。它包裹裝飾的方法,進行 for 循環遍歷,它調用裝飾方法,一次一條記錄,然後返回一個結果列表。因此在@api.one裝飾的方法內,self 一定是單例。

小貼士:@api.one的返回值有些搞怪,它返回一個列表,而不實際方法返回的數據結構。比如方法代碼如果返回字典,實際返回值是一個字典值列表。這種誤導性也是該方法被棄用的主要原因。

對於要操作單條記錄的方法,我們應還是使用@api.multi,在代碼頂部添加一行self.ensure_one(),來確保操作的是單條記錄。

類靜態方法:@api.model

有時方法需要在類級別而不是具體記錄上操作。面向對象編程語言中,這稱之爲靜態方法。這些類級別的靜態方法應由@api.model裝飾。在這些情況下,self 應作爲模型的引用 ,無需包含實際記錄。

ℹ️@api.model裝飾的方法無法用於用戶界面按鈕,在這種情況下,應使用@api.multi。

onchange 方法

onchange由用戶界面表單視圖觸發,當用戶編輯指定字段值時,立即執行一段業務邏輯。這可用於執行驗證,向用戶顯示消息或修改表單中的其它字段。支持該邏輯的方法就使用@api.onchange(‘fld1’, ‘fld2’, …)裝飾。裝飾器的參數是用戶界面通過編輯需觸發方法的字段名。

小貼士:通過爲字段添加屬性on_change=」0″可在特定表單中關閉 on change 行爲,比如<field name=」fld1″ on_change=」0″ />

在方法內,self 參數是帶有當前表單數據的一條虛擬記錄。如果在記錄上設置了值,就會在用戶界面表單中被修改。注意它並沒有向數據庫實際寫入記錄,而是提供信息來修改 UI表單中的數據。無需返回信息,但可以返回一個字典結構的警告信息來顯示在用戶界面中。

作爲示例,我們可以使用它來執行借閱表單中的部分自動化:在圖書會員變更時,請求日期設置爲當天,並且顯示一個警告信息告知用戶。下面我們就在library_checkout/models/library_checkout.py文件中添加如下代碼:

通過用戶界面修改member_id字段時,此處使用了@api.onchange裝飾器來觸發一些邏輯。實際方法不存在關聯,但按照慣例名稱應以onchange_開頭,方法中我們更新了request_date的值並返回警告信息。在onchange方法內,self 表示一條虛擬記錄,它包含當前正在編輯的記錄的所有字段,我們可以與這些字段進行交互。大多數情況下我們想要根據修改字段設置的值自動在其它字段填充值。本例中,我們將request_date更新爲當天。

onchange 方法無需返回任何值,但可以返回一個包含警告或作用域鍵的字典:

  • 警告的鍵應描述顯示在對話框中的消息,如{‘title’: ‘Message Title’, ‘message’: ‘Message Body’}
  • 作用域鍵可設置或修改其它字段的域屬性。通過讓to-many字段僅展示在當下有意義的字段,會使得用戶界面更加友好。作用域鍵類似這樣:{‘user_id’: [(’email’, ‘!=’, False)]}

其它模型方法裝飾器

以下裝飾器也會經常使用到,它們與模型內部行爲有關,在第六章 Odoo 12開發之模型 – 結構化應用數據中進行了詳細討論。羅列如下供您參考:

  • @api.depends(fld1,…)用於計算字段函數,來識別(重新)計算應觸發什麼樣的修改。必須設置在計算字段值上,否則會報錯。
  • @api.constrains(fld1,…)用於模型驗證函數並在任意參數中包含的字段修改時執行檢查。它不應向數據庫寫入修改,如檢查失敗,則拋出異常。

使用 ORM 內置方法

上一部分討論的裝飾器允許我們爲模型添加一些功能,如實施驗證或自動運算。

ORM 提供對模型數據執行增刪改查(CRUD)操作的方法。下面我們來探討如何擴展寫操作來支持自定義邏輯。讀取數據的主要方法search()和browse()在中第七章 Odoo 12開發之記錄集 – 使用模型數據已進行討論。

寫入模型數據的方法

ORM 爲三種基本寫操作提供了三個方法,如下所示:

  • <Model>.create(values)在模型上創建新記錄,它返回所創建記錄。
  • <Recordset>.write(values) 更新記錄集中的字段值,它不返回值。
  • <Recordset>.unlink()從數據庫中刪除記錄,它不返回值。

values參數是一個字典,映射要寫入的字段名和值。這些方法由@api.multi裝飾,除create()方法使用@api.model裝飾器外。

ℹ️Odoo 12中的修改
create()現在也可批量創建數據,這通過把單個字典對象修改爲字典對象列表來傳參進行實現。這由帶有@api.model_create_multi裝飾器的create() 方法來進行支持。

有些情況下,我們需要擴展這些方法來添加一些業務邏輯,在這些操作執行時觸發。通過將邏輯放到自定義方法的適當位置,我們可以讓代碼在主操作執行之前或之後運行。

我們將使用借閱模型類創建一個示例:添加兩個日期字段來記錄進入 open 狀態的時間和進入 closed 狀態的時間。這是計算字段所無法實現的,我們還將添加一個檢查來阻止對已爲 done 狀態的創建借閱。

因此我們應在 Checkout 類中添加兩個新字段,在library_checkout/models/library_checkout.py文件中添加如下代碼:

現在就可以創建自定義的create()方法來設置checkout_date了,如果狀態正確則創建,而如果已經是完成狀態則不予創建,代碼如下:

注意在實際新記錄創建之前,不存在其它記錄,僅帶有用於創建記錄的值的字典。這也就是我們使用browse()來獲取新記錄stage_id的原因,然後對值進行相應的檢查。作爲對比,一旦創建了新記錄,相應的操作就變簡單了,使用對象的點號標記即可:new_record.state。在執行super().create(vals)命令之前可以對值字典進行修改,我們使用它在狀態合適的情況下寫入checkout_date。

ℹ️Odoo 11中的修改
Python 3中有一種super()的簡寫方式,我們上例中使用的就是這種方式。而在 Python 2中則寫成super(Checkout, self).create(vals),其中 Checkout 爲代碼所在的 Python 類名。在 Python 3這種語法仍然可用,但同時帶有簡寫語法:super().create(vals)。

修改記錄時,如果訂閱進入的是合適的狀態我們需要更新checkout_date和close_date。實現這一功能需要使用自定義的write() 方法,代碼如下:

我們一般會盡量在super().write(vals)之前修改寫入的值。如果write()方法在同一模型中有其它的寫操作,會導致遞歸循環,它在工作進程資源耗盡後結束並報錯。請考慮是否需要這麼做,如果需要,避免遞歸循環的一個技巧是在上下文中添加一個標記。作爲示例,我們添加類似如下代碼:

通過這個技巧,具體的邏輯受到 if 語句的保護,僅在上下文中出現指定標記時纔會運行。再深入一步,self.write()操作應使用with_context來設置標記。這種組合確保 if 語句中自定義登錄(login)只執行一次,並且不會觸發更多的write()調用,避免進入無限循環。

在write()內運行write()方法會導致無限循環。要避免這一循環,我們需要在上下文中設置標記值來在代碼中進行檢查避免進入循環。

應仔細考慮是否需要對create或write方法進行擴展。大多數情況下我們只需要在保存記錄時執行一些驗證或自動計算某些值:

  • 對於根據其它字段自動計算的字段值,我們應使用計算字段。這樣的例子有在各行值修改時對頭部彙總的計算。
  • 要使字段默認值動態計算,我們可以將字段賦值的默認值修改爲一個函數綁定。
  •  要讓字段根據其它字段的修改來設置值,我們可以使用 onchange 函數。舉個例子,在選定客戶時,將用戶的幣種設置爲文檔的幣種,但隨後可由用戶手動修改。記住 onchange 僅用於表單視圖的交互,不直接進行寫入調用。
  • 對於驗證,我們應使用由@api.constraints(fld1,fld2,…)裝飾的約束函數。這和計算字段相似,但不同處在於它會拋出錯誤。

數據導入、導出方法

導入、導出操作在第五章 Odoo 12開發之導入、導出以及模塊數據已做討論,也可以通過 ORM API 中的如下方法操作:

  • load([fields], [data]) 用於導入從 CSV 文件中獲取的數據。第一個參數是導入的字段列表,與 CSV 的第一行對應。第二個參數是記錄列表,每條記錄是一個待解析和導入的字符串列表,與 CSV 數據中的行和列直接對應。它實現了 CSV 數據導入的功能,比如對外部標識符的支持。它用於網頁客戶端的導入函數。
  • export_data([fields], raw_data=False)用於網頁客戶端導出函數。它返回一個字典,帶有包含數據(一個行列表)的數據鍵。字段名可使用 CSV 文件使用的.id和/id後綴,數據格式與需導入的 CSV 文件兼容。可選raw_data參數讓數據值與 Python 類型一同導出,而不是 CSV 文件中的字符串形式。

用戶界面的支持方法

以下方法最常用於網頁客戶端中渲染用戶界面和執行基礎交互:

  • name_get()返回一個表示每條記錄的文本的元組(ID, name)列表。它默認用於計算display_name值,來提供關聯字段的文本表示。可擴展它來實現自定義的顯示方式,如將僅顯示名稱改爲顯示記錄編號和名稱。
  • name_search(name=」, args=None, operator=’ilike’, limit=100)返回一個元組(ID, name)列表,其顯示名與 name 參數的文本相匹配。它用於 UI 中,在關聯字段中通過輸入來生成帶有匹配所輸入文本推薦記錄的列表。例如,它可用於在挑選產品的字段中輸入時,實現通過名稱和引用來查找產品。
  • name_create(name)創建一條僅帶有要使用的標題名的新記錄。它用於在 UI 中快速創建(quick-create)功能,這裏我們可以僅提供名稱快速創建一條關聯記錄。可擴展來爲通過此功能創建的新記錄提供指定默認值。
  • default_get([fields])返回一個帶有要創建的新記錄默認值的字典。默認值可使用變量,如當前用戶或會話上下文。
  • fields_get()用於描述模型字段的定義,在開發者菜單的View Fields選項中也可以看到。
  • fields_view_get()在網頁客戶端中用於獲取要渲染的 UI視圖的結構。可傳入視圖的 ID或想要使用的視圖類型(view_type=’form’)作爲參數。例如可使用self.fields_view_get(view_type=’tree’)。

消息和活動(activity)功能

Odoo 自帶全局的消息和活動規劃功能,由 Discuss 應用提供,技術名稱爲 mail。mail 模塊提供包含mail.thread抽象類,它讓在任意模型中添加消息功能都變得很簡單。還提供mail.activity.mixin用於添加規劃活動功能。在第四章 Odoo 12 開發之模塊繼承中已講解了如何從 mixin 抽象類中繼承功能。

要添加這些功能,我們需要在library_checkout中先添加對 mail 的依賴,然後在圖書借閱模型中繼承抽象類中提供的這些功能。編輯library_checkout/__manifest__.py文件,在 depends 鍵下添加 mail 模塊:

然後編輯library_checkout/models/library_checkout.py文件來繼承 mixin 抽象模型,代碼如下:

然後我們的模型就會添加三個新字段,每條記錄(有時也稱文檔)都包含:

  • mail_follower_ids:存儲 followers 和相應的通知首選項
  • mail_message_ids:列出所有包含關聯活動規劃的關聯messages.activity_id

follower 可以是夥伴(partner)或頻道(channel)。partner表示一個具體的人或組織,頻道不是具體的人,而是體現爲訂閱列表。每個follower還有一個他們訂閱的消息類型列表,僅有已選消息類型纔會發送通知。

消息子類型

一些消息類型稱爲子類型,它們存儲在mail.message.subtype模型中,可通過Settings > Technical > Email > Subtypes菜單訪問。默認我們有如下三種消息子類型:

  • Discussions:帶有mail.mt_comment XML ID,用於創建帶有Send message鏈接的消息,默認會發送通知。
  • Activities:帶有mail.mt_activities XML ID,用於創建帶有Schedule activity鏈接的消息,默認不會發送通知。
  • Note:帶有mail.mt_note XML ID,用於創建帶有Log note鏈接的消息,默認不會發送通知。

子類型默認通知設置如上所述,但用戶可以就具體文檔來進行調整,比如關閉他們不感興趣的討論的通知。除內置子類型之外,我們還可以添加自己的子類型並在應用中自定義通知。子類型既可以是通用的也可以只針對具體模型。對於後者,我們應將其所作用的模型名填入子類型的res_model字段中。

Odoo 12開發之業務邏輯 - 業務流程的支持

發送消息

我們的業務邏輯可利用這個消息系統來向用戶發送通知。可使用message_post() 方法來發送通知,示例如下:

這會添加一個普通文本消息,但不會向follower發送通知。這是因爲默認由mail.mt_note子類型發送消息。但我們可以通過指定的子類型來發送消息。要添加一條向follower發送通知的消息,應使用mt_comment子類型。另一個可選屬性是消息標題,使用這兩項的示例如下:

消息體是HTML格式的,所以我們可以添加標記來實現文本效果,如<b>爲加粗,<i>爲斜體。

ℹ️出於安全原因消息體會被清洗,所以有些 HTML 元素可能最終無法出現在消息中。

添加 follower

從業務邏輯角度來看還有一個有意思的功能:可以向文檔添加 follower,這樣他們可以獲取相應的通知。我們有以下幾種方法來添加 follower:

  • message_subscribe(partner_ids=<整型 id 列表>)添加夥伴
  • message_subscribe(channel_ids=<整型 id 列表>) 添加頻道
  • message_subscribe_users(user_ids=<整型 id 列表>) 添加用戶

默認的子類型會作用於每個訂閱者。強制訂閱指定的子類型列表,可添加subtype_ids=<整型 id 列表>屬性,來列出在訂閱中使用指定子類型。

創建嚮導

假定我們的圖書館用戶需要向一組借閱者發送消息。比如他們可選擇某本書最早的借閱者,向他們發送消息要求歸還圖書。這可通過嚮導來實現。嚮導是接受用戶輸入的一系列表單,然後使用輸入來做進一步操作。

我們的用戶開始從借閱列表中選擇待使用的記錄,然後從視圖頂級菜單中選擇 wizard 選項。這會打開向導表單,可填入消息主題和內容。一旦點擊 Send 就將會向所有已選借閱者發送消息。

嚮導模型

嚮導對用戶顯示爲一個表單視圖,通常是一個對話窗口,可填入一些字段。這些字段會隨後在嚮導邏輯中使用。這通過普通視圖同樣的模型/視圖結構實現,但支持的模型繼承的是models.TransientMode而不是models.Model。這種類型的模型也會在數據庫體現並存儲狀態,但數據僅在嚮導完成操作前有用。定時 job 會定期清除嚮導數據表中的老數據。

我們將使用wizard/checkout_mass_message.py 文件來定義與用戶交互的字段:通知的訂閱者列表,標題和消息體。

首先編輯library_checkout/__init__.py文件並導入wizard/子目錄:

添加wizard/__init__.py文件並加入如下代碼:

然後創建實際的wizard/checkout_mass_message.py文件,內容如下:

值得注意的是普通模型中的one-to-many關聯不能在臨時模型中使用。這是因爲那樣就會要求普通模型中添加與臨時模型的反向many-to-one關聯。但這是不允許的,因爲那樣普通記錄的已有引用會阻止對老的臨時記錄的清除。替代方案是使用many-to-many關聯。

ℹ️Many-to-many關聯存儲在獨立的表中,會在關聯任意一方被刪除時自動刪除表中對應行。

臨時模型無需安全規則 ,因爲它們是用於輔助執行的一次性記錄。那麼也就不需要添加ecurity/ir.model.access.csv權限控制列表文件。

嚮導表單

嚮導表單視圖與普通模型相同,只是它有兩個特定元素:

  • 可使用<footer>元素來替換操作按鈕
  • special=」cancel」按鈕用於中斷嚮導,不執行任何操作

wizard/checkout_mass_message_wizard.xml文件的內容如下:

XML 中的窗口操作使用src_model屬性向圖書借閱的Action按鈕添加了一個選項。target=」new」屬性讓它以對話窗口形式打開。打開向導,我們可以從借閱列表中選擇一條或多條記錄,然後從Action菜單中選擇 Send Messages 選項,Action 菜單顯示在列表頂部的Filters菜單旁。

Odoo 12開發之業務邏輯 - 業務流程的支持

現在這會打開向導表單,但從列表中所選的記錄會被忽略。如果能在嚮導中任務列表中顯示預選的記錄會很棒。表單會調用default_get() 方法來計算要展示的默認值,這正是我們需要的功能。注意在打開向導表單時,有一條空記錄並且還沒有使用create()方法,該方法僅在點擊按鈕時纔會觸發,所以暫不能滿足我們的需求。

Odoo 視圖向上下文字典添加一些元素,可在點擊操作或跳到其它視圖時使用。它們分別是:

  • active_model:帶有視圖模型的技術名
  • active_id:帶有表單活躍記錄或表中第一條記錄的 ID
  • active_ids:帶有一個列表中活躍記錄的列表(如果是表單則只有一個元素)
  • active_domain:如果在表單視圖中觸發了該操作

本例中,active_ids中保存任務列表中所選記錄的 ID,可使用這些 ID 作爲嚮導task_ids字段的默認值,相關代碼如下(izard/checkout_mass_message.py):

我們首先使用了super()來調用標準的default_get()運算,然後向默認值添加了一個checkout__id,而active_ids值從環境下文中讀取。

下面我們需要實現點擊表單中Send按鈕的操作。

嚮導業務邏輯

除了無需進行任何操作僅僅關閉表單的 Cancel 按鈕外,我們還有一個Send按鈕的操作需要實現。該按鈕調用的方法爲button_send,需要在wizard/checkout_mass_message.py文件中使用如下代碼定義:

我們的代碼一次僅需處理一個嚮導實例,所以這裏通過self.ensure_one()以示清晰。這裏的 self 表示嚮導表單裏顯示的數據。以上方法遍歷已選借閱記錄並向其中的每個借閱者發送消息。這裏使用mt_comment子類型,因此會向每個 follower 發送消息通知。

ℹ️讓方法至少返回一個 True 值是一個很好的編程實踐。主要是因爲有些XML-RPC協議不支持 None 值,所以對於這些協議就用不了那些方法了。在實際工作中,我們可能不會遇到這個問題,因爲網頁客戶端使用JSON-RPC而不是XML-RPC,但這仍是一個可遵循的良好實踐。

Odoo 12開發之業務邏輯 - 業務流程的支持

使用日誌消息

向日志文件寫入消息有助於監控和審計運行的系統。它還有助於代碼維護,在無需修改代碼的情況下可以從運行的進程中輕鬆獲取調試信息。要讓我們的代碼能使用日誌功能,首先要準備一個日誌記錄器(logger),在library_checkout/wizard/checkout_mass_message.py文件的頭部添加如下代碼:

這裏使用了 Python標準庫logging模塊。_logger通過當前代碼文件名__name__來進行初始化。這樣日誌信息就會帶有生成日誌文件的信息。有以下幾種級別的日誌信息:

現在就可以使用logger向日志中寫入消息了,讓我們爲button_send嚮導方法來添加日誌。在文件最後的return True前添加如下代碼:

這樣在使用嚮導發送消息時,服務器日誌中會出現類似如下消息:

注意我們沒有在日誌消息中使用 Python 內插字符串。我們沒使用_logger.info(‘Hello %s’ % ‘World’),而是使用了類似_logger.info(‘Hello %s’, ‘World’)。不使用內插使我們的代碼少執行一個任務,讓日誌記錄更爲高效。因此我們應一直爲額外的日誌參數傳入變量。

ℹ️服務器日誌的時間戳總是使用 UTC 時間。因此打印的日誌消息中也是 UTC 時間。你可能會覺得意外 ,但 Odoo服務內部都是使用 UTC 來處理日期的。

對於調試級別日誌,我們使用_logger.debug()。例如,可以在checkout.message_post() 命令後添加如下調試日誌消息:

這不會在服務器日誌中顯示任何消息,因爲默認的日誌級別是INFO。需要將日誌級別設置爲DEBUG纔會輸出調試日誌消息。

Odoo 命令行選項–log-level=debug可用於設置通用日誌級別。我們還可以對指定模塊設置日誌級別。我們的嚮導的 Python 模塊是odoo.addons.library_checkout.wizard.checkout_mass_message,這在 INFO 日誌消息中也可以看到。要開啓嚮導的調試消息,使用–loghandler 選項,該選項還可重複多次來對多個模塊設置日誌級別,示例如下:

有關 Odoo 服務器日誌選項的完整手冊可參見官方文檔。如果想要了解原始的 Python 日誌細節,可參見Python 官方文檔

拋出異常

在操作和預期不一致時,我們可能需要通知用戶並中斷程序,顯示錯誤信息。這可通過拋出異常來實現。Odoo 中提供了一些異常類供我們使用。插件模塊中最常用的 Odoo 異常有:

ValidationError異常用於 Python 代碼中的驗證,比如使用@api.constrains裝飾的方法。UserError應該用在其它所有操作不被允許的情況,因爲這不符合業務邏輯。

ℹ️Odoo 9中的修改
引用了UserError異常來替換掉Warning異常,淘汰掉 Warning 異常的原因是因爲它與 Python 內置異常衝突,但 Odoo 保留了它以保持向後兼容性。

通常所有在方法執行期間的數據操縱在數據庫事務中,發生異常時會進行回滾。也就是說在拋出異常時,所有此前對數據的修改都會被取消。

下面就使用本例嚮導button_send方法來進行舉例說明。試想一下如果執行發送消息邏輯時沒有選中任何借閱文檔是不是不合邏輯?同樣如果沒有消息體就發送消息也不合邏輯。下面就來在發生這些情況時向用戶發出警告。

編輯button_send()方法,在self.ensure_one()一行後加入如下代碼:

補充:經測試發現消息體不填內容並不會拋出異常,因爲默認的會發送<p><br></p>這段 html 標籤

Odoo 12開發之業務邏輯 - 業務流程的支持

單元測試

自動化測試是廣泛接受的軟件開發最佳實踐。不僅可以幫助我們確保代碼正確實施,更重要的爲我們未來的代碼修改和重寫提供了一個安全保障。對於 Python 這樣的動態編程語言,因爲沒有編譯這一步,語法錯誤經常不容易注意到。這也使得單元測試愈發重要,覆蓋的代碼行數越多越好。

以上兩個目標是我們編寫測試時的燈塔。測試的第一個目標應是提供更好的測試覆蓋:設置測試用例運行所有代碼行。單單這個就會爲第二個目標邁出很大一步:顯示代碼有無功能性錯誤,因爲在這之後,我們一定可以很好地開始爲不顯著的使用特例添加測試用例。

ℹ️Odoo 12中的修改
在該版本之前,Odoo 還支持通過 YAML格式的數據文件進行測試。Odoo 12中刪除了YAML數據文件引擎,不再支持該格式,有關該格式的最後一個文檔請見官方網站

添加單元測試

Python 測試文件添加在模塊的tests/子目錄下,測試執行器會自動在該目錄下查找測試文件。爲測試library_checkout模塊嚮導邏輯,我們可以創建tests/test_checkout_mass_message.py,老規矩,需要添加tests/__init__.py文件,內容如下:

tests/test_checkout_mass_message.py代碼的基礎框架如下:

Odoo 提供了一些供測試使用的類:

  • TransactionCase測試爲每個測試使用不同的事務,在測試結束時自動回滾。
  • SingleTransactionCase將所有測試放在一個事務中運行,在最後一條測試結束後才進行回滾。在每條測試的最終狀態需作爲下一條測試的初始狀態時這會非常有用。

setUp()方法用於準備數據以及待使用的變量。通常我們將數據和變量存放在類屬性中,這樣就可在測試方法中進行使用。測試應使用類方法實現,如test_button_send()。測試用例方法名必須以test_爲前綴。這些方法被自動發現,該前綴就是用於辨別是否爲實施測試用例的方法。根據測試方法名的順序來運行。

在使用TransactionCase類時,在每個測試用例運行完後都會進行回滾。在測試運行時會顯示方法的文檔字符串(docstring),因此可以使用它來作爲所執行測試的簡短描述。

ℹ️這些測試類是對Python 標準庫中unittest測試用例的封裝。有關unittest詳細內容,請參見官方文檔

運行測試

下面就來運行已書寫的測試。我們僅需在安裝或升級(-i或-u)模塊時在 Odoo 服務啓動命令中添加– test-enable選項即可。具體命令如下:

僅在安裝或升級模塊時纔會運行測試,這也就是爲會什麼添加了-u 選項。如果需要安裝一些依賴,它的測試也會運行。想要避免這一情況,可以像平常那樣測試安裝模塊,然後在升級(-u)模塊時運行測試。以上測試中實際沒有做任何測試,但應該可以正常運行。仔細查看服務器日誌可看到報告測試運行的INFO信息,例如:

 

配置測試

我們應開始在setUp方法中準備測試中將使用的數據。這裏我們要創建一條在嚮導中使用的借閱記錄。使用指定用戶執行測試操作會很便捷,這樣可以同時測試權限控制是否正常配置。這通過sudo(<user>)模型方法來實現。記錄集中攜帶這一信息,因此在使用 sudo()創建後,相同記錄集後續的操作都會使用相同上下文執行。以下是setUp方法中的代碼:

此時我們就可以在測試中使用self.checkout0記錄和self.Wizard模型了。

編寫測試用例

現在讓我們來擴展一下初始框架中的test_button_test()方法吧。最簡單的測試是運行測試對象中的部分代碼,獲取結果,然後使用斷言語句來與預期結果進行對比。

要測試發送消息的方法,測試計算嚮導運行前後的消息條數來確定有沒有增加新消息。要運行嚮導,需要在上下文中設置active_ids,像 UI 表單一樣,創建帶有填寫嚮導表單(至少是消息體)的嚮導記錄,然後運行button_send方法。完整代碼如下:

這一檢測在self.assertEqual語句中驗證測試成功還是失敗。它對比運行嚮導前後的消息數,預期會比運行前多一條消息。最後一個參數在測試失敗時作爲信息提示,它是可選項,但推薦使用。

assertEqual方法僅是斷言方法的一種,我們應根據具體用例選擇合適的斷言方法,這樣才更易於理解導致測試錯誤的原因。單元測試文檔提供對所有這些方法的說明,參見 Python 官方文檔

要添加新的測試用例,在類中添加另一個實現方法。要記住TransactionCase測試,每次測試結束後都會回滾。因此,前一次測試的操作會被撤銷,我需要重新打開向導表單。然後模擬用戶填寫消息內容,執行消息發送。最後檢測消息條數來進行驗證。

補充:此處原文已慘不忍睹,通篇是任務清單項目的描述,筆者自行做了對應的調整。

測試異常

有時我們需要測試來檢查是否生成了異常,常用的情況是測試是否正確地進行了驗證。本例中,我們可以測試嚮導的一些驗證。例如,我們可以測試空消息體拋出錯誤。要檢查是否拋出異常,我們將相應代碼放在self.assertRaises()代碼塊中。

首先在文件頂部導入 Odoo 的異常類:

然後,在測試類中添加含有測試用例的另一個方法:

如果button_send()沒有拋出異常,則檢測失敗。如果拋出了異常,檢測成功並將異常存儲在 e 變量中,我們可以使用它來做進一步的檢測。

Odoo 12開發之業務邏輯 - 業務流程的支持

開發工具

開發者應學習一些技巧有協助開發工作。本系列曾介紹過用戶界面的開發者模式。也可以在服務端使用該選項來提供對開發者更友好的功能。這一部分就來進行詳細說明。然後我們會討論另一個開發者相關話題:如何對服務端代碼進行調試。

服務端開發選項

Odoo服務提供一個–dev選項來開啓開發者功能、加速開發流程,比如:

  • 在發現插件模塊中有異常時進入調試器
  • Python 文件保存時自動重新加載代碼,避免反覆手動重啓服務
  • 直接從 XML 文件中讀取視圖定義,無需手動更新模塊

–dev參數接收一個逗號分隔列表選項,通常 all 選項可適用大多數情況。我們可以指定想要用的調試器。默認使用Python 調試器pdb,有些人可能喜歡安裝使用其它調試器,Odoo 對ipdb和pudb都予以支持。

ℹ️Odoo 9中的修改
Odoo 9之前的版本中,可使用–debug 選項來對某一模塊異常打開調試器。從Odoo 9開始不再支持改選項,改用– dev=all選項了。

在使用 Python 代碼時,每次代碼修改都需重啓服務來重新加載代碼。–dev命令選項會處理重新加載,在服務監測到 Python 代碼被修改時,自動重複服務加載序列,讓代碼修改立即生效。使用它僅需在服務命令後添加–dev=all 選項:

要正常運行,要求安裝watchdog Python包,可通過 pip 命令來執行安裝:

注意這僅對 Python 代碼和 XML 文件中視圖結構的修改有益。對於其它修改,如模型數據結構,需要進行模塊升級,僅僅重新加載是不夠的。

調試

我們都知道開發者的大部分工作是調試代碼。我們通常使用代碼編輯器打斷點,運行程序來進行單步調試。如果使用 Windows 系統來開發,配置可運行 Odoo 源碼的環境可不是個簡單的工作。 Odoo是一個等待客戶端調用的服務,然後才進行操作,這一事實讓 Odoo 的調試與客戶端程序截然不同。

Python 調試器

對於初學者可能有點高山仰止的感覺,最實際的方法是使用 Pyhton 集成的調試器pdb來對 Odoo 進行調試。我們會介紹它的擴展,會提供豐富的用戶界面,類似於高級 IDE那樣。

要使用調試器,最好的方法是在需要查看的代碼(通常是模型方法)處插入斷點。這通過在具體位置添加如下行來實現:

現在重啓服務來加載修改代碼。一旦程序運行到該行,服務運行窗口就會進入一個(pdb)Python 命令對話框,等待輸入。這個對話框和 Python shell 一樣,你可以輸入當前執行上下文的任何表達式或命令來運行。這意味着可以檢查甚至修改當前變量,以下是最常用的快捷命令:

  • h (help) 顯示可用 pdb 命令的彙總
  • p (print) 運行並打印表達式
  • pp (pretty print) 有助於打印數據結構,如字典或列表
  • l (list) 列出下一步要執行的代碼及周邊代碼
  • n (next) 進入下一條命令
  • s (step) 進入當前命令
  • c (continue)繼續正常執行
  • u (up) 在執行棧中上移
  • d (down)在執行棧中下移
  • bt (backtrace)顯示當前執行棧

如果啓動服務時使用了dev=all選項,拋出異常時服務在對應行進行後驗模式。這是一個pdb對話框,和前述的一樣,允許我們檢查在發現錯誤那一刻的程序狀態。

Odoo 12開發之業務邏輯 - 業務流程的支持

示例調試會話

讓我們來看看一個簡單調試會長什麼樣。可以在button_send嚮導方法的第一行添加調試器斷點:

現在重啓服務,打開一個發送消息嚮導表單並點擊 Send 按鈕。這會觸發服務器上的button_send ,客戶端會保持在Still loading…的狀態,等待服務端響應。查看運行服務的終端窗口,可看到類似如下信息:

這是pdb調試器對話框,第一行告訴我們 Python 代碼執行的位置以及所在的函數名,第二行是要運行的下一行代碼。在調試會話中,可能會跳出一些日誌消息。這對於調試沒有傷害,但會打擾到我們。可以通過減少日誌輸出來避免這一情況。大多數據情況下日誌消息都來自werkzeug模塊。我們可通過–log-handler=werkzeug:CRITICAL 選項來停止日誌輸出。如果這還不夠,可以使用–log-level=warn來降低通用日誌級別。另一種方法是啓用–logfile=/path/to/log選項,這樣會將日誌消息從標準輸出重定向到文件中。

小貼士:如果終端不響應,在終端中的輸入不被顯示,這可能與終端會話的顯示問題有關,通過輸入<enter>reset<enter>有可能解決這一問題。

此時輸入 h,可以看到可用命令的一個快速指南。輸入 l 顯示當前行代碼,以及其周邊的代碼。輸入 n 會運行當前行代碼並進入下一行。如果只按下 Enter,會重複上一條命令。執行三次應該就可以進入方法的 return 語句。我們可以查看任意變量或屬性的內容,如嚮導中使用的checkout_ids字段:

它允許使用任何 Python 表達式,甚至是分配變量。我們可以逐行調試,在任意時刻按下 c 繼續正常運行。

其它 Python 調試器

pdb 的優勢是「開箱即用」,它簡單但粗暴,還有一些使用上更舒適的選擇。

ipdb(Iron Python debugger)是一個常用的選擇,它使用和 pdb 一樣的命令,但做了一些改進,比如添加 Tab 補全和語法高亮來讓使用更舒適。可通過如下命令安裝:

使用如下行添加斷點:

Odoo 12開發之業務邏輯 - 業務流程的支持

另一個可選調試器是pudb,它也支持和pdb相同的命令,僅用在文本終端中,但使用了類似 IDE 調試器的圖形化顯示。當前上下文的變量及值這類有用信息,在屏幕上它自己的窗口中顯示。可通過系統包管理器或 pip 來進行安裝:

添加pudb斷點和其它調試器沒什麼分別:

也可以使用更短更易於記憶的方式:

 

Odoo 12開發之業務邏輯 - 業務流程的支持

打印消息和日誌

有時我們只需要查看一些變量的值或者檢查一些代碼段是否被執行。Python的print()函數可以在不中斷執行流的情況下完美解決這些問題。因爲我們在服務器窗口中運行,打印的內容會顯示在標準輸出中,但如果日誌是寫入文件的打印內容不會存儲到服務器日誌中。

print()僅用於開發輔助,不應出現最終部署的代碼中。如果你可能需要代碼執行的更多細節,請使用debug 級別日誌消息。在代碼敏感點添加調試級別日誌消息讓我們可以在部署實例中調查問題。只需將服務器日誌級別提升爲 debug,然後查看日誌文件。

查看和關閉運行進程

還有一些查看 Odoo 運行進程的小技巧。首先我們需要找到相應的進程ID (PID),要找到 PID,再打開一個終端窗口並輸入如下命令:

輸入的第一列是進程的PID,記錄下要查看進程的 PID,在下面會使用到。現在,我們要向進程發送信號。使用的命令是 kill,默認它發送一個終止進程的信號,但也可以發送其它更友好的信號。知道了運行中的 Odoo 服務進程 PID,我們可以通過向進程發送SIGQUIT信號打印正在執行的代碼蹤跡,命令如下:

然後如果我們查看終端窗口或寫入服務輸出的日誌文件,就可以看到正常運行的一些線程的信息,以及它們運行所在行代碼的細節棧蹤跡。這用於一些代碼性能分析中,追蹤服務時間消耗在何處,來將代碼執行性能歸類。有關代碼profiling的資料可參見官方文檔。其它可向 Odoo 服務器進程發送的信號有:HUP來重新加載服務,INT或TERM強制服務關閉:

 

總結

我們詳細解釋了ORM API 的功能,以及如何使用這些功能來創建動態應用與用戶互動,這可以幫助用戶避免錯誤並自動化一些單調的任務。

模型驗證和計算字段可以處理很多用例,但並不是所有的。我們學習瞭如何繼承API的create, write和unlink 方法來處理更多用例。

對更豐富的用戶交互,我們使用了 mail 內核插件 mixin 來爲用戶添加功能,方便他們圍繞文檔和活動規則進行交流。嚮導讓應用可以與用戶對話,收集所需數據來運行具體進程。異常允許應用終止錯誤操作,通知用戶存在的問題並回滾中間的修改,保持系統的一致性。

我們還討論了開發者可使用來創建和維護應用的工具:記錄日誌消息、調試工具和單元測試。

在下一篇文章中,我們還將使用 ORM,但會從外部應用的視角來操作,將 Odoo 服務器作爲存儲數據和運行業務進程的後端。