跳至內容

【Odoo 開發分享】如何建立第一個 Odoo 應用程式(三)

大家好,

增加列表以及搜尋視圖

上篇文章談到如何設定表單視圖,這次要介紹另外兩種常用的視圖。在列表模式下查看模型時,使用 <tree> 視圖。<tree> 視圖能夠按層次結構顯示,但大多數情況下它們都用於顯示簡單列表。

我們可以將以下的 <tree> 視圖定義加入到 todo_view.xml 中:

<record id="view_tree_todo_task" model="ir.ui.view">
    <field name="name">To-do Task Tree</field>
    <field name="model">todo.task</field>
    <field name="arch" type="xml">
        <tree colors="decoration-muted:is_done==True">
            <field name="name"/>
            <field name="is_done"/>
        </tree>
    </field>
</record>

程式定義了一個只有兩行的列表:name 和 is_done。

我們還加入了一個很好的使用者體驗:已完成任務 (is_done==True) 的行顯示為灰色。

這是透過 Bootstrap 的類別來完成的。搜尋 http://getbootstrap.com/docs/3.3/css/helper-classes-colors 以獲取 Bootstrap 及其上下文顏色的更多訊息。

因為 is_done 是一個布林欄位,所以 “decoration-muted:is_done==True” 可以優雅地簡化為 “decoration-muted:is_done”。

在列表的右上角,Odoo 顯示一個搜索框。它搜索的欄位和可用的過濾器是由視圖所定義。

和以前一樣,我們將這個加入到 todo_view.xml 中:

<record id="view_filter_todo_task" model="ir.ui.view">
    <field name="name">To-do Task Filter</field>
    <field name="model">todo.task</field>
    <field name="arch" type="xml">
        <search>
            <field name="user_id"/>
            <filter string="Not Done" domain="[('is_done','=',False)]"/>
            <filter string="Done" domain="[('is_done','!=',False)]"/>
        </search>
    </field>
</record>

<field> 元素定義在搜索框中鍵入時也搜索的欄位。我們加入了 user_id 在 Responsible 欄位中進行自動建議搜尋。

<filter> 元素加入預定義的過濾條件,可以使用特定語法定義的使用者點擊進行切換。

擴展視圖

表單,列表和搜索視圖是使用 arch XML 結構定義的。為了擴展視圖,我們需要一種方法來修改這個 XML。

那就是用定位 XML 元素方式,然後在這些定位點上引入修改。視圖繼承的 XML 記錄與常規視圖的 XML 記錄相似,但也使用了 inherit_id 屬性,並引用了要擴展的視圖。

舉一個例子,我們將擴展 Partner 視圖,使 todo_ids 欄位可見,並顯示所涉及的所有任務。

首先要做的是找到要擴展的視圖的 XML ID。我們可以在 Settings 中的 Technical | User Interface | Views 找到 Partner 的 XML ID 是 base.view_partner_form。在那裡,我們也應該找到我們想要插入我們的改變的 XML 元素。我們將在 Language 欄位之後的右行末尾增加"協同作者"列表。我們通常透過其名稱屬性來標識要使用的元素。這裡是 <field name="lang" />。我們將為加入到 Partner 視圖的擴展加入 XML 檔案 views/res_partner_view.xml,其中包含以下內容:

<?xml version="1.0"?>
<odoo>
    <record id="my_inherited_view" model="ir.ui.view">
        <field name="name">Add To-Dos to Partner Form</field>
        <field name="model">res.partner</field>
        <field name="inherit_id" ref="base.view_partner_form"/>
        <field name="arch" type="xml">
            <field name="lang" position="after">
                <field name="todo_ids" />
            </field>
        </field>
    </record>
</odoo>

inherit_id 欄位透過特殊參數 ref 屬性引用其外部 XML ID 標識符來擴展視圖。

通常是使用 XPath 來定位 XML 元素。 Odoo 也可以使用 XPath,但我們剛剛使用的是更簡單的形式。

其他視圖類型(例如列表視圖和搜索視圖)也具有一個 arch field,並且可以像表單視圖一樣進行擴展。

商務邏輯層

現在我們將加入按鈕的邏輯。這是用 Python 程式碼完成的,使用模型的 Python 類別中的方法。

新增商務邏輯

我們應該編輯 todo_task_model.py 檔案以加入按鈕呼叫的方法。首先,我們需要導入新的 API,因此將它加入到 Python 檔案頂部的 import 語句中:

from odoo import api, fields, models

Clear Done 按鈕的邏輯非常簡單:只需設置 Active? 的 flag 為 false。

