本指南旨在提高 Odoo 应用程序代码的质量。事实上,正确的代码可以提高可读性、简化维护、帮助调试、降低复杂性并提高可靠性。这些指南应适用于每个新模块和所有新的开发。
注意:不要直接更改ODOO的原始文件,须继承修改。
一、模块结构
目录
模块的文件都被组织在相关重要的目录中。也包含业务逻辑;查看它们可以让您了解模块的用途。
data/ : 演示数据、预置数据
models/:模型定义
controllers/:包含控制器(HTTP 路由)
views/:包含视图和模板(template)
static/ : 包含 web 资源,分为css/, js/, img/, lib/, …
其他可选目录组成模块。
Wizard/ : 包含瞬态模型 ( models.TransientModel) 及其视图
report/:包含基于视图的可打印报告和模型。Python 对象和 XML 视图包含在此目录中
tests/:包含 Python 测试
二、文件命名
文件命名对于通过odoo 插件快速查找信息很重要。本节说明如何在标准 odoo 模块中命名文件。作为示例,我们使用具体的应用程序进行说明。它拥有两个主要模型plant.nursery和plant.order。
关于模型,将业务逻辑划分为属于同一主模型的模型集。每个集合都位于一个给定的文件中,该文件根据其主模型命名。如果只有一个模型,则其名称与模块名称相同。每个继承的模型都应该在自己的文件中,以帮助理解受影响的模型。
addons/plant_nursery/
|-- models/
| |-- plant_nursery.py (第一个主模型)
| |-- plant_order.py (另一个主模型)
| |-- res_partner.py (继承ODOO模型)
对于安全性,应该使用三个主要文件:
第一个是在ir.model.access.csv文件中完成的访问权限的定义。
用户组定义<模块名>_groups.xml。
记录规则定义<模型名>_security.xml.
addons/plant_nursery/
|-- security/
| |-- ir.model.access.csv
| |-- plant_nursery_groups.xml
| |-- plant_nursery_security.xml
| |-- plant_order_security.xml
关于视图,后端视图应该像模型一样拆分,格式:_views.xml。后端视图包括列表(tree)、表单(form)、看板(kanban)、活动(activity)、图表(gant)、数据透视(povit)等。对于菜单的管理,应该将其提取到可选<模块名>_menus.xml文件中。模板(主要用于门户/网站显示的 QWeb 页面)和包(JS 和 CSS 资产的导入)放在单独的文件中。这些分别是 <模型>_templates.xml和assets.xml文件。
addons/plant_nursery/
|-- views/
| | -- assets.xml (import of JS / CSS)
| | -- plant_nursery_menus.xml (optional definition of main menus)
| | -- plant_nursery_views.xml (backend views)
| | -- plant_nursery_templates.xml (portal templates)
| | -- plant_order_views.xml
| | -- plant_order_templates.xml
| | -- res_partner_views.xml
关于数据,按用途(演示或数据)和主要模型对其进行拆分。文件名如 main_model_demo.xml或 main_model_data.xml。例如,对于一个应用程序,它的主模型具有演示数据和所需的预置数据,以及与邮件模块相关的子类型、活动和邮件模板:
addons/plant_nursery/
|-- data/
| |-- plant_nursery_data.xml
| |-- plant_nursery_demo.xml
| |-- mail_data.xml
关于控制器,控制器放在一个<模块名称>.py文件中. 之前Odoo 是把它们放在主文件main.py里(已弃用)。如果需要在另一个模块中继承现有控制器,请放在
addons/plant_nursery/
|-- controllers/
| |-- plant_nursery.py
| |-- portal.py (继承 portal/controllers/portal.py)
| |-- main.py (已弃用, 用plant_nursery.py代替)
关于静态文件,Javascript 文件遵循与 python 模型相同的逻辑。
每个组件都应位于其自己的文件中,并具有有意义的名称。例如,活动小部件位于activity.js邮件模块中。还可以创建子目录来构建“包”(有关更多详细信息,请参见 web 模块)。JS 小部件的模板(静态 XML 文件)和它们的样式(scss 文件)应该应用相同的逻辑。不要在 Odoo 之外链接数据(图像、库):不要使用图像的 URL,而是要将文件复制到代码库中。
关于wizards,比较特殊。
命名约定与 python 模型相同: .py和_views.xml. 两者都放在相同的向导目录中。这个命名规则来自以前的 odoo 应用程序,它使用了用于瞬态模型的 Wizard 关键字。
addons/plant_nursery/
|-- wizard/
| |-- make_plant_order.py
| |-- make_plant_order_views.xml
关于report报表部分,结构如下:
addons/plant_nursery/
|-- report/
| |-- plant_order_report.py
| |-- plant_order_report_views.xml
关于包含基础数据和 Qweb 模板的可打印报表,目录结构如下:
addons/plant_nursery/
|-- report/
| |-- plant_order_reports.xml (report actions, paperformat, ...)
| |-- plant_order_templates.xml (xml report templates)
至此,我们的 Odoo 模块的完整结构如下:
addons/plant_nursery/
|-- __init__.py
|-- __manifest__.py
|-- controllers/
| |-- __init__.py
| |-- plant_nursery.py
| |-- portal.py
|-- data/
| |-- plant_nursery_data.xml
| |-- plant_nursery_demo.xml
| |-- mail_data.xml
|-- models/
| |-- __init__.py
| |-- plant_nursery.py
| |-- plant_order.py
| |-- res_partner.py
|-- report/
| |-- __init__.py
| |-- plant_order_report.py
| |-- plant_order_report_views.xml
| |-- plant_order_reports.xml (report actions, paperformat, ...)
| |-- plant_order_templates.xml (xml report templates)
|-- security/
| |-- ir.model.access.csv
| |-- plant_nursery_groups.xml
| |-- plant_nursery_security.xml
| |-- plant_order_security.xml
|-- static/
| |-- img/
| | |-- my_little_kitten.png
| | |-- troll.jpg
| |-- lib/
| | |-- external_lib/
| |-- src/
| | |-- js/
| | | |-- widget_a.js
| | | |-- widget_b.js
| | |-- scss/
| | | |-- widget_a.scss
| | | |-- widget_b.scss
| | |-- xml/
| | | |-- widget_a.xml
| | | |-- widget_a.xml
|-- views/
| |-- assets.xml
| |-- plant_nursery_menus.xml
| |-- plant_nursery_views.xml
| |-- plant_nursery_templates.xml
| |-- plant_order_views.xml
| |-- plant_order_templates.xml
| |-- res_partner_views.xml
|-- wizard/
| |--make_plant_order.py
| |--make_plant_order_views.xml
注意:文件名只能包含[a-z0-9_](小写字母数字和_)
警告:请使用正确的文件权限:文件夹 755 和文件 644。
三、XML文件
格式
要在 XML 中声明记录,需要使用记录表示法():
将id属性放在model前面
<record id='fleet_vehicle_log_services_view_tree' model='ir.ui.view'>
对于字段声明,name属性是第一位的。然后将 value 放在field标签中,或者在eval 属性中,最后是其他属性(widget,options,…)。
标签仅用于设置不可更新的数据noupdate=1。
<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的快捷方式
template: 声明QWEB视图.
report: 声明一个report动作
act_window: windows动作
XML 记录集及命名规范
安全、视图和操作
使用以下模式:
对于菜单命名规则:
对于视图:
对于动作: 动作的命名规范可以考虑
对于windows窗口动作: 用特定的视图信息作为动作名称的后缀,如
对于group组:
对于记录规则:
名称应与xml id相同,用点替换下划线。动作应该有一个真实的名称,因为它常用作显示。
<!-- views -->
<record id="model_name_view_form" model="ir.ui.view">
<field name="name">model.name.view.form</field>
...
</record>
<record id="model_name_view_kanban" model="ir.ui.view">
<field name="name">model.name.view.kanban</field>
...
</record>
<!-- actions -->
<record id="model_name_action" model="ir.act.window">
<field name="name">Model Main Action</field>
...
</record>
<record id="model_name_action_child_list" model="ir.actions.act_window">
<field name="name">Model Access Children</field>
</record>
<!-- menus and sub-menus -->
<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"
/>
<!-- 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的继承
继承视图的Xml ID应使用与原始记录相同的ID。它有助于一目了然地找到所有继承. 由于最终的Xml ID由创建它们的模块作为前缀,因此没有混淆.
命名应包含.inherit.{details}后缀,便于在查看其名称时理解重写目的.
<record id="model_view_form" model="ir.ui.view">
<field name="name">model.view.form.inherit.module2</field>
<field name="inherit_id" ref="module1.model_view_form"/>
...
</record>
新的主视图不需要继承后缀,因为这些是基于第一个主视图的新记录。
<record id="module2.model_view_form" model="ir.ui.view">
<field name="name">model.view.form.module2</field>
<field name="inherit_id" ref="module1.model_view_form"/>
<field name="mode">primary</field>
...
</record>
四、Python
PEP8 规范
pep8 python编码规范要求每个缩进级别使用4个空格,语句后面跟注释的话至少要隔两个空格。
使用此规范时可以辅助用户显示语法和语义警告或错误。Odoo源代码遵循Python标准,但其中一些代码可以忽略.
E501:行太长
E301: 应为1空行,找到0
E302: 应为2空行,找到0
Imports 导入
顺序如下:
import base64(外部库,还有re,time等)
Imports of odoo
Imports from Odoo modules (很少,只在有必要的时候这样做)
注:导入的行按字母顺序排序。
# 1 : 导入python外部库
import base64
import re
import time
from datetime import datetime
# 2 : 导入 odoo
import odoo
from odoo import api, fields, models, _ # 按字母顺序排列
from odoo.tools.safe_eval import safe_eval as eval
# 3 : 从odoo的模块导入
from odoo.addons.website.models.website import slug
from odoo.addons.web.controllers.main import login_redirect
编程习惯用语 (Python)
建议倾向于可读性而不是简洁性,或者使用语言特征或习语
不要使用: .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']}
如果语法简单,可以使用多个return
# 有点复杂,有冗余的成份
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
您至少应该对所有Python内置代码有一个基本的了解:(http://docs.python.org/library/functions.html)
value = my_dict.get('key', None) # 多余
value = my_dict.get('key') # good
此外, 如果 ‘key’ 在 my_dict里并且 my_dict.get(‘key’) 有不同含义, 则需要保证使用正确(上面两行其中一行)。
学习:使用列表、dict和map、filter、sum等的基本操作,它们使代码更易于阅读。
# not very good
cube = []
for i in res:
cube.append((i['id'],i['name']))
# better
cube = [(i['id'], i['name']) for i in res]
集合也是布尔值 :
bool([]) is False
bool([1]) is True
bool([False]) is True
所以你可以用 if some_collection来代替 if len(some_collection)
迭代
# creates a temporary list and looks bar
for key in my_dict.keys():
"do something..."
# better
for key in my_dict:
"do something..."
# accessing the key,value pair
for key, value in my_dict.items():
"do something..."
使用dict.setdefault
setdefault()函数为python字典的内置函数。
dic={}
dic.setdefault(1)
print(dic)
--------结果--------
{1: None}
# 比较长,难于理解
values = {}
for element in iterable:
if element not in values:
values[element] = []
values[element].append(other_value)
# 较好.. 使用dict.setdefault 方法
values = {}
for element in iterable:
values.setdefault(element, []).append(other_value)
作为一名好的开发人员,要擅于做好代码注释
在Odoo中编程
避免创建生成器和装饰器:只使用ODOO API提供的生成器和装饰程序。
与python一样,使用过滤、映射、排序等方法来简化代码读取和性能。
让你的方法批量工作
添加函数时,请确保它可以通过在self上迭代来处理每个记录的方式来处理目标数据中的多个记录.
def my_method(self)
for record in self:
record.do_cool_stuff()
对于性能问题,在开发时,不要在循环中执行搜索或search_count。建议使用read_group方法,仅在一个请求中计算所有值。
def _compute_equipment_count(self):
""" Count the number of equipment 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)
context的处理
上下文不允许被修改。
如在调用方法时需要使用不同的上下文,应该使用 with_context方法
self = self.with_context(get_sizes=True)
等同
self.with_context(get_sizes=True).create()
或
new_context = dict(self.env.context)
new_context.update({'new_key': 'new_value', 'new_key2': 'new_value2'})
self.env.context = new_context
或
records.with_context(new_context).do_stuff() # 所有的上下文都被替换
如果需要创建影响某些对象行为的关键上下文,请选择一个好的名称,并最终以模块名称作为前缀,以隔离其影响。如: mail_create_nosubscribe, mail_notrack, mail_notify_user_signature, …
延伸扩展:
函数和方法不应该包含太多的逻辑:有很多小而简单的方法比有几个大而复杂的方法更可取。一个好的经验法则是,一旦一个方法有多个功能,就将其拆分。
应避免在方法中对业务逻辑进行硬编码,因为会阻碍子模块扩展。
请合理命名函数:正确命名函数是可读/可维护代码和更紧密文档的起点。
不用提交事务,因为封装的函数execute会自动处理
Odoo框架负责为所有RPC调用提供事务上下文。其原理是在每个RPC调用开始时打开一个新的数据库游标,并在调用返回时提交。
注:游标(Cursor)是处理数据的一种方法,为了查看或者处理结果集中的数据,游标提供了在结果集中一次一行或者多行前进或向后浏览数据的能力。
把游标当作一个指针,它可以指定结果中的任何位置,然后允许用户对指定位置的数据进行处理。
大致如下所示:
def execute(self, db_name, uid, obj, method, *args, **kw):
db, pool = pooler.get_db_and_pool(db_name)
# 建立事物游标
cr = db.cursor()
try:
res = pool.execute_cr(cr, uid, obj, method, *args, **kw)
cr.commit() # 提交
except Exception:
cr.rollback() # 如遇错误, 回滚所有内容
raise
finally:
cr.close() # 始终关闭手动打开的光标
return res
如果在执行RPC调用期间发生任何错误,事务将自动回滚,恢复系统原有状态。
如果您在程序中手动调用cr.commit(),则很有可能以各种方式破坏系统,因为您将导致部分提交,从而导致部分和不干净的回滚,其中包括:
1、业务数据不一致,通常导致数据丢失
2、工作流不同步;文档永久卡住
3、导致无法或干净地回滚
关于事务处理的简单原则:
永远不要手工执行cr.commit(), 除非您明确创建了自己的数据库游标!
顺便说一句,如果您确实创建了自己的游标,那么您需要处理错误情况和适当的回滚,以及在使用完光标后适当地关闭光标。
正确使用翻译方法
Odoo使用一个名为“underline”_()的类似GetText的方法来指示代码中使用的静态字符串需要在运行时使用上下文语言进行翻译。通过导入以下内容,就可以访问此伪方法:
from odoo import _
在使用它时,必须遵循一些非常重要的规则,以避免用无用的垃圾填充译文。
基本上,此方法只应用于代码中手动编写的静态字符串,并不适用于转换字段值,例如产品名称等。必须使用相应字段上的translate标志来完成。
该方法使用规则非常简单:对下划线方法的调用应始终采用_(“文字字符串”)的形式
# good: 普通字符串
error = _('This record is locked!')
# good: 包含格式模式的字符串
error = _('Record %s cannot be modified!', record)
# ok too: 多行文字字符串
error = _("""This is a bad multiline example
about record %s!""", record)
error = _('Record %s cannot be modified' \
'after being validated!', record)
# bad: 这不起作用,会把翻译搞砸!
error = _('Record %s cannot be modified!' % record)
# bad: 格式在翻译之外
error = _('Record %s cannot be modified!') % record
# bad: 禁止动态字符串、字符串连接等!
error = _("'" + que_rec['question'] + "' \n")
# bad: 字段值由框架自动转换
# 无用的代码,不会按你认为的方式工作:
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)
# 相反,您可以执行以下操作,所有内容都将被翻译,
error = _("Product %s is not available!", product.name)
此外,请记住,翻译器一定会处理传递到下划线函数内的文体,所以请努力保证它们易于理解并使用规范的字符。翻译器必须知道格式化模式,例如%s或%d, 新行, 等等.
# Bad: 使翻译难以处理
error = "'" + question + _("' \nPlease enter an integer value ")
# Ok
error = _("Answer to question %s is not valid.\n" \
"Please enter an integer value.", question)
# 更优
error = _("Answer to question %(title)s is not valid.\n" \
"Please enter an integer value.", title=question)
通常,在Odoo中,在处理字符串时,首选%而不是.format()(当字符串中只有一个变量要替换时),并且首选%(变量名)。这使得翻译器更容易翻译。
符号和惯例
Model name (使用点符号,以模块名称作为前缀) :
当定义一个新的模型时 : 使用单数形式的名称(res.partner和sale.order,而不是res.partnerS和saleS.orderS)
当定义瞬态模型时 (wizard) : 使用
当定义报表模型时 : 使用
Odoo Python 类 : 使用驼峰格式 (面向对象风格).
class AccountInvoice(models.Model):
...
变量名称 :
模型变量的命名使用驼峰格式
对公共变量使用下划线小写符号.
如果变量名包含记录id或id列表,则在变量名后面加上_id或_ids。
Partner = self.env['res.partner']
partners = Partner.browse(ids)
partner_id = partners[0].id
方法命名规则
Compute Field : compute
Search method : search
Default method : default
Selection method: selection
Onchange method : onchange
Constraint method )(限制方法): check
Action method : action_. 如果只用于单条记录, 在方法的开始位置添加self.ensure_one() .
在模型中,属性顺序应为
1、自有属性 (_name, _description, _inherit, …)
2、默认方法和 _default_get
3、字段声明
4、按与字段声明相同的顺序定义计算、求逆和搜索方法
5、Selection 方法 (用于返回选择字段的计算值的方法)
6、Constrains方法 (@api.constrains) and onchange方法 (@api.onchange)
7、CRUD 方法(ORM 部分)
8、Action 方法
9、最后是其他商业方法
class Event(models.Model):
# 1
_name = 'event.event'
_description = 'Event'
# 2
def _default_name(self):
...
# 3
name = fields.Char(string='Name', default=_default_name)
seats_reserved = fields.Integer(string='Reserved Seats', store=True
readonly=True, compute='_compute_seats')
seats_available = fields.Integer(string='Available Seats', store=True
readonly=True, compute='_compute_seats')
price = fields.Integer(string='Price')
event_type = fields.Selection(string="Type", selection='_selection_type')
# 4
@api.depends('seats_max', 'registration_ids.state', 'registration_ids.nb_register')
def _compute_seats(self):
...
# 5
@api.model
def _selection_type(self):
return []
# 6
@api.constrains('seats_max', 'seats_available')
def _check_seats_limit(self):
...
@api.onchange('date_begin')
def _onchange_date_begin(self):
...
# 7
def create(self, values):
...
# 8
def action_validate(self):
self.ensure_one()
...
# 9
def mail_user_confirm(self):
...
Javascript and CSS
静态文件组织
Odoo插件在如何构造各种文件方面有一些规定. 以下内容详细地解释了如何组织web assets.
第一件要知道的事,是odoo将本地静态服务文件统一放在一个 static/ 目录下,但前缀为加载项名称.。举例说明, 如是一个文件的位置是 addons/web/static/src/js/some_file.js, 运行后它的 url为: your-odoo-server.com/web/static/src/js/some_file.js
惯例是按照以下结构组织代码:
static: 所有静态文件
static/lib: js libs文件夹。例如,jquery库中的所有文件都位于addons/web/static/lib/jquery中
static/src: 通用静态源代码文件夹
static/src/css: 所有 css 文件
static/fonts:字体
static/img:图片
static/src/js:JS文件
static/src/js/tours: 最终用户教程文件(教程,非测试)
static/src/scss: scss 文件
static/src/xml: 所有可在js中渲染的 qweb templates
static/tests:这是我们放置所有测试相关文件的地方.
static/tests/tours:放置所有测试文件(而不是教程)的地方。
Javascript编码准则
严格使用;建议用于所有javascript文件