Table of Contents
命令入口
odoo-bin 腳本
支持的子命令
help
deploy
scaffold
server
shell
start
子命令注冊
command 基類, 往 commands 注冊 支持的子命令, 等定義 接口 run()…. 每個子命令必須實現它
服務器
運行 子命令 server ,
檢查運行用戶、pg用戶、讀取配置、顯示主要配置
啟動server
加載 全局 addons… load_server_wide_modules() #L864 service\server.py
此時 initialize_sys_path() , 並 import odoo模塊, 名稱空間為 'odoo.addons.'+module_name
# 默認全局加載的模塊為 web
根據 配置 運行 具體模式server 的 run(preload, stop) 方法 ; 支持以下集中模式
- 多線程 --> ThreadedServer
- 多進程 --> PreforkServer
- gevent --> GeventServer
運行 server 時, 根據配置 確定server ,然后將 wsgi應用 傳遞給 它, 也就是將 self.app 指定為 wsgi 應用 odoo.service.wsgi_server.application
, 以便在啟動http server后,以此wsgi 應用來響應請求
thread 模式
對於 Thread 模式, 通過 run 入口 調用 http_spawn() 以線程模式啟動 wsgi服務器
以 werkzeug.serving.ThreadedWSGIServer 模式 啟動 wsgi application
prefork 模式
對於 Prefork 模式, 通過 run 入口 調用 worker_spawn() 依次調用對應的 worker,
http worker --> WorkerHTTP
cron worker --> WorkerCron
long_polling 進程
在 worker_spawn 初始化 對應的 worker 后 調用 run() 啟動它
對於 http worker ,,, 則 調用WorkerHTTP,self.workers_http), 啟動 BaseWSGIServerNoBind(self.multi.app)
使用 werkzeug 啟動 wsgi 服務器werkzeug.serving.BaseWSGIServer.__init__(self,"127.0.0.1",0,app)
gevent模式
對於 gevent 模式, 以 gevent.wsgi 方式啟動 服務器
說明: gevent模式專用於 longpooling
wsgi 應用
封裝了2個 wsgi 處理器
- xmlrpc
- http root # 也就是在 http.py 里面定義的 class Root
響應客戶端請求
當對 http server發起請求時, http server將請求轉發給 wsgi應用
當wsgi 接收到 請求時, 嘗試調用所有支持的 wsgi 處理器[handler]對請求進行處理,
調用的先后順序為
wsgi_xmlrpc(environ,start_response)
odoo.http.root(environ,start_response)
如果handler處理不了,則返回 "No handler found." 錯誤
xmlrpc
調用 xmlrpc 處理器 #L102 wsgi_xmlrpc wsgi_server.py
驗證xmlrpc 調用請求,取出 xmlrpc 調用的服務,方法,以及參數, 傳遞給 odoo.http.dispatch_rpc(service,method,params),返回 最終 dispath() 方法
各個服務將調用相應的模塊的dispatch() 方法來 分發 遠程調用
- common --> odoo.service.common
- db --> odoo.service.db
- object --> odoo.service.model
每個服務都實現了 dispatch 方法
web
odoo web 將 http請求分為2種
- jsonrequest
- httprequest
它們都繼承webreqeust
http.root # odoo/http.py#L1294
http Root() 初始初始化,並 將請求通過 dispatch() 進行分發
先判斷是否 第一次加載 addons, 如是, 則 加載 模塊 ,並使用 靜態文件 處理器的 dispatch 方法 分發請求
如果不是 第一次加載 addons, 則調用 root 的 dispatch() 方法 分發請求
加載 模塊,目的是 加載所有包含了 靜態文件 和 controller的 odoo addons
對於靜態文件的addons, 則將 使用 disablecachemiddleware 對 wsgi 應用 進行 處理
對於其他的,則使用 root 的 dispatch 方法, 根據 請求的不同情形 進行分發
可能的情形
- http請求不包含 db 時, 分發到創建數據庫
-
http請求包含 db 時, 檢查注冊表信號【如果注冊表還沒有 ready, 則先 准備好注冊表】
- 如果在檢查注冊表信號,或者調取ir_http 出現異常, 則 分發到創建數據庫 或則 選擇數據庫
- 然后通過注冊表獲取 ir_http 對象,將請求交給 ir_http dispatch 進行路由選擇, 然后交給相應的 http Endpoint 進行處理
同時,在做實際的dispatch() 之前,先對 web request 進行識別, 判斷到底是 jsonrequest 還是 httpreqeust
根據請求數據 判別 request 類型, 然后用 對應的 request 方式進行數據處理
http路由處理
ir_http dispatch 通過 _find_handler() 調用 ir.http 類方法 routing_map()獲取 路由表 # L227 ir_http.py
根據已經 安裝的 模塊, 經由 http.routing_map() 得到 route map.
例如,
根據 route_map 選擇 對應的 endpoint 處理 web 請求
調用 web 請求的 dispatch() 方法 對請求進行處理, 而相應的 request 最終會 調用
父類 Request 方法 _call_function() 調用 endpoint 處理 request…
HTTP request
如果是 http 類型, 調用 HttpRequest.dispatch() 處理
使用 對應的 endpoint 處理 reqeust.. 並返回結果
JSON request
如果是 http 類型, 調用 JsonRequest.dispatch() 處理
對應的endpoint 處理 請求, 返回結果 經 _json_response 處理為 jsonrpc 返回數據規范
路由注冊
在加載odoo addons的時候,如果是controller 會往 controllers_per_module{} 注冊 控制器類, 注冊內容是
{ 模塊:[ (模塊名.類名, 類)] }
例如
生成路由表時, 從 注冊表讀出已安裝的模塊, 然后從上面數據讀出控制器類,並讀出 方法的 routing 屬性
routing屬性,是在往 控制器方法修飾 route 時, 注入進去的
注冊表
registry , 每個數據庫 一個 注冊表, 在 每個 odoo實例 的 registry 對象 的 registries 屬性記錄 全部的 注冊表
主要 方法
load() |
加載 模型, 構建 model class |
setup_models() |
設置 base , 設置 字段, 設置 計算字段 |
init_models() |
初始模型,調用 model 的 auto_init() 和 init( ) 操作數據庫, 建立 數據庫表 , 增加字段 字段 , 增加 約束 /// 在此 實現 MPTT 【 預排序遍歷樹 】 // 提示, 可以在model 定制 init() 改變 數據庫初始化邏輯 |
注冊表創建或更新
wsgi 應用 Dispatch 請求時, dispatch 邏輯里,在檢查注冊表時, 先嘗試 獲取 注冊表, 然后檢查 "信號" # odoo/odoo/http.py:1445
注冊表獲取 # odoo/odoo/__init__.py:76
根據db 創建 注冊表, 加載 模塊 # odoo/odoo/modules/registry.py:61
加載 模塊 # odoo/odoo/modules/registry.py:85
模塊
模塊信息
load_information_from_description_file()
遷移鈎子 migration hook
在 安裝/升級 模塊時, 執行 migrations
Migrations 定義:
This class manage the migration of modules
Migrations files must be python files containing a `migrate(cr, installed_version)`
function. Theses files must respect a directory tree structure: A 'migrations' folder
which containt a folder by version. Version can be 'module' version or 'server.module'
version (in this case, the files will only be processed by this version of the server).
Python file names must start by `pre` or `post` and will be executed, respectively,
before and after the module initialisation. `end` scripts are run after all modules have
been updated.
Example:
<moduledir>
`-- migrations === 目錄名必須
|-- 1.0 === 版本號, odoo服務版本號,或者模塊版本號
| |-- pre-update_table_x.py === 升級前執行腳本
| |-- pre-update_table_y.py
| |-- post-create_plop_records.py === 升級后執行腳本
| |-- end-cleanup.py === 最終執行腳本
| `-- README.txt # not processed
|-- 9.0.1.1 # processed only on a 9.0 server
| |-- pre-delete_table_z.py
| `-- post-clean-data.py
`-- foo.py # not processed
當 遷移腳本的 版本 處於 已安裝的版本, 和當前版本直接時, 才 會執行
if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
模塊依賴關系圖
模塊安裝/升級/卸載
代碼 odoo/odoo/modules/loading.py
背景知識點: python 環境 sys.modules
- 調用 initialize_sys_path() 引入 odoo 模塊, 加入到 sys.modules
- 如果數據庫還沒建立,初始化數據庫, 對應的SQL 文件 odoo/odoo/addons/base/base.sql
- 獲取 注冊表
- 初始化 模塊依賴關系圖
-
按 模塊依賴關系圖, 運行以下邏輯 load_module_graph()
- 運行預遷移腳本
- 加載 odoo模塊,如果 模塊指定了 post_load 運行它 # load_openerp_module()
- 對於新安裝模塊, 運行模塊指定的 pre_init_hook
- 往注冊表加載 模塊
- 對於新安裝/升級的模塊, 通過注冊表 設置模型 setup_models(), 初始化模型 init_models()
- 對於新安裝/升級的模塊, 加載 數據 以及 演示數據
- 運行遷移后腳本
- 如果在config 設置了overwrite_existing_translations,則更新翻譯,
- 驗證 視圖
- 對於新安裝模塊, 運行模塊指定的 post_init_hook
- 計算 依賴模塊, 再次 按 模塊依賴關系圖 運行 模塊安裝/升級 邏輯
- 運行最終遷移腳本
- 完成安裝並清理
- 如果是 卸載模塊, 執行 卸載,並重置 注冊表 // 卸載時,從數據庫表 ir_model_data 刪除相關數據, 將模塊標記為 uninstalled
- 驗證 自定義視圖
- 運行 模型注冊鈎子 _register_hook()
load_openerp_module 處理 odoo 名稱 空間
odoo.addons.[addons_name].models.[model_name]
別名
openerp.addons. *
# 注意
此外,在引入 odoo模塊的時候,通過 MetaModel 將 addons 登記 module_to_models, 以便 注冊表 在 load 模型時, 構建 model.
模塊登記
在 模塊加載 邏輯的 第二步, 更新 數據庫表 ir_module_module 往里面 登記 需要 加載的模塊
模型初始化
往注冊表 加載 模塊時, 調用 model 的 build_model()方法 建立 模型 # L233 load() registry.py
def load(self, cr, module):
""" Load a given module in the registry, and return the names of the
modified models.
At the Python level, the modules are already loaded, but not yet on a
per-registry level. This method populates a registry with the given
modules, i.e. it instanciates all the classes of a the given module
and registers them in the registry.
"""
from .. import models
lazy_property.reset_all(self)
# Instantiate registered classes (via the MetaModel automatic discovery
# or via explicit constructor call), and add them to the pool.
model_names = []
for cls in models.MetaModel.module_to_models.get(module.name, []):
# models register themselves in self.models
model = cls._build_model(self, cr)
model_names.append(model._name)
return self.descendants(model_names, '_inherit', '_inherits')
根據 addons depends 以及 _inherit 來決定 model 繼承
例如 , 通過 type(env['res.users']).mro() 查看 繼承 順序, 調用 supper() 時, 調用父級的先后順序
env['res.users'] 為 空記錄集
type(env['res.users']) 得到 記錄集對應的模型, 類型 [ class ]
例如, res.users 模型
模塊數據加載
在 模塊 加載時, 通過 _load_data() 調用 tools.convert_file() 將 data file 導入到 db
tools.convert_file(cr, module_name, filename, idref, mode, noupdate, kind, report)
convert_file()
def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None, pathname=None):
if pathname is None:
pathname = os.path.join(module, filename)
ext = os.path.splitext(filename)[1].lower()
with file_open(pathname, 'rb') as fp:
if ext == '.csv':
convert_csv_import(cr, module, pathname, fp.read(), idref, mode, noupdate)
elif ext == '.sql':
convert_sql_import(cr, fp)
elif ext == '.yml':
convert_yaml_import(cr, module, fp, kind, idref, mode, noupdate, report)
elif ext == '.xml':
convert_xml_import(cr, module, fp, idref, mode, noupdate, report)
elif ext == '.js':
pass # .js files are valid but ignored here.
else:
raise ValueError("Can't load unknown file type %s.", filename)
記錄集
實例化一個 model..
records=object.__new__(cls)
然后 給它的 屬性 _ids 賦值
這樣, 就可以 通過 __getitem__() 獲取 字段數據, __setitem__() 設置 字段數據
記錄集 操作
Recordsets are immutable, but sets of the same model can be combined using various set operations, returning new recordsets. Set operations do not preserve order.
- record in set returns whether record (which must be a 1-element recordset) is present in set. record not in set is the inverse operation # __contains__()
- set1 <= set2 and set1 < set2 return whether set1 is a subset of set2 (resp. strict) # __le__()
- set1 >= set2 and set1 > set2 return whether set1 is a superset of set2 (resp. strict)# __ge__()
- set1 | set2 returns the union of the two recordsets, a new recordset containing all records present in either source# __or__()
- set1 & set2 returns the intersection of two recordsets, a new recordset containing only records present in both sources# __and__()
- set1 - set2 returns a new recordset containing only records of set1 which are notin set2 # __sub__()
數據讀寫
_read_from_database()
調用 cursor 執行 數據庫 讀取, 同時 更新 record cache.
# store result in cache
for vals in result:
record = self.browse(vals.pop('id'), self._prefetch)
record._cache.update(record._convert_to_cache(vals, validate=False))
read()
create()
write()
unlink()
約束
模型的 _constraints 屬性和 _sql_constraints 屬性
其中 _constraints 已廢棄,改用 @api. Constraints()
SQL 約束
(name, sql_definition, message)
模型初始化 db 時,往 db 建立約束, _add_sql_constraints () #L2175 model.py
Python 約束
通過 _validate_fields() 驗證 #L933 model.py /// create() 和 write() 時調用。
通過返回 true, 否則返回異常
detailed,,,
_constraints
list of (constraint_function, message, fields) defining Python constraints. The fields list is indicative
Deprecated since version 8.0: use constrains()
_sql_constraints
list of (name, sql_definition, message) triples defining SQL constraints to execute when generating the backing table
默認值
模型 defaults 屬性
通過上下文默認值,用戶默認值, 模型默認值[ 字段默認值,父級默認值] 進行維護
上下文默認值
default_ 開頭, 加上 字段
用戶默認值
self.env['ir.default'].get_model_defaults(self._name)
模型默認值
字段默認值
field.default
父級字段默認值
if field and field.inherited:
field = field.related_field
parent_fields[field.model_name].append(field.name)
具體 邏輯
default_get() #L974 model.py
實踐用法:
改寫 default_get() 改變默認值
視圖和字段
Web client 通過 rpc 調用 load_views 得到視圖定義
@api.model
def load_views(self, views, options=None):
""" Returns the fields_views of given views, along with the fields of
the current model, and optionally its filters for the given action.
:param views: list of [view_id, view_type]
:param options['toolbar']: True to include contextual actions when loading fields_views
:param options['load_filters']: True to return the model's filters
:param options['action_id']: id of the action to get the filters
:return: dictionary with fields_views, fields and optionally filters
"""
options = options or {}
result = {}
toolbar = options.get('toolbar')
result['fields_views'] = {
v_type: self.fields_view_get(v_id, v_type if v_type != 'list' else 'tree',
toolbar=toolbar if v_type != 'search' else False)
for [v_id, v_type] in views
}
result['fields'] = self.fields_get()
if options.get('load_filters'):
result['filters'] = self.env['ir.filters'].get_filters(self._name, options.get('action_id'))
return result
底層 2個 方法:
fields_view_get 獲取視圖定義
fields_get 獲取字段定義
權限
ACL 記錄在 ir.model.access
Record rule 記錄在 ir.rule
對於 admin ,,, user_id =1 旁路
if self._uid == 1:
# User root have all accesses
return True
對於 admin ,,, user_id =1 旁路
if self._uid == SUPERUSER_ID:
return
在 增create() 刪 unlink() 改 write() 查 search() 時, 調用 check_access_rights() 以及 check_access_rule() 檢查是否有權限
視圖之權限處理
調用 _apply_group() 將 無權限訪問的 node 去除
對於 field 設置了 權限的, 將 字段設置為 readonly
服務
wsgi 應用
導出 xmlrpc 和 http 服務
WSGI application 被 server 使用
XML RPC 服務
http.py dispatch_rpc()
根據不同的名稱空間,轉發給對應的 handler
Web服務
http.py Root.dispatch()
# 通過 Root 類 的 __call__() 調用 dispatch()