這利用了 ORM 的內建功能:預設情況下 active flag 設置為 False 的記錄被過濾掉,不會呈現給使用者。您可以將其視為 “soft delete.”。

在 models/todo_task_model.py 檔案中,將以下內容加入到 TodoTask 類別中:

@api.multi
def do_clear_done(self):
    for task in self:
        task.active = False
    return True

對於記錄上的邏輯,我們使用 @api.multi 裝飾器。在這裡,self 將代表一個記錄集,然後我們應該遍歷每條記錄。

實際上,這是 Model 方法的預設設定,所以 @api.multi 裝飾器可以在這裡省略。但我們寧願保持清晰。

程式碼循環遍歷所有待辦事項任務記錄,並為每個任務記錄指定 active 欄位上為 False。method 不需要返回任何東西,但我們應該至少返回一個 True 值。

原因是並非 XMLRPC 協議的所有客戶端實作都支援 None/Null 值,並且可能會在方法返回此類值時引發錯誤。

Extending Python methods

Python 方法也可以擴展用於其他商務邏輯。這是透過借用 Python 已經為繼承對象提供的機制來擴展它們的父類別行為來完成的:super()。

例如,我們將透過 TodoTask 類別擴展 ORM write() 方法來為其加入一些邏輯:在非活動記錄上寫入時,它會自動重新啟用它。

在 models/todo_task_model.py 檔案中加入這個附加方法:

@api.multi
def write(self, values):
    if 'active' not in values:
        values['active'] = True
    super().write(values)

write() 方法需要一個額外的參數,並在記錄上寫入值的字典。除非在 active 欄位上明確設置了值,否則我們將其設置為 True,以防我們寫入非活動記錄。然後我們使用 super()來使用修改後的值呼叫父級 write() 方法。

在以前使用 Python 2.7 的 Odoo 版本中,super() 需要兩個參數,傳遞類別名稱和 self。在這種情況下,最後一條語句是 super(TodoTask, self).write(values)。這種語法也適用於 Python 3,因此如果保持 Python 2 的兼容性非常重要,它應該是首選。

加入測試

幫程式碼進行自動化測試。對於像 Python 這樣的動態語言來說,這更為重要。由於沒有編譯步驟,因此只有在解釋器實際執行程式碼之前,您才能確定沒有語法錯誤。一個好的編輯器可以幫助我們提前發現這些問題,但不能幫助我們確保程式碼像自動化測試那樣按預期執行。

測試程式碼檔案應該有一個以 test_ 開頭的名稱,並且應該從 tests/__init__.py 中導入。但是測試目錄(也是一個 Python 子模組)不應該從模組的頂層 __init__.py 中導入,因為只有在執行測試時它才會被自動發現和載入。

測試必須放置在 tests/ 子目錄中。使用以下命令加入一個 tests/__ init__.py 檔案:

from . import test_todo

加入測試的程式碼到 tests/test_todo.py:

from odoo.tests.common import TransactionCase
class TestTodo(TransactionCase):
    def test_create(self):
        "Create a simple Todo"
        Todo = self.env['todo.task']
        task = Todo.create({'name': 'Test Task'})
        self.assertEqual(task.is_done, False)

這增加了一個簡單的測試用例來建立一個新的待辦任務並驗證是否完成?欄位具有正確的預設值。

現在我們想要執行我們的測試。這可以透過在安裝或升級模組時加入 --test-enable 選項來完成:

$ ./odoo-bin -d todo -u todo_app --test-enable

Odoo 服務器將在以下目錄中搜尋 tests/ 子目錄升級後的模組並執行它們。如果任何測試失敗,服務器日誌將顯示。

測試商務邏輯

現在我們應該為商務邏輯加入測試。理想情況下,我們希望至少一個測試用例涵蓋每行程式碼。在 tests/test_todo.py 中,向 test_create() 方法加入幾行程式碼:

def test_clear_done(self):
    # Clear Done sets Todo to not active
    Todo = self.env['todo.task']
    task = Todo.create({'name': 'Test Task'})
    task.do_clear_done()
    self.assertFalse(task.active)

建議盡可能為每個要檢查的操作編寫不同的測試用例。此測試用例以與前一個測試用例類似的方式開始:透過建立新的待辦事項任務。這是必需的,因為測試用例是獨立的,並且在一個測試用例期間建立或更改的資料在結束時返回。然後我們在建立的記錄上呼叫測試方法,然後檢查 Active? flag 設置為預期值。

如果我們現在執行測試並且正確編寫了模型方法,我們應該在服務器日誌中看不到任何錯誤消息。

