# 指導規范
[TOC]
## 模塊構造
### 文件夾
模塊的文件夾列表及對應作用:
* `data/` 演示和實際數據的xml
* `models/` 模型定義
* `controllers/` 包含控制器
* `views/` 包含視圖和模板
* `static/` 包含頁面相關的,一般劃分為`css/`,`/js`,`/img`...
其他可選的文件夾:
* `wizard/` 放臨時的model和視圖
* `report/` 存放報表相關的python對象和xml
* `tests/` 存放python和yml測試用例
### 文件命名
一般把后臺視圖和前端頁面視圖分兩個文件夾存放。一系列業務model放置在一個文件里,如果只有一個model它的名字就與模型名一致。如:
* `models/<main_model>.py`
* `models/<inherited_main_model>.py`
* `views/<main_model>_templates.xml`
* `views/<main_model>_views.xml`
數據文件根據其內容分開命名,如果demo和data,文件名是以主模型名命令,并以_demo.xml,_data.xml結尾
控制器中唯一的文件被命名為`main.py`。如果需要從其他的模塊中繼承控制器,就命令為`<module_name>.py`
由于靜態文件在前端頁面和后臺頁面是共用的,css、js、xml文件一般以該組的名字來結尾如:`im_chat_common.css, im_chat_common.js`,如果模塊只有唯一一個文件,就直接用`module_name.ext`
對于wizards,一般命名為`<main_transient>.py,<main_transient>_views.xml`
對于報表,一般命名為`<report_name_A>_report.py,<report_name_A>_report_views.py`
對于打印報表,一般命名為`<print_report_name>_reports.py , <print_report_name>_templates.xml`
完整的文件列表如下:
~~~
addons/<my_module_name>/
|-- __init__.py
|-- __manifest__.py
|-- controllers/
| |-- __init__.py
| |-- <inherited_module_name>.py
| `-- main.py
|-- data/
| |-- <main_model>_data.xml
| `-- <inherited_main_model>_demo.xml
|-- models/
| |-- __init__.py
| |-- <main_model>.py
| `-- <inherited_main_model>.py
|-- report/
| |-- __init__.py
| |-- <main_stat_report_model>.py
| |-- <main_stat_report_model>_views.xml
| |-- <main_print_report>_reports.xml
| `-- <main_print_report>_templates.xml
|-- security/
| |-- ir.model.access.csv
| `-- <main_model>_security.xml
|-- static/
| |-- img/
| | |-- my_little_kitten.png
| | `-- troll.jpg
| |-- lib/
| | `-- external_lib/
| `-- src/
| |-- js/
| | `-- <my_module_name>.js
| |-- css/
| | `-- <my_module_name>.css
| |-- less/
| | `-- <my_module_name>.less
| `-- xml/
| `-- <my_module_name>.xml
|-- views/
| |-- <main_model>_templates.xml
| |-- <main_model>_views.xml
| |-- <inherited_main_model>_templates.xml
| `-- <inherited_main_model>_views.xml
`-- wizard/
|-- <main_transient_A>.py
|-- <main_transient_A>_views.xml
|-- <main_transient_B>.py
`-- <main_transient_B>_views.xml
~~~
## XML文件
### 格式
當定義一個記錄的xml時,需要一個`<record>`標記:
* 將id屬性放在model前面
* 對于字段的字義,name屬性放在最前面,然后放`value`,`eval`,之后再按重要程度放其他屬性如widget, options
* 根據model將record進行分組,但在action/menu/views有依賴關系時該規則不好用
* 使用好的命名習慣
* `<data>`標簽只在設置不可更新的數據時用,使用`noupdate=1`
~~~
<record id="view_id" model="ir.ui.view">
<field name="name">view.name</field>
<field name="model">object_name</field>
<field name="priority" eval="16"/>
<field name="arch" type="xml">
<tree>
<field name="my_field_1"/>
<field name="my_field_2" string="My Label" widget="statusbar" statusbar_visible="draft,sent,progress,done" />
</tree>
</field>
</record>
~~~
odoo還支持一些自定義的標簽如:
* `menuitem` - 用于定義一個`ir.ui.menu`
* `workflow` - `<workflow>`標簽向工作流發送信號
* `template` - 用于定義一個只需要`arch`片段的view
* `report` - 用于定義報表action
* `act_window`- 當record用不了的時候用它
### 命名xml_id
#### 安全、視圖和action
* 菜單 - `<model_name>_menu`
* 視圖 - `<model_name>_view_<view_type>`, view_type=>`kanban, form, tree, search...`
* action - 主action命名為`<model_name>_action`,其他用`_<detail>`后綴,detail可用于簡要描述該action功能
* 分組 - `<model_name>_group_<group_name>` ,`group_name`是分組名如用戶、管理員……
* 規則 - `<model_name>_rule_<concerned_group>`,concerned_group 代表對應組的簡寫如user,public...
~~~
<!-- views and menus -->
<record id="model_name_view_form" model="ir.ui.view">
...
</record>
<record id="model_name_view_kanban" model="ir.ui.view">
...
</record>
<menuitem
id="model_name_menu_root"
name="Main Menu"
sequence="5"
/>
<menuitem
id="model_name_menu_action"
name="Sub Menu 1"
parent="module_name.module_name_menu_root"
action="model_name_action"
sequence="10"
/>
<!-- actions -->
<record id="model_name_action" model="ir.actions.act_window">
...
</record>
<record id="model_name_action_child_list" model="ir.actions.act_window">
...
</record>
<!-- security -->
<record id="module_name_group_user" model="res.groups">
...
</record>
<record id="model_name_rule_public" model="ir.rule">
...
</record>
<record id="model_name_rule_company" model="ir.rule">
...
</record>
~~~
#### 繼承xml
繼承視圖的命名方式是`<base_view>_inherit_<current_module_name>`
~~~
<record id="inherited_model_view_form_inherit_my_module" model="ir.ui.view">
...
</record>
~~~
## python
### PEP8
使用linter來幫助顯示語法錯誤和警告,odoo遵循了大部分的python標準,但忽略了以下幾項:
* E501: line too long
* E301: expected 1 blank line, found 0
* E302: expected 2 blank lines, found 1
* E126: continuation line over-indented for hanging indent
* E123: closing bracket does not match indentation of opening bracket's line
* E127: continuation line over-indented for visual indent
* E128: continuation line under-indented for visual indent
* E265: block comment should start with '# '
### 導入
導入的順序為:
1. 導入標準庫,每個庫一行
2. 導入odoo
3. 從odoo模塊導入(需要的時候)
~~~
# 1 : imports of python lib
import base64
import re
import time
from datetime import datetime
# 2 : imports of odoo
import odoo
from odoo import api, fields, models # alphabetically ordered
from odoo.tools.safe_eval import safe_eval as eval
from odoo.tools.translate import _
# 3 : imports from odoo modules
from odoo.addons.website.models.website import slug
from odoo.addons.web.controllers.main import login_redirect
~~~
### 符合習慣的python編程
* 每個文件放置`# -*- coding: utf-8 -*-`在頭一行
* 從更易讀的角度出發
* 不用clone()
~~~
# bad
new_dict = my_dict.clone()
new_list = old_list.clone()
# good
new_dict = dict(my_dict)
new_list = list(old_list)
~~~
* Python字典:創建和修改
~~~
# -- creation empty dict
my_dict = {}
my_dict2 = dict()
# -- creation with values
# bad
my_dict = {}
my_dict['foo'] = 3
my_dict['bar'] = 4
# good
my_dict = {'foo': 3, 'bar': 4}
# -- update dict
# bad
my_dict['foo'] = 3
my_dict['bar'] = 4
my_dict['baz'] = 5
# good
my_dict.update(foo=3, bar=4, baz=5)
my_dict = dict(my_dict, **my_dict2)
~~~
* 使用有意義的變量名、函數名、類名
* 臨時變量有時可以讓賦值給對象更簡單:
~~~
# pointless
schema = kw['schema']
params = {'schema': schema}
# simpler
params = {'schema': kw['schema']}
~~~
* 為了簡單可使用多重返回
~~~
# a bit complex and with a redundant temp variable
def axes(self, axis):
axes = []
if type(axis) == type([]):
axes.extend(axis)
else:
axes.append(axis)
return axes
# clearer
def axes(self, axis):
if type(axis) == type([]):
return list(axis) # clone the axis
else:
return [axis] # single-element list
~~~
* 內置函數
* 正確使用list,dict,map
* 集合也可以當成布爾型
~~~
bool([]) is False
bool([1]) is True
bool([False]) is True
~~~
* 循環
~~~
# creates a temporary list and looks bar
for key in my_dict.keys():
"do something..."
# better
for key in my_dict:
"do something..."
# creates a temporary list
for key, value in my_dict.items():
"do something..."
# only iterates
for key, value in my_dict.iteritems():
"do something..."
~~~
* 使用dict.setdefault
~~~
# longer.. harder to read
values = {}
for element in iterable:
if element not in values:
values[element] = []
values[element].append(other_value)
# better.. use dict.setdefault method
values = {}
for element in iterable:
values.setdefault(element, []).append(other_value)
~~~
* 添加注釋
### odoo中編程
* 避免自定義生成器和裝飾器,只使用odoo API已有的
* 使用更易理解的方法名
#### 讓你的方法可以批量處理
當添加一個函數時,確保它可以處理多重數據,如通過`api.multi()`裝飾器,可以在self上進行循環處理
~~~
@api.multi
def my_method(self)
for record in self:
record.do_cool_stuff()
~~~
避免使用`api.one`裝飾器,因為它可能不會像你想象中一樣工作。
為了更好的性能,比如當定義一個狀態按鈕時,不在api.multi循環里用search和search_count方法,而用read_group一次計算
~~~
@api.multi
def _compute_equipment_count(self):
""" Count the number of equipement per category """
equipment_data = self.env['hr.equipment'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id'])
mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in equipment_data])
for category in self:
category.equipment_count = mapped_data.get(category.id, 0)
~~~
#### 擴散上下文環境
在新API中,context變量是不能修改的。可以通過`with_context`來使用新的運行環境調用方法。
~~~
records.with_context(new_context).do_stuff() # all the context is replaced
records.with_context(**additionnal_context).do_other_stuff() # additionnal_context values override native context ones
~~~
如果需要創建一個新的context來對某對象進行操作, 選擇一個好名字,以模塊名為前綴用隔離它的影響,如:`mail_create_nosubscribe, mail_notrack, mail_notify_user_signature...`
#### 盡量使用ORM
當ORM可以實現的時候盡量使用ORM而不要直接寫sql,因為它可能會繞過orm的一些規則如權限、事務等
,還會讓代碼變得難讀且不安全。
~~~
# very very wrong
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in (' + ','.join(map(str, ids))+') AND state=%s AND obj_price > 0', ('draft',))
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]
# no injection, but still wrong
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in %s '\
'AND state=%s AND obj_price > 0', (tuple(ids), 'draft',))
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]
# better
auction_lots_ids = self.search([('auction_id','in',ids), ('state','=','draft'), ('obj_price','>',0)])
~~~
#### 不要進行sql注入
不要用python的+號連接符、%解釋符來拼sql
~~~
self.env.cr.execute('SELECT distinct child_id FROM account_account_consol_rel ' +
'WHERE parent_id IN ('+','.join(map(str, ids))+')')
# better
self.env.cr.execute('SELECT DISTINCT child_id '\
'FROM account_account_consol_rel '\
'WHERE parent_id IN %s',
(tuple(ids),))
~~~
#### 盡量把方法寫的短而簡單
#### 不要手動提交事務
odoo有自己的一套機制用于事務處理
#### 正確的使用翻譯方法
odoo用一個下劃線方法來表明某字段需要翻譯,該方法通過`from odoo.tools.translate import _`導入
一般情況下該方法只能被用在手動寫在代碼里的字符串的翻譯,不能用在動態的值中,翻譯方法的調用只能是`_('literal string')`,里面不能加其他的。
~~~
# good: plain strings
error = _('This record is locked!')
# good: strings with formatting patterns included
error = _('Record %s cannot be modified!') % record
# ok too: multi-line literal strings
error = _("""This is a bad multiline example
about record %s!""") % record
error = _('Record %s cannot be modified' \
'after being validated!') % record
# bad: tries to translate after string formatting
# (pay attention to brackets!)
# This does NOT work and messes up the translations!
error = _('Record %s cannot be modified!' % record)
# bad: dynamic string, string concatenation, etc are forbidden!
# This does NOT work and messes up the translations!
error = _("'" + que_rec['question'] + "' \n")
# bad: field values are automatically translated by the framework
# This is useless and will not work the way you think:
error = _("Product %s is out of stock!") % _(product.name)
# and the following will of course not work as already explained:
error = _("Product %s is out of stock!" % product.name)
# bad: field values are automatically translated by the framework
# This is useless and will not work the way you think:
error = _("Product %s is not available!") % _(product.name)
# and the following will of course not work as already explained:
error = _("Product %s is not available!" % product.name)
# Instead you can do the following and everything will be translated,
# including the product name if its field definition has the
# translate flag properly set:
error = _("Product %s is not available!") % product.name
~~~
格式化字符串如`%s, %d`需要被保留,并以更明確的方式使用:
~~~
# Bad: makes the translations hard to work with
error = "'" + question + _("' \nPlease enter an integer value ")
# Better (pay attention to position of the brackets too!)
error = _("Answer to question %s is not valid.\n" \
"Please enter an integer value.") % question
~~~
### 符號和習慣
* 模型名-使用`.`分隔,模塊名做前綴
* 定義odoo模型時,使用單數形式的名字如res.user,res.partner
* 定義wizard時,使用`<related_base_model>.<action>`,related_base_model 代表其所相關的模型名,action是其功能簡稱,如`account.invoice.make`
* 定義報表模型時,使用`<related_base_model>.report.<action>`,和wizard一樣
* python類-使用駝峰命名方式
~~~
class AccountInvoice(models.Model):
...
class account_invoice(osv.osv):
...
~~~
* 變量名
* 模型變量使用駝峰命名方式
* 普通變量用下劃線+小寫字母
* 由于新api中記錄是集合形式,當變量不包含id時不以id作后綴
~~~
ResPartner = self.env['res.partner']
partners = ResPartner.browse(ids)
partner_id = partners[0].id
~~~
`One2Many, Many2Many`字段一般以ids作為后綴如:sale_order_line_ids
`Many2One` 一般以_id為后綴如:partner_id, user_id
* 方法習慣
* 計算字段- 計算方法一般是`_compute_<field_name>`
* 搜索方法- `_search_<field_name>`
* 默認方法- `_default_<field_name>`
* onchange方法- `_onchange_<field_name>`
* 約束方法 - `_check_<constraint_name>`
* action方法- 一個對象的動作方法一般以action_開頭,它的裝飾器是`@api.multi`,如果它只使用單條計算,可在方法頭添加`self.ensure_one()`
* 模型中屬性的順序
* 私有屬性:_name, _description, _inherit, ...
* 默認方法和_default_get
* 字段聲明
* 計算和搜索方法和字段聲明順序一致
* 約束方法(`@api.constrains`)和onchange方法(`@api.onchange`)
* CRUD方法
* action方法
* 其他業務方法
~~~
class Event(models.Model):
# Private attributes
_name = 'event.event'
_description = 'Event'
# Default methods
def _default_name(self):
...
# Fields declaration
name = fields.Char(string='Name', default=_default_name)
seats_reserved = fields.Integer(oldname='register_current', string='Reserved Seats',
store=True, readonly=True, compute='_compute_seats')
seats_available = fields.Integer(oldname='register_avail', string='Available Seats',
store=True, readonly=True, compute='_compute_seats')
price = fields.Integer(string='Price')
# compute and search fields, in the same order of fields declaration
@api.multi
@api.depends('seats_max', 'registration_ids.state', 'registration_ids.nb_register')
def _compute_seats(self):
...
# Constraints and onchanges
@api.constrains('seats_max', 'seats_available')
def _check_seats_limit(self):
...
@api.onchange('date_begin')
def _onchange_date_begin(self):
...
# CRUD methods (and name_get, name_search, ...) overrides
def create(self, values):
...
# Action methods
@api.multi
def action_validate(self):
self.ensure_one()
...
# Business methods
def mail_user_confirm(self):
...
~~~
## Javascript和CSS
javascript:
* 在所有javascript文件中使用`use strict;`
* 使用linter
* 不添加壓縮javascript庫
* 類名使用駝峰命名
* 如果javascript代碼需要全局運行,在website模塊中聲明一個if_dom_contains 方法
~~~
odoo.website.if_dom_contains('.jquery_class_selector', function () {
/*your code here*/
});
~~~
CSS:
* 將所有的class命名為`o_<module_name>`,如`o_forum`
* 避免使用id
* 使用bootstrap的class
* 用下劃線+小寫來命名
## GIT
建議看專門的GIT教程
# 更新API
[http://www.odoo.com/documentation/10.0/reference/upgrade_api.html](http://www.odoo.com/documentation/10.0/reference/upgrade_api.html)
* * *
譯自odoo官方文檔:[http://www.odoo.com/documentation/10.0/reference/guidelines.html](http://www.odoo.com/documentation/10.0/reference/guidelines.html) ,不當之處歡迎批評指正。
*內容發布自[http://www.jianshu.com/u/6fdae8ec06bc](http://www.jianshu.com/u/6fdae8ec06bc),轉載請注明出處*
作者:湖南玖零網絡科技
鏈接:http://www.jianshu.com/p/dfe15ef18769
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
- 開發教程
- Odoo10開發教程一(構建模塊)
- Odoo10開發教程二(基本視圖)
- Odoo10開發教程三(模型關聯)
- Odoo10開發教程四(繼承)
- Odoo10開發教程五(計算字段和默認值)
- Odoo10開發教程六(高級視圖)
- Odoo10開發教程七(工作流和安全)
- 參考手冊
- odoo V10中文參考手冊(一:ORM API)
- odoo V10中文參考手冊(指導規范)
- 技巧
- odoo 常用widget
- Odoo(OpenERP)開發實踐:菜單隱藏(1)
- Odoo(OpenERP)開發實踐:菜單隱藏(2)
- Odoo(OpenERP)開發實踐:數據模型學習
- Odoo中自動備份數據庫
- Odoo(OpenERP)應用實踐: 使用db-filter參數實現通過域名指定訪問哪個數據庫
- Odoo(OpenERP)配置文件openerp-server.conf詳解
- Odoo(OpenERP v8)數據模型(Data Model)
- odoo10學習筆記十七:controller
- Qweb定義