Odoo 13開發之外部 API – 集成第三方系統
Odoo 服務器端帶有外部 API,可供網頁客戶端和其它客戶端應用使用。本文中我們將學習如何在我們的客戶端程序中使用 Odoo 的外部 API。為避免引入大家所不熟悉的編程語言,此處我們將使用基於 Python 的客戶端,但這種 RPC 調用的處理方法也適用於其它編程語言。
我們將一起了解如何使用 Odoo RPC調用,然后根據所學知識使用 Python創建一個簡單的圖書命令行應用。
本文主要內容有:
- 在客戶端機器上安裝 Python
- 使用XML-RPC連接 Odoo
- 使用XML-RPC運行服務器端方法
- 搜索和讀取 API 方法
- 圖書客戶端XML-RPC 接口
- 圖書客戶端用戶界面
- 使用OdooRPC庫
- 了解ERPpeek客戶端
開發准備
本文基於第三章 Odoo 13 開發之創建第一個 Odoo 應用創建的代碼,具體代碼請參見 GitHub 倉庫。應將library_app模塊放在addons路徑下並進行安裝。為保持前后一致,我們將使用第二章 Odoo 13開發之開發環境准備所進行安裝並使用dev13 數據庫。本章完成后的代碼請參見 GitHub 倉庫。
學習項目-圖書目錄客戶端
本文中,我們將開發一個簡單的客戶端應用來管理圖書目錄。這是一個命令行接口(CLI) 應用,使用 Odoo 來作為后端。應用的功能非常有限,這樣我們可以聚焦在用於與 Odoo服務端交互的技術,而不是具體應用的實現細節。我們的簡單應用可以完成如下功能:
- 通過標題搜索並列出圖書
- 向目錄添加新標題
- 修正圖書標題
- 從目錄中刪除圖書
這個應用是一個 Python 腳本,等待輸入命令來執行操作。使用會話示例如下:
$ python3 library.py add "Moby-Dick" $ python3 library.py list "moby" 60 Moby-Dick $ python3 library.py set-title 60 "Moby Dick" $ python3 library.py del 60
在客戶端機器上安裝 Python
Odoo API 可以在外部通過兩種協議訪問:XML-RPC和JSON-RPC。任意外部程序,只要能實施其中一種協議的客戶端,就可以與 Odoo 服務端進行交互。為避免引入其它編程語言,我們將保持使用 Python 來研究外部 API。
到目前為止我們僅在服務端運行了 Python 代碼。現在我們要在客戶端上使用 Python,所以你可能需要在電腦上做一些額外設置。要學習本文的示例,你需要能在操作電腦上運行 Python 文件。可通過在命令行終端運行python3 –version命令來進行確認。如果沒有安裝,請參考官方網站針對你所使用的平台的安裝包。
對於 Ubuntu,你可能已經安裝了 Python 3,如果沒有安裝,可通過以下命令進行安裝:
sudo apt-get install python3 python3-pip
如果你使用的是 Windows 並且已安裝了 Odoo,可能會奇怪為什么沒有 Python 解釋器,還需要進行額外安裝。簡單地說是因為 Odoo 安裝帶有一個內嵌的 Python 解釋器,無法在外部使用。
使用XML-RPC連接 Odoo API
訪問服務的最簡單方法是使用XML-RPC,我們可以使用 Python 標准庫中的xmlrpclib來實現。不要忘記我們是在編寫客戶端程序連接服務端,因此需運行 Odoo 服務端實例來供連接。本例中我們假設 Odoo 服務端實例在同一台機器上運行,但你可以使用任意運行服務的其它機器,只要能連接其IP 地址或服務器名。
讓我們來初次連接 Odoo 外部 API。打開 Python 3終端並輸入如下代碼:
>>> from xmlrpc import client >>> srv = 'http://localhost:8069' >>> common = client.ServerProxy('%s/xmlrpc/2/common' % srv) >>> common.version() {'server_version': '13.0', 'server_version_info': [13, 0, 0, 'final', 0, ''], 'server_serie': '13.0', 'protocol_version': 1}
這里我們導入了xmlrpc.client庫,然后創建了一個包含服務地址和監聽端口信息的變量。請根據自身狀況進行修改。
下一步訪問服務端公共服務(無需登錄),在終端地址/xmlrpc/2/common上暴露。其中一個可用方法是version(),用於查看服務端版本。我們使用它來確認可與服務端進行通訊。
另一個公共方法是authenticate()。你可能你會以為它會創建會話,但實際上不會。該方法僅僅確認用戶名和密碼可被接受,請求不使用用戶名而是它返回的用戶 ID。示例如下:
>>> db = 'dev13' >>> user, pwd = 'admin', 'admin' >>> uid = common.authenticate(db, user, pwd, {}) >>> print(uid)
首先創建變量 db,來存儲使用的數據庫名。本例中為 dev13,但可以修改為任意其它你所使用的數據庫名。如果登錄信息錯誤,將不會返回用戶 ID,而是返回 False 值。authenticate()最后一個參數是用戶代理(User Agent)環境,用於提供客戶端的一些元數據(metadata),它是必填的,但可以是一個空字典。
使用XML-RPC運行服務器端方法
使用XML-RPC,不會維護任何會話,每次請求都會發送驗證信息。這讓協議過重,但使用簡單。下一步我們設置訪問需登錄才能訪問的服務端方法。暴露的終端地址為/xmlrpc/2/object,示例如下:
>>> api = client.ServerProxy('%s/xmlrpc/2/object' % srv) >>> api.execute_kw(db, uid, pwd, 'res.partner', 'search_count', [[]]) 48
此處我們第一次訪問了服務端 API,執行了Partner 記錄的計數。通過 execute_kw() 方法來調用方法,接收如下參數:
- 連接的數據庫名
- 連接用戶ID
- 用戶密碼
- 目標模型標識符名稱
- 調用的方法
- 位置參數列表
- 可選的關鍵字參數字典(本例中未使用)
上面的例子中對res.partner模型調用了search_count方法,僅一個位置參數[],沒有關鍵字參數。該位置參數是一個搜索域,因我們傳入的是一個空列表,它對所有伙伴進行計數。常用的操作有搜索和讀取。在使用RPC調用時,search方法返回一個區塊的 ID 列表。browse方法不可用於RPC,而應使用read來得到記錄 ID 列表並獲取相應數據,示例如下:
>>> domain = [('is_company', '=', True)] >>> api.execute_kw(db, uid, pwd, 'res.partner', 'search', [domain]) [14, 10, 11, 15, 13, 13, 9, 1] >>> api.execute_kw(db, uid, pwd, 'res.partner', 'read', [[14]], {'fields': ['id', 'name', 'country_id']}) [{'id': 14, 'name': 'Azure Interior', 'country_id': [233, 'United States']}]
對於 read 方法,我們使用了一個位置參數[14]
來作為 ID 列表,以及一個關鍵字參數fields。還可以看到many-to-one關聯字段如country_id,被成對獲取,包含關聯的記錄 ID 和顯示名稱。在代碼中處理數據時應記住這一點。
經常會使用search和 read 的組合,所以提供了一個search_read方法來在同一步中執行兩者的操作。可通過如下命令來獲取以上兩段代碼的同樣結果:
api.execute_kw(db, uid, pwd, 'res.partner', 'search_read', [domain], {'fields': ['id', 'name', 'country_id']})
補充:以上代碼會為 read 方法傳入所有 search 方法的結果,因此內容要較僅傳入[14]
多
search_read方法和 read 相似,但需要 domain代替 id 列表來作為第一個位置參數。需要說明在 read 和search_read中fields參數並非必須。如果不傳入,則獲取所有字段。這可能會帶來對函數字段的大量計算,並且獲取大量可能從來都不會用到的數據,所以通常建議明確傳入字段列表。
搜索和讀取 API 方法
在第七章 Odoo 13開發之記錄集 – 使用模型數據我們學習了用於生成記錄的最重要的模型方法以及代碼書寫。但還有一些其它模型方法可用於更具體的操作,如:
read([fields])
類似於browse方法,但返回的不是記錄集,而是包含按參數指定的字段的各行數據列表。每一行是一個字典。它提供可供 RPC 協議使用的數據的序列化展示,設計用於客戶端程序中而非服務端邏輯中。 search_read([domain], [fields], offset=0, limit=None, order=None)
在讀取結果記錄列表之后執行搜索操作。設計用於 RPC 客戶端,避免了反復進行讀取結果和搜索的操作。 所有其它模型方法都對 RPC 暴露,但以下划線開頭的除外,這些是私有方法。也就是說我們可以像下面這樣使用create, write,和unlink修改服務端數據:
>>> x = api.execute_kw(db, uid, pwd, 'res.partner', 'create', [{'name': 'Packt Pub'}]) >>> print(x) 69 >>> api.execute_kw(db, uid, pwd, 'res.partner', 'write', [[x], {'name': 'Packt Publishing'}]) True >>> api.execute_kw(db, uid, pwd, 'res.partner', 'read', [[x], ['id', 'name']]) [{'id': 69, 'name': 'Packt Publishing'}] >>> api.execute_kw(db, uid, pwd, 'res.partner', 'unlink', [[x]]) True >>> api.execute_kw(db, uid, pwd, 'res.partner', 'read', [[x], ['id', 'name']]) []
XML-RPC的一個缺陷是它不支持 None 值。有一個XML-RPC擴展可以支持 None 值,但這取決於我們客戶端所依賴的具體XML-RPC庫。不返回任何值的方法不能在XML-RPC中使用,因為默認返回的是 None。這也是為什么方法在結束時至少會帶有一個return True語句。另一個方案是使用 Odoo 同時支持的JSON-RPC。OdooRPC對其進行運行,在稍后的使用OdooRPC庫一節會進行使用。
應反復強調 Odoo 的外部 API 可在大部分編程語言中使用。官方文檔中我們可以看到Ruby, PHP和Java實際示例。
ℹ️以下划線開頭的模塊方法被認為是私有方法,不對XML-RPC暴露。
圖書客戶端XML-RPC 接口
下面就來實現圖書客戶端應用。我們將使用兩個文件:一個處理服務端的接口:library_api.py,另一個處理應用的用戶界面:library.py。然后我們會使用現有的OdooRPC庫來提供一個替代的實現方法。
我們將創建類來配置與 Odoo 服務端的連接,以及讀取/寫入圖書數據。這將暴露基本的增刪改查方法:
- search_read()獲取圖書數據
- create()創建圖書
- write()更新圖書
- unlink()刪除圖書
選擇一個目錄來放置應用文件並創建library_api.py文件。首先添加類的構造方法,代碼如下:
from xmlrpc import client
class LibraryAPI(): def __init__(self, srv, port, db, user, pwd): common = client.ServerProxy( 'http://%s:%d/xmlrpc/2/common' % (srv, port)) self.api = client.ServerProxy( 'http://%s:%d/xmlrpc/2/object' % (srv, port)) self.uid = common.authenticate(db, user, pwd, {}) self.pwd = pwd self.db = db self.model = 'library.book'
此處我們存儲了所有創建執行模型調用的對象的所有信息:API引用、uid、密碼、數據庫名和要使用的模型。接下來我們定義一個幫助方法來執行調用。有賴於前面對象存儲的數據該方法可以很精煉:
def execute(self, method, arg_list, kwarg_dict=None): return self.api.execute_kw( self.db, self.uid, self.pwd, self.model, method, arg_list, kwarg_dict or {})
現在就可以使用它來實現更高級的方法了。search_read()方法接收一個可選的 ID 列表來獲取數據。如果沒傳入數據,則返回所有記錄:
def search_read(self, text=None): domain = [('name', 'ilike', text)] if text else [] fields = ['id', 'name'] return self.execute('search_read', [domain, fields])
create()方法用於創建給定書名的新書並返回所創建記錄的 ID:
def create(self, title): vals = {'name': title} return self.execute('create', [vals])
write()方法中傳入新書名和圖書 ID 作為參數,並對該書執行寫操作:
def write(self, title, id): vals = {'name': title} return self.execute('write', [[id], vals])
然后我們可以實現unlink()方法,非常簡單:
def unlink(self, id):
return self.execute('unlink', [[id]])
在該Python文件最后添加一段測試代碼在運行時執行:
if __name__ == '__main__': # 測試配置 srv, db, port = 'localhost', 'dev13', 8069 user, pwd = 'admin', 'admin' api = LibraryAPI(srv, port, db, user, pwd) from pprint import pprint pprint(api.search_read())
如果執行以上 Python 腳本,我們可以打印出圖書的內容:
$ python3 library_api.py
[{'id': 56, 'name': 'Brave New World'}, {'id': 40, 'name': 'Hands-On System Programming with Linux'}, {'id': 41, 'name': 'Lord of the Flies'}, {'id': 39, 'name': 'Mastering Docker - Third Edition'}, {'id': 38, 'name': 'Mastering Reverse Engineering'}, {'id': 55, 'name': 'Odoo 11 Development Cookbook'}, {'id': 54, 'name': 'Odoo Development Essentials 11'}]
現在已經有了對 Odoo 后端的簡單封裝,下面就可以處理命令行用戶界面了。
圖書客戶端用戶界面
我的目標是學習如何寫外部應用和 Odoo 服務之間的接口,前面已經實現了。但不能止步於此,我們還應讓終端用戶可以使用它。為使設置盡量簡單,我們將使用 Python 內置功能來實現這個命令行應用。該功能是標准庫自帶的,因此不需要進行額外的安裝。
在library_api.py 同目錄,新建一個library.py文件。首先導入命令行參數解析器,然后導入LibraryAPI類,代碼如下:
from argparse import ArgumentParser from library_api import LibraryAPI
下面我們來看看參數解析器接收的命令,有以下四個命令:
- 搜索並列出圖書
- 添加圖書
- 設置(修改)書名
- 刪除圖書
在命令行解析器中添加這些命令的代碼如下:
parser = ArgumentParser()
parser.add_argument( 'command', choices=['list', 'add', 'set-title', 'del']) parser.add_argument('params', nargs='*') # 可選參數 args = parser.parse_args()
這里 args 是一個包含傳入腳本參數的對象,args.command是提供的命令,args.params是可選項,用於存放命令所需的其它參數。如果使用了不存在或錯誤的命令,參數解析器會進行處理並提示用戶應輸入的內容。有關argparse更完整的說明,請參考官方文檔。
下一步是執行所計划的操作。首先為 Odoo服務准備連接:
srv, port, db = 'localhost', 8069, 'dev13' user, pwd = 'admin', 'admin' api = LibraryAPI(srv, port, db, user, pwd)
第一行代碼設置服務實例的一些固定參數以及要連接的數據庫。本例中,我們連接 Odoo 服務本機(localhost),監聽8069默認端口,並使用 dev13數據庫。如需連接其它服務器和數據庫,請對參數進行相應調整。
這里硬編碼了服務器地址並且密碼使用了明文,顯然與最佳實施相差甚遠。我們應該包含配置步驟讓客戶提供相關設置信息,並以安全的方式進行存儲。但此處我們的目標是學習使用 Odoo RPC,所以可把它看作概念代碼,而非已完成的產品。下面寫代碼來利用 api 對象處理所支持的命令。我們可以先寫list命令來列出圖書:
if args.command == 'list': text = args.params[0] if args.params else None books = api.search_read(text) for book in books: print('%(id)d %(name)s' % book)
這里我們使用了LibraryAPI.search_read()來從服務端獲取圖書記錄列表。然后遍歷列表中每個元素並打印。我們使用 Python 字符串格式化來向用戶顯示每條圖書記錄,記錄是一個鍵值對字典。下面添加add命令,這里使用了額外的書名作為參數:
if args.command == 'add': for title in args.params: new_id = api.create(title) print('Book added with ID %d.' % new_id)
因為主要的工作已經在LibraryAPI對象中完成,下面我們只要調用write()方法並向終端用戶顯示結果即可。 set-title命令允許我們修改已有圖書的書名,應傳入兩個參數,新的書名和圖書的 ID:
if args.command == 'set-title': if len(args.params) != 2: print("set command requires a title and ID.") else: book_id, title = int(args.params[0]), args.params[1] api.write(title, book_id) print('Title set for Book ID %d.' % book_id)
最終我們要實現 del 命令來刪除圖書記錄,學到這里應該不再有任何挑戰性了:
if args.command == 'del': for param in args.params: api.unlink(int(param)) print('Book with ID %s deleted.' % param)
到這里我們就完成了基礎的 API CLI (命令行接口)了,讀者可以嘗試執行命令來查看效果。比如,我們可以運行本文開頭學習項目-圖書目錄客戶端中的命令。通過普通客戶端來訪問圖書中的數據也會有助於確認該CLI是否如預想般運行。這是一個非常基礎的應用,查看代碼你應該可以想到一些改進它的方式。但要記住我們這里的目的是以相對有趣的方式舉例說明Odoo RPC API的使用。
使用OdooRPC庫
另一個可以考慮的客戶端庫是OdooRPC。它是一個更流行的客戶端庫,使用JSON-RPC 協議而不是XML-RPC。事實上 Odoo 官方客戶端使用的就是JSON-RPC,XML-RPC更多是用於支持向后兼容性。
ℹ️OdooRPC庫現在由 OCA 管理和持續維護。了解更多請參見OCA。
OdooRPC庫可通過PyPI安裝:
pip3 install --user odoorpc
不管是使用JSON-RPC還是XML-RPC,Odoo API的使用方式並沒什么分別。所以在下面我們可以看一些細節可能有區別,但這些客戶端庫的使用方式並沒有什么分別。
OdooRPC庫在創建新的odoorpc.ODOO對象時建立服務端連接,然后應使用ODOO.login()方法來創建用戶會話。和服務端相似,會話有一個帶有會話環境的 env 屬性,包含用戶 ID-uid 和上下文。我們可以使用OdooRPC來重新實現library_api.py對服務端的接口。它應提供相同的功能,但使用JSON-RPC代替XML-RPC來實施。在相同目錄下創建library_odoorpc.py文件並加入如下代碼:
from odoorpc import ODOO class LibraryAPI(): def __init__(self, srv, port, db, user, pwd): self.api = ODOO(srv, port=port) self.api.login(db, user, pwd) self.uid = self.api.env.uid self.model = 'library.book' self.Model = self.api.env[self.model] def execute(self, method, arg_list, kwarg_dict=None): return self.api.execute( self.model, method, *arg_list, **kwarg_dict)
OdooRPC庫實現Model和Recordset對象來模仿服務端對應的功能。目標是在客戶端編程與服務端編程應基本一致。客戶端使用的方法將通過存儲在self.Model屬性中的圖書模型來利用這點。這里實現的execute()方法並不會在我們客戶端中使用,僅用於與本文中討論的其它實現進行對比。
下面我們來實現search_read(), create(), write()和unlink()這些客戶端方法。在相同文件的LibraryAPI()類中添加如下方法:
def search_read(self, text=None): domain = [('name', 'ilike', text)] if text else [] fields = ['id', 'name'] return self.Model.search_read(domain, fields) def create(self, title): vals = {'name': title} return self.Model.create(vals) def write(self, title, id): vals = {'name': title} self.Model.write(id, vals) def unlink(self, id): return self.Model.unlink(id)
注意這段代碼和 Odoo 服務端代碼相似,因為它使用了與 Odoo 中插件寫法相近的 API。然后可以將library.py文件中的from library_api import LibraryAPI一行修改為library_odoorpc import LibraryAPI。現在再次運行library.py客戶端應用進行測試,執行的效果和之前應該一致。
了解ERPpeek客戶端
ERPpeek是一個多功能工具,既可以作為交互式命令行接口(CLI)也可以作為 Python庫,它提供了比xmlrpc庫更便捷的 API。它在PyPi索引中,可通過如下命令安裝:
pip3 install --user erppeek
ERPpeek不僅可用作 Python 庫,它還可作為 CLI 來在服務器上執行管理操作。Odoo shell 命令在主機上提供了一個本地交互式會話功能,而erppeek庫則為網絡上的客戶端提供了一個遠程交互式會話。打開命令行,通過以下命令可查看能夠使用的選項:
erppeek --help
下面一起來看看一個示例會話:
$ erppeek --server='http://192.168.16.161:8069' -d dev13 -uadmin Usage (some commands): models(name) # List models matching pattern model(name) # Return a Model instance ... Password for 'admin': Logged in as 'admin' dev13 >>> model('res.users').count() 3 dev13 >>> rec = model('res.partner').browse(14) dev13 >>> rec.name 'Azure Interior'
如上所見,建立了服務端的連接,執行上下文引用了model() 方法來獲得模型實例並對其進行操作。連接使用的erppeek.Client實例也可通過客戶端變量來使用。 值得一提的是它可替代網頁客戶端來管理所安裝的插件模塊:
client.modules()列出可用或已安裝模塊 client.install()執行模塊安裝 client.upgrade()執行模塊升級 client.uninstall()卸載模塊 因此ERPpeek可作為 Odoo 服務端遠程管理的很好的服務。有關ERPpeek的更多細節請見 GitHub。
總結
本文的目標是學習外部 API 如何運作以及它們能做些什么。一開始我們通過一個簡單的Python XML-RPC客戶端來進行探討,但外部 API 可用於其它編程語言。事實上官方文檔中包含了Java, PHP和Ruby的代碼示例。
有很多庫可處理XML-RPC或JSON-RPC,有些是通用的,有些僅適用於 Odoo。我們使用了一個指定庫OdooRPC。
以上我們就完結了本文有關編程 API 和業務邏輯的學習。是時候深入視圖和用戶界面了。在下一篇文章中,我們進一步學習網頁客戶端所提供的后台視圖和用戶體驗。
☞☞☞第十章 Odoo 13開發之后台視圖 – 設計用戶界面