測試存取安全性

事實上,由於缺少存取規則,我們的測試現在應該失敗。Admin 使用者才可以完成。在啟用了 Demo 資料的資料庫中,我們還提供了具有"典型"後端存取權限的演示使用者。我們應該更改測試,以便使用 Demo 使用者執行它們。

我們可以修改我們的測試以考慮到這一點。編輯 tests/test_todo.py 檔案以加入 setUp 方法:

def setUp(self, *args, **kwargs):
    result = super(TestTodo, self).setUp(*args, **kwargs)
    user_demo = self.env.ref('base.user_demo')
    self.env= self.env(user=user_demo)
    return result

第一條指令呼叫父類的 setUp 程式碼。下一個使用 Demo 使用者將用於執行測試的環境 self.env 更改為新的環境。我們已經編寫的測試不需要進一步的更改。

我們還應該加入一個測試用例,以確保使用者只能看到自己的任務。為此,首先在頂部加入一個額外的導入:

from odoo.exceptions import AccessError 

接下來,向測試類別加入一個額外的方法:

def test_record_rule(self):
    # Test per user record rules
    Todo = self.env['todo.task']
    task = Todo.sudo().create({'name': 'Admin Task'})
    with self.assertRaises(AccessError):
    Todo.browse([task.id]).name

由於我們的 env 方法現在使用 Demo 使用者,因此我們使用 sudo() 方法將上下文更改為 admin 使用者。然後,我們使用它來建立 Demo 使用者無法存取的任務。

當嘗試存取此任務資料時,我們期望引發 AccessError 異常。如果我們現在執行測試,它們應該失敗,所以讓我們來處理。

設定存取安全性

您可能已經注意到,在載入時,我們的模組在服務器日誌中收到一條警告消息:

模型 todo.task 沒有存取規則,請考慮加入一個。

消息非常明確:我們的新模型沒有存取規則,因此管理員超級使用者以外的任何人都無法使用它。作為超級使用者,管理員會忽略資料存取規則,這就是我們能夠無錯誤地使用表單的原因。

但我們必須在其他使用者可以使用我們的模型之前解決這個問題;我們尚未解決的另一個問題是我們希望 To-Do 任務對每個使用者都是私有的。 Odoo 支援 row-level 存取規則,我們可以使用它來實作它。

加入存取控制安全性

要了解向模型加入存取規則所需的訊息,請使用 Web 客戶端並轉到 Settings | Technical | Security | Access Controls List:

在這裡,我們可以看到某些模型的 ACL。它表示每個安全組對記錄允許的操作。

此訊息必須由模組使用資料檔案提供,以將 lines 載入到 ir.model.access 模型中。我們將在模型上加入對員工組的完全存取權限。

員工是幾乎每個人都屬於的基本存取組。例外是外部使用者,例如客戶或網站存取者。

這是使用名為 security/ir.model.access.csv 的 CSV 檔案完成的。讓我們加入以下內容:

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_todo_task_group_user,todo.task.user,model_todo_task,base.group_user,1,1,1,1

檔案名對應於要載入資料的模型,檔案的第一行包含行名。這些是我們的 CSV 檔案中提供的行:

  • id 是記錄外部標識符(也稱為XML ID)。它在我們的模組中應該是唯一的。
  • name 是一個描述標題。它只是提供訊息,如果它保持獨特是最好的。官方模組通常使用帶有模型名稱和組的點分隔字符串。遵循這個約定,我們使用了 todo.task.user。
  • model_id 是我們可以存取的模型的外部標識符。Models 具有由 ORM 自動產生的 XML ID; 對於 todo.task,標識符是 model_todo_task。
  • group_id 標識要授予權限的安全組。最重要的是由基本模組提供。Employee 組就是這種情況,並且具有 base.group_user 標識符。
  • perm fields 標記授予讀取,寫入,建立或取消鏈接(刪除)存取權限的存取權限。
    我們不要忘記在 __manifest__.py 描述符的資料屬性中加入對這個新檔案的引用。它應該如下所示:
'data': [
    'security/ir.model.access.csv',
    'views/todo_menu.xml',
    'views/todo_view.xml',
    'views/res_partner_view.xml',
],

和以前一樣,升級模組後,警告消息應該消失,我們可以透過使用者 demo 登錄來確認權限是否正常(密碼也是 demo)。

Row-level access rules

我們可以在 Technical 選單中找到 Record Rules 選項以及 Access Control List。

記錄規則在 ir.rule 模型中定義。像往常一樣,我們需要

    1. 一個與眾不同的名稱。
    1. 操作的模型。
    1. 用於存取限制的域過濾器。

域過濾器使用 Odoo 中使用的通常的元組語法列表。

通常,規則適用於某些特定安全組。在我們的案例中,我們將其應用於 Employees 組。如果它特別適用於沒有安全組,則將其視為全域(全域欄位自動設置為 True)。

全域規則是不同的,因為它們施加了非全域規則無法覆蓋的限制。

要添加記錄規則,我們應該建立一個 security/todo_access_rules.xml 檔案,其中包含以下內容:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <data noupdate="1">
        <record id="todo_task_user_rule" model="ir.rule">
            <field name="name">ToDo Tasks only for owner</field>
            <field name="model_id" ref="model_todo_task"/>
            <field name="domain_force">
                [('create_uid','=',user.id)]
            </field>
            <field name="groups" eval="[(4,ref('base.group_user'))]"/>
        </record>
    </data>
</odoo>

注意 noupdate=“1” 屬性。這意味著在模組升級中不會更新此資料。

這將允許稍後自定義,因為模組升級不會破壞使用者自己的更改。

但請注意,在開發過程中也會出現這種情況,因此您可能希望在開發期間設置 noupdate=“0”,直到您對資料檔案感到滿意為止。

在 groups 欄位中,您還會找到一個特殊的表達式。它是一對多的關係欄位,它們具有特殊的語法來操作。

在這種情況下,(4, x) 元組表示將 x 附加到記錄,此處 x 是對 Employees 組的引用,由 base.group_user 標識。

和以前一樣,我們必須先將檔案添加到 __manifest__.py,然後才能載入到模組中:

'data': [
    'security/ir.model.access.csv',
    'security/todo_access_rules.xml',
    'views/todo_menu.xml',
    'views/todo_view.xml',
    'views/res_partner_view.xml',
],

如果我們做的一切正確,我們可以執行模組測試,現在他們應該 Pass。

網頁與控制器

Odoo 還提供了一個 Web 開發框架,可用於開發與我們的後端應用程式緊密集成的網站功能。我們將透過建立一個簡單的網頁來顯示待處理的待辦事項列表。

透過 http://myserver/todo 的 URL 進行回應,因此 /todo 是我們要實作的 URL 端點。

  • Web 控制器是負責網頁呈現的組件。
  • Controller 是 http.Controller 類別中定義的方法,它們綁定到 URL 端點。

存取該 URL 端點時,執行控制器程式碼,產生要呈現給使用者的 HTML。為了幫助渲染 HTML,我們提供了 QWeb 模板引擎。

控制器的程式碼放在 /controllers 子目錄中。首先,編輯 todo_app/__init__.py,以便它導入控制器模組目錄:

from odoo import http
class Todo(http.Controller):

@http.route('/todo')
def Main(self, **kwargs):
    TodoTask = http.request.env['todo.task']
    domain_todo = [('is_done', '=', False)]
    tasks = TodoTask.search(domain_todo)
    return http.request.render('todo_app.index_template', {'tasks': tasks})

這裡導入的 odoo.http 模組是提供網路相關功能的核心組件。

http.Controller 物件是 Todo 應該衍生的類別控制器。我們將它用於主控制器類別。

我們為類別和他們的方法選擇的特定名稱是不相關的。

http.route 裝飾器是重要的部分,因為它宣告了綁定到類別方法 /todo 的 URL 端點。

預設情況下,存取 URL 端點需要使用者登錄。這可以更改,並且可以透過將 auth =‘public’ 參數添加到 http.route 來允許匿名存取。

在控制器方法內部,我們使用 http.request.env 存取環境。我們使用它來獲取包含所有未完成的待辦任務的記錄集。

最後一步是使用 http.request.render() 來處理 todo_app.index_template QWeb 模板並產生輸出 HTML。

我們可以透過字典為模板提供值。

如果我們現在嘗試這個,我們應該在服務器日誌中收到一條錯誤消息:

ValueError: External ID not found in the system: todo_app.index_template 

這是預期的,因為我們尚未定義該模板。這是我們的下一步。

QWeb 模板是一種視圖,也應該放在 /views 子目錄中。

讓我們建立 views/index_template.xml 檔案:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <template id="index_template" name="My Todo List">
    <div id="wrap" class="container">
        <h1>Todo Tasks</h1>
            <!-- List of Tasks -->
            <t t-foreach="tasks" t-as="task">
                <div class="row">
                    <span t-field="task.name" />
                </div>
            </t>
        </div>
    </template>
</odoo>

<template> 元素用於聲明 QWeb 模板。實際上,它是 ir.ui.view 記錄的快捷方式,這是儲存模板的基本模型。

該模板包含要使用的 HTML,並使用特定於 QWeb 的屬性。t-foreach 用於遍歷任務變量的每個項目,由控制器的 http.request.render() 呼叫提供。

t 欄位負責正確呈現記錄欄位的內容。這只是 QWeb 語法的一瞥。更多細節未來會提到。

我們仍然需要在模組清單中聲明此檔案,就像其他 XML 資料檔案一樣,以便它可以載入並可以使用。執行此操作並執行模組升級後,我們的網頁應該正常工作。

打開 http://<my-server>:8069/todo,您應該看到一個簡單的待辦事項列表。

網頁與控制器

Odoo 還提供了一個 Web 開發框架,可用於開發與我們的後端應用程式緊密集成的網站功能。我們將透過建立一個簡單的網頁來顯示待處理的待辦事項列表。

透過 http://myserver/todo 的 URL 進行回應,因此 /todo 是我們要實作的 URL 端點。

  • Web 控制器是負責網頁呈現的組件。
  • Controller 是 http.Controller 類別中定義的方法,它們綁定到 URL 端點。

存取該 URL 端點時,執行控制器程式碼,產生要呈現給使用者的 HTML。為了幫助渲染 HTML,我們提供了 QWeb 模板引擎。

控制器的程式碼放在 /controllers 子目錄中。首先,編輯 todo_app/__init__.py,以便它導入控制器模組目錄:

from odoo import http
class Todo(http.Controller):

@http.route('/todo')
def Main(self, **kwargs):
    TodoTask = http.request.env['todo.task']
    domain_todo = [('is_done', '=', False)]
    tasks = TodoTask.search(domain_todo)
    return http.request.render('todo_app.index_template', {'tasks': tasks})

這裡導入的 odoo.http 模組是提供網路相關功能的核心組件。

http.Controller 物件是 Todo 應該衍生的類別控制器。我們將它用於主控制器類別。

我們為類別和他們的方法選擇的特定名稱是不相關的。

http.route 裝飾器是重要的部分,因為它宣告了綁定到類別方法 /todo 的 URL 端點。

預設情況下,存取 URL 端點需要使用者登錄。這可以更改,並且可以透過將 auth =‘public’ 參數添加到 http.route 來允許匿名存取。

在控制器方法內部,我們使用 http.request.env 存取環境。我們使用它來獲取包含所有未完成的待辦任務的記錄集。

最後一步是使用 http.request.render() 來處理 todo_app.index_template QWeb 模板並產生輸出 HTML。

我們可以透過字典為模板提供值。

如果我們現在嘗試這個,我們應該在服務器日誌中收到一條錯誤消息:

ValueError: External ID not found in the system: todo_app.index_template 

這是預期的,因為我們尚未定義該模板。這是我們的下一步。

QWeb 模板是一種視圖,也應該放在 /views 子目錄中。

讓我們建立 views/index_template.xml 檔案:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <template id="index_template" name="My Todo List">
    <div id="wrap" class="container">
        <h1>Todo Tasks</h1>
            <!-- List of Tasks -->
            <t t-foreach="tasks" t-as="task">
                <div class="row">
                    <span t-field="task.name" />
                </div>
            </t>
        </div>
    </template>
</odoo>

<template> 元素用於聲明 QWeb 模板。實際上,它是 ir.ui.view 記錄的快捷方式,這是儲存模板的基本模型。

該模板包含要使用的 HTML,並使用特定於 QWeb 的屬性。t-foreach 用於遍歷任務變量的每個項目,由控制器的 http.request.render() 呼叫提供。

t 欄位負責正確呈現記錄欄位的內容。這只是 QWeb 語法的一瞥。更多細節未來會提到。

我們仍然需要在模組清單中聲明此檔案,就像其他 XML 資料檔案一樣,以便它可以載入並可以使用。執行此操作並執行模組升級後,我們的網頁應該正常工作。

打開 http://<my-server>:8069/todo,您應該看到一個簡單的待辦事項列表。

Summary

這裡建立了一個 To-Do 新模組,涵蓋了模組中最常用的元素:模型,視圖的三種基本類型(表單,列表和搜索),模型方法中的商務邏輯以及存取安全性。

除此之外還了解了安全存取控制,包括記錄規則,以及如何使用 Web 控制器和 QWeb 模板建立網頁。


參考資料

  1. Daniel Reis. Odoo 11 Development Essentials. Third Edition.

【odoo產業應用】如何使用odoo的付款條款管理應收付款項
金流管理不再是難事~