Odoo编码规范¶
本页介绍 Odoo 编码规范。这些规范旨在提升 Odoo 应用代码的质量。规范的代码确实可以提高可读性、便于维护、帮助调试、降低复杂度并提升可靠性。这些规范应应用于每个新模块和所有新开发工作。
警告
在 稳定版本 中修改现有文件时,文件的原始风格严格优先于任何其他风格规范。换言之,请切勿为了应用这些规范而修改现有文件,以免打乱代码行的修订历史。差异应保持最小。有关更多细节,请参阅我们的 拉取请求指南。
警告
在 master(开发)版本 中修改现有文件时,仅对修改过的代码,或在文件大部分都处于修订中的情况下,才将这些规范应用于现有代码。换言之,仅当文件结构将发生重大变更时,才修改现有文件结构。在这种情况下,请先进行一次 移动 提交,然后应用与该功能相关的更改。
模块结构¶
警告
对于社区开发的模块,强烈建议在模块名前加上类似公司名的前缀。
目录¶
模块按重要目录进行组织。这些目录包含业务逻辑;查看它们应当能让您理解该模块的用途。
data/:演示数据和数据 XML
models/:模型定义
controllers/:包含控制器(HTTP 路由)
views/:包含视图和模板
static/:包含 Web 资源,分为 css/、js/、img/、lib/ 等
其他可选目录也构成模块的一部分。
wizard/:归组临时模型(
models.TransientModel)及其视图report/:包含可打印报表以及基于 SQL 视图的模型。Python 对象和 XML 视图也包含在此目录中。
tests/:包含 Python 测试
populate/:包含数据填充蓝图和可选的生成器
文件命名¶
文件命名对于在 Odoo 所有插件中快速查找信息非常重要。本节介绍标准 Odoo 模块中文件的命名方式。作为示例,我们使用一个 植物苗圃 应用。它包含两个主要模型 plant.nursery 和 plant.order。
关于 模型,请按归属于同一主模型的模型集合来拆分业务逻辑。每个集合放在一个给定的文件中,文件名基于其主模型。如果只有一个模型,则其名称与模块名相同。每个继承的模型都应放在单独的文件中,以便理解受影响的模型。
addons/plant_nursery/
|-- models/
| |-- plant_nursery.py (first main model)
| |-- plant_order.py (another main model)
| |-- res_partner.py (inherited Odoo model)
关于 安全,应使用三个主要文件:
第一条是在
ir.access.csv文件中定义的访问权限。用户组在
<module>_groups.xml中定义。记录规则在
<model>_security.xml中定义。
addons/plant_nursery/
|-- security/
| |-- ir.access.csv
| |-- plant_nursery_groups.xml
| |-- plant_nursery_security.xml
| |-- plant_order_security.xml
关于 视图,后端视图应像模型一样拆分,并以 _views.xml 作为后缀。后端视图包括列表、表单、看板、活动、图形、透视等视图。为了便于按模型拆分,未链接到特定操作的视图主菜单可以提取到可选的 <module>_menus.xml 文件中。模板(特别是用于门户/网站显示的 QWeb 页面)放在名为 <model>_templates.xml 的单独文件中。
addons/plant_nursery/
|-- views/
| | -- 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
关于 数据,请按用途(演示或数据)和主模型进行拆分。文件名将是主模型名称加上 _demo.xml 或 _data.xml 后缀。例如,对于某个应用,其主模型以及子类型、活动和邮件模板都与 mail 模块相关,其演示数据和数据如下:
addons/plant_nursery/
|-- data/
| |-- plant_nursery_data.xml
| |-- plant_nursery_demo.xml
| |-- mail_data.xml
关于 populate,蓝图是 populate.blueprint 的记录,以声明式方式描述要创建哪些数据。它们通常随模块的 populate/ 目录一起分发,并在 populate 模块升级时自动加载。如果该目录是一个有效的 Python 包(包含 __init__.py),其中的代码会被导入,从而允许模块注册自定义生成器。
addons/plant_nursery/
|-- populate/
| |-- plant_nursery.xml (blueprint records)
| |-- __init__.py (optional, imports custom generators)
| |-- generators.py (optional custom generators)
关于 控制器,通常所有控制器都属于名为 <module_name>.py 的文件中所包含的单个控制器。Odoo 中有一个旧约定是将此文件命名为 main.py,但这被视为过时的做法。如果您需要继承另一个模块的现有控制器,请在 <inherited_module_name>.py 中进行。例如,在应用中添加门户控制器是在 portal.py 中完成的。
addons/plant_nursery/
|-- controllers/
| |-- plant_nursery.py
| |-- portal.py (inheriting portal/controllers/portal.py)
| |-- main.py (deprecated, replaced by plant_nursery.py)
关于 静态文件,JavaScript 文件总体上遵循与 Python 模型相同的逻辑。每个组件都应放在单独的文件中,并使用有意义的名称。例如,活动小部件位于 mail 模块的 activity.js 中。也可以创建子目录来组织“包”(有关更多细节,请参阅 web 模块)。JS 小部件的模板(静态 XML 文件)及其样式(scss 文件)也应遵循相同的逻辑。不要链接 Odoo 外部的数据(图像、库):不要使用指向图像的 URL,而应将其复制到代码库中。
关于 向导,命名约定与 Python 模型相同:<transient>.py 和 <transient>_views.xml。两者都放在 wizard 目录中。这种命名源于旧的 Odoo 应用,它们将 wizard 关键字用于临时模型。
addons/plant_nursery/
|-- wizard/
| |-- make_plant_order.py
| |-- make_plant_order_views.xml
关于使用 Python / SQL 视图和经典视图完成的 统计报表,命名如下:
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
|-- populate/
| |-- plant_nursery.xml
| |-- __init__.py
| |-- generators.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.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/
| |-- 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 中声明记录,建议使用 record 记法(使用 <record>):
将
id属性放在model之前对于字段声明,
name属性放在最前面。然后将 值 放在field标签或eval属性中,最后按重要程度排列其他属性(widget、options 等)。尽量按模型对记录进行分组。如果 action/menu/views 之间存在依赖关系,此约定可能不适用。
使用下一点中定义的命名约定
<data> 标签仅用于设置带
noupdate=1的不可更新数据。如果文件中只有不可更新数据,可以在<odoo>标签上设置noupdate=1,而不要设置<data>标签。
<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">
<list>
<field name="my_field_1"/>
<field name="my_field_2" string="My Label" widget="statusbar" statusbar_visible="draft,sent,progress,done" />
</list>
</field>
</record>
Odoo 支持作为语法糖的自定义标签:
menuitem:用作声明
ir.ui.menu的快捷方式template:用于声明仅需视图
arch部分的 QWeb 视图。
这些标签优先于 record 记法使用。
XML ID 与命名¶
安全、视图和操作¶
使用以下模式:
对于菜单:
<model_name>_menu,对于子菜单则使用<model_name>_menu_do_stuff。对于视图:
<model_name>_view_<view_type>,其中 view_type 为kanban、form、list、search……对于动作,主动作遵循
<model_name>_action。其余动作以_<detail>为后缀,其中 detail 是一个小写字符串,简要说明该动作。仅当为模型声明了多个动作时才会使用。对于窗口动作,在动作名称后加上具体的视图信息作为后缀,例如
<model_name>_action_view_<view_type>。对于群组,请使用
<module_name>_group_<group_name>,其中 group_name 是群组的名称,通常为 ‘user’、’manager’ 等。
名称应与 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>
继承 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 选项¶
使用 linter 有助于显示语法和语义方面的警告或错误。Odoo 源代码尽量遵循 Python 标准,但其中部分规则可以忽略。
E501:行过长
E301:预期 1 个空行,实际为 0 个
E302:预期 2 个空行,实际为 1 个
导入¶
导入的顺序如下
外部库(每行一个,按字母排序,与 Python 标准库分开)
odoo子模块的导入从 Odoo 插件模块导入(很少使用,且仅在必要时)
在这 3 组内部,导入行按字母顺序排序。
# 1 : imports of python lib
import base64
import re
import time
from datetime import datetime
# 2 : imports of odoo
from odoo import Command, _, api, fields, models # ASCIIbetically ordered
from odoo.fields import Domain
from odoo.tools.safe_eval import safe_eval as eval
# 3 : imports from odoo addons
from odoo.addons.web.controllers.main import login_redirect
from odoo.addons.website.models.website import slug
编程惯用法(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']}
当更简单时,多个返回点是可行的
# 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
了解内置函数:您至少应对所有 Python 内置函数有基本的了解(http://docs.python.org/library/functions.html)
value = my_dict.get('key', None) # very very redundant
value = my_dict.get('key') # good
此外,if 'key' in my_dict 与 if my_dict.get('key') 的含义有很大不同,请确保您使用的是正确的那一个。
学习列表推导式:使用列表推导式、字典推导式,以及通过
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]
集合也可以是布尔值:在 Python 中,许多对象在布尔上下文(例如 if)中求值时都具有“类布尔”值。其中就包括集合(列表、字典、集合等),它们在为空时为“假值”,在包含元素时为“真值”:
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
# 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)
作为一名优秀的开发者,请为代码编写文档(在方法上使用文档字符串,对代码中棘手的部分写简单的注释)
除这些规范之外,你可能还会对以下链接感兴趣:https://david.goodger.org/projects/pycon/2007/idiomatic/handout.html(略显过时,但仍相当有价值)
在 Odoo 中编程¶
避免自行创建生成器和装饰器:只使用 Odoo API 提供的那些。
与 Python 中一样,使用
filtered、mapped、sorted等方法,使代码更易读并提升性能。
传递上下文¶
上下文是一个无法修改的 frozendict。要以不同的上下文调用方法,应使用 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
警告
在上下文中传递参数可能会产生危险的副作用。
由于这些值会自动传播,因此可能会出现一些意外的行为。在上下文中带有 default_my_field 键调用模型的 create() 方法,将会为相关模型设置 my_field 的默认值。但如果在这次创建过程中,还创建了其他对象(例如在创建 sale.order 时创建 sale.order.line),而这些对象具有名为 my_field 的字段,那么它们的默认值也会被设置。
如果您需要创建一个影响某个对象行为的上下文键,请选择一个合适的名称,必要时还可以用模块名作为前缀,以隔离其影响范围。mail 模块的键就是一个很好的例子:mail_create_nosubscribe、mail_notrack、mail_notify_user_signature 等。
考虑可扩展性¶
函数和方法不应包含过多逻辑:拥有许多小而简单的方法,比拥有少量大而复杂的方法更可取。一个很好的经验法则是:一旦某个方法承担了不止一项职责,就应将其拆分(请参阅 http://en.wikipedia.org/wiki/Single_responsibility_principle)。
应避免在方法中硬编码业务逻辑,因为这会妨碍子模块轻松对其进行扩展。
# do not do this
# modifying the domain or criteria implies overriding whole method
def action(self):
... # long method
partners = self.env['res.partner'].search(complex_domain)
emails = partners.filtered(lambda r: arbitrary_criteria).mapped('email')
# better but do not do this either
# modifying the logic forces to duplicate some parts of the code
def action(self):
...
partners = self._get_partners()
emails = partners._get_emails()
# better
# minimum override
def action(self):
...
partners = self.env['res.partner'].search(self._get_partner_domain())
emails = partners.filtered(lambda r: r._filter_partners()).mapped('email')
为便于示例,上面的代码过度追求可扩展性,但必须考虑可读性并做出权衡。
此外,请为函数取恰当的名称:小而命名得体的函数是代码可读、可维护以及文档更严谨的起点。
这条建议同样适用于类、文件、模块和包。(另请参阅 http://en.wikipedia.org/wiki/Cyclomatic_complexity)
切勿提交事务¶
Odoo 框架负责为所有 RPC 调用提供事务上下文。服务器框架之外的所有 cr.commit() 调用都必须带有 明确的注释,说明为什么它们绝对必要、为什么它们确实正确,以及为什么它们不会破坏事务。否则,它们可能且将会被移除!
其原理是:在每次 RPC 调用开始时打开新的数据库游标,并在调用返回后、把应答发送给 RPC 客户端之前提交事务,大致如下所示:
def execute(self, db_name, uid, obj, method, *args, **kw):
db, pool = pooler.get_db_and_pool(db_name)
# create transaction cursor
cr = db.cursor()
try:
res = pool.execute_cr(cr, uid, obj, method, *args, **kw)
cr.commit() # all good, we commit
except Exception: # try to be more specific
cr.rollback() # error, rollback everything atomically
raise
finally:
cr.close() # always close cursor opened manually
return res
如果 RPC 调用执行期间发生任何错误,事务会被原子性地回滚,从而保持系统原有的状态。
同样,系统在执行测试套件和计划动作期间也会提供专用事务。
其结果是,如果您在任何地方手动调用 cr.commit(),那么极有可能会以多种方式破坏系统,因为您会造成部分提交,进而导致部分且不干净的回滚,从而引起以下问题:
业务数据不一致,通常会导致数据丢失
工作流不同步,单据永久卡住
测试无法干净地回滚,并开始污染数据库并触发错误(即使事务期间没有发生任何错误,情况也是如此)
- 以下是极其简单的规则:
你 绝不应 自行调用
cr.commit()或cr.rollback(),除非 你已显式创建了自己的数据库游标!而且需要这样做的情形极为罕见!顺便说一句,如果您确实创建了自己的游标,那么您需要处理错误情况并正确回滚,同时在使用完毕后正确关闭游标。
与普遍看法相反,在以下情形中你甚至不需要调用 cr.commit():
在 models.Model 对象的
_auto_init()方法中:这由插件模块的初始化方法处理,或者在创建自定义模型时由 ORM 事务处理在报表中:
commit()同样由框架处理,因此即使在报表内部也可以更新数据库在 models.Transient 方法中:这些方法的调用方式与常规 models.Model 方法完全相同,都在事务中执行,并在结束时执行相应的
cr.commit()/rollback()等等(如有疑问,请参见上文的通用规则!)
避免捕获异常¶
只捕获特定的异常,避免过于宽泛的异常处理。未捕获的异常会被记录日志,并由框架妥善处理。
你应明确指定要捕获的异常类型并相应处理,同时应尽可能缩小 try-catch 代码块的范围。
# BAD CODE
try:
do_something()
except Exception as e:
# if we caught a ValidationError, we did not rollback and we left the
# ORM in an undefined state
_logger.warning(e)
对于计划动作,如果你捕获到错误并希望继续执行,应回滚所做的更改。计划动作在单独的事务中运行,因此你可以在报告进度时直接回滚或提交。
更多内容
如果你必须处理框架异常,必须使用 保存点 来尽可能隔离你的函数。这会在进入代码块时刷新计算结果,并在出现异常时正确回滚更改。
try:
with self.env.cr.savepoint():
do_stuff()
except ...:
...
警告
在单个事务中启动超过 64 个保存点后,PostgreSQL 会变慢。无论如何,如果服务器运行了副本,保存点的开销都非常大。如果你在循环中处理记录并创建保存点,例如在批量逐个处理记录时,请限制批次的大小。如果记录更多,该函数也许应该改为计划任务,或者你必须接受性能损失。
正确使用翻译方法¶
Odoo 使用一个类似 GetText 的、名为“下划线”的方法 _(),用于标明代码中使用的静态字符串需要在运行时翻译。该方法可通过 self.env._ 使用,并采用环境对应的语言。
使用它时必须遵循几条非常重要的规则,以确保其正常工作,并避免翻译内容被无用的垃圾信息填满。
基本上,此方法只应用于代码中手动编写的静态字符串,它无法用于翻译字段值,例如产品名称等。这类内容必须改为在相应字段上使用翻译标志来完成。
该方法接受可选的位置参数或命名参数。规则非常简单:对下划线方法的调用应始终采用 self.env._('literal string') 的形式,不应有其他写法:
_ = self.env._
# 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: formatting outside of translation
# This won't benefit from fallback mechanism in case of bad translation
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)
# 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 ")
# Ok (pay attention to position of the brackets too!)
error = _("Answer to question %s is not valid.\n" \
"Please enter an integer value.", question)
# Better
error = _("Answer to question %(title)s is not valid.\n" \
"Please enter an integer value.", title=question)
在 Odoo 中处理字符串时,一般优先使用 % 而不是 .format() (当字符串中只有一个变量需要替换时),并优先使用 %(varname) 而不是位置参数(当有多个变量需要替换时)。这样可让社区译员的翻译工作更轻松。
符号与约定¶
- 模型名称(使用点号表示法,以模块名作为前缀):
定义 Odoo 模型时:使用名称的单数形式(用 res.partner 和 sale.order,而不是 res.partnerS 和 saleS.orderS)
定义 Odoo 瞬态模型(向导)时:使用
<related_base_model>.<action>,其中 related_base_model 是与该瞬态模型相关的基础模型(定义在 models/ 中),而 action 是该瞬态模型所做事情的简称。避免使用 wizard 一词。例如:account.invoice.make、project.task.delegate.batch等。定义 report 模型(即 SQL 视图)时:使用
<related_base_model>.report.<action>,基于瞬态模型的约定。
Odoo Python 类:使用 Pascal 命名法(面向对象风格)。
class AccountInvoice(models.Model):
...
- 变量名:
模型变量使用 Pascal 命名法
普通变量使用下划线小写表示法。
如果变量包含记录 ID 或 ID 列表,请在变量名后加上 _id 或 _ids 后缀。不要用
partner_id来存放 res.partner 的记录
Partner = self.env['res.partner']
partners = Partner.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>
选择方法:选择方法的命名模式为 _selection_<field_name>
Onchange 方法:Onchange 方法的命名模式为 _onchange_<field_name>
约束方法:约束方法的命名模式为 _check_<constraint_name>
动作方法:对象动作方法以 action_ 作为前缀。由于该方法只使用一条记录,请在方法开头添加
self.ensure_one()。
- 在模型中,属性的顺序应为
私有属性(
_name、_description、_inherit……)默认方法和
default_get字段声明
SQL 约束与索引
计算、反向和搜索方法,顺序与字段声明相同
选择方法(用于返回选项字段计算值的方法)
约束方法(
@api.constrains)和 onchange 方法(@api.onchange)CRUD 方法(ORM 重写)
动作方法
最后是其他业务方法。
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(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')
# compute and search fields, in the same order of fields declaration
@api.depends('seats_max', 'registration_ids.state', 'registration_ids.nb_register')
def _compute_seats(self):
...
@api.model
def _selection_type(self):
return []
# 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_search, _search, ...) overrides
@api.model
def create(self, vals_list):
...
# Action methods
def action_validate(self):
self.ensure_one()
...
# Business methods
def mail_user_confirm(self):
...
JavaScript¶
静态文件组织¶
Odoo 插件对各类文件的结构有一些约定。这里我们将更详细地说明 Web 资源应如何组织。
首先要了解的是,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 库的位置,每个库放在一个子文件夹中。例如,jquery 库的所有文件都位于 addons/web/static/lib/jquery
static/src:通用的静态源代码文件夹
static/src/css:所有 CSS 文件
static/fonts
static/img
static/src/js
static/src/js/tours:最终用户漫游文件(教程,而非测试)
static/src/scss:SCSS 文件
static/src/xml:所有将在 JS 中渲染的 QWeb 模板
static/tests:这里放置所有与测试相关的文件。
static/tests/tours:这里放置所有漫游测试文件(不是教程)。
Javascript 编码规范¶
建议所有 JavaScript 文件都使用
use strict;使用代码检查工具(jshint 等)
切勿添加经过压缩的 JavaScript 库
类声明使用 Pascal 命名法
更详细的 JS 指南请参阅 github wiki。您也可以查看 JavaScript 参考资料,了解现有的 JavaScript API。
CSS 与 SCSS¶
语法与格式¶
.o_foo, .o_foo_bar, .o_baz {
height: $o-statusbar-height;
.o_qux {
height: $o-statusbar-height * 0.5;
}
}
.o_corge {
background: $o-list-footer-bg-color;
}
.o_foo, .o_foo_bar, .o_baz {
height: 32px;
}
.o_foo .o_quux, .o_foo_bar .o_quux, .o_baz .o_qux {
height: 16px;
}
.o_corge {
background: #EAEAEA;
}
使用四(4)个空格缩进,不使用制表符;
每列最多 80 个字符宽;
左花括号(
{):最后一个选择器之后留一个空格;右花括号(
}):独占一行;每条声明单独一行;
合理使用空白。
"stylelint.config": {
"rules": {
// https://stylelint.io/user-guide/rules
// Avoid errors
"block-no-empty": true,
"shorthand-property-no-redundant-values": true,
"declaration-block-no-shorthand-property-overrides": true,
// Stylistic conventions
"indentation": 4,
"function-comma-space-after": "always",
"function-parentheses-space-inside": "never",
"function-whitespace-after": "always",
"unit-case": "lower",
"value-list-comma-space-after": "always-single-line",
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
"declaration-colon-space-after": "always",
"declaration-colon-space-before": "never",
"block-closing-brace-empty-line-before": "never",
"block-opening-brace-space-before": "always",
"selector-attribute-brackets-space-inside": "never",
"selector-list-comma-space-after": "always-single-line",
"selector-list-comma-space-before": "never-single-line",
}
},
属性顺序¶
属性按由“外”到内的顺序排列:从 position 开始,到装饰性规则(font、filter 等)为止。
作用域 SCSS 变量 和 CSS 变量 必须放在最顶部,其后用一个空行与其他声明分隔。
.o_element {
$-inner-gap: $border-width + $legend-margin-bottom;
--element-margin: 1rem;
--element-size: 3rem;
@include o-position-absolute(1rem);
display: block;
margin: var(--element-margin);
width: calc(var(--element-size) + #{$-inner-gap});
border: 0;
padding: 1rem;
background: blue;
font-size: 1rem;
filter: blur(2px);
}
命名约定¶
CSS 中的命名约定对于让代码更严谨、更透明、更具信息量极为有用。
id 选择器,并为类名添加 o_<module_name> 前缀,其中 <module_name> 是模块的技术名称(如 sale、im_chat 等),或模块保留的主路由(主要用于网站模块,例如 website_forum 模块使用 o_forum)。o_ 前缀。避免创建过于具体的类和变量名。为嵌套元素命名时,请采用“孙级(Grandchild)”方式。
示例
错误做法
<div class=“o_element_wrapper”>
<div class=“o_element_wrapper_entries”>
<span class=“o_element_wrapper_entries_entry”>
<a class=“o_element_wrapper_entries_entry_link”>Entry</a>
</span>
</div>
</div>
正确做法
<div class=“o_element_wrapper”>
<div class=“o_element_entries”>
<span class=“o_element_entry”>
<a class=“o_element_link”>Entry</a>
</span>
</div>
</div>
这种方式除了更紧凑之外,还便于维护,因为它减少了 DOM 发生变化时需要重命名的情况。
SCSS 变量¶
我们的标准约定是 $o-[root]-[element]-[property]-[modifier],其中:
$o-前缀。
[root]组件名 或 模块名(组件优先)。
[element]内部元素的可选标识符。
[property]变量所定义的属性/行为。
[modifier]可选的修饰符。
示例
$o-block-color: value;
$o-block-title-color: value;
$o-block-title-color-hover: value;
SCSS 变量(作用域内)¶
这些变量在代码块内声明,外部无法访问。我们的标准约定是 $-[variable name]。
示例
.o_element {
$-inner-gap: compute-something;
margin-right: $-inner-gap;
.o_element_child {
margin-right: $-inner-gap * 0.5;
}
}
更多内容
SCSS 混入与函数¶
我们的标准约定是 o-[name]。请使用具有描述性的名称。为函数命名时,请使用祈使语气的动词(例如 get、make、apply 等)。
请按 作用域变量的形式 来命名可选参数,即 $-[argument]。
示例
@mixin o-avatar($-size: 1.5em, $-radius: 100%) {
width: $-size;
height: $-size;
border-radius: $-radius;
}
@function o-invert-color($-color, $-amount: 100%) {
$-inverse: change-color($-color, $-hue: hue($-color) + 180);
@return mix($-inverse, $-color, $-amount);
}
更多内容
CSS 变量¶
在 Odoo 中,CSS 变量的使用严格与 DOM 相关。请使用它们来 按上下文 调整设计和布局。
我们的标准约定是 BEM,即 --[root]__[element]-[property]--[modifier],其中:
[root]组件名 或 模块名(组件优先)。
[element]内部元素的可选标识符。
[property]变量所定义的属性/行为。
[modifier]可选的修饰符。
示例
.o_kanban_record {
--KanbanRecord-width: value;
--KanbanRecord__picture-border: value;
--KanbanRecord__picture-border--active: value;
}
// Adapt the component when rendered in another context.
.o_form_view {
--KanbanRecord-width: another-value;
--KanbanRecord__picture-border: another-value;
--KanbanRecord__picture-border--active: another-value;
}
CSS 变量的使用¶
在 Odoo 中,CSS 变量的使用严格与 DOM 相关,也就是说,它们用于 按上下文 调整设计和布局,而不是用于管理全局设计系统。当组件的属性在特定上下文或其他情形下可能变化时,通常会使用这些变量。
我们在组件的主代码块中定义这些属性,并提供默认回退值。
CSS 与 SCSS 变量¶
尽管表面上相似,CSS 变量与 SCSS 变量的行为却大不相同。主要区别在于:SCSS 变量是 命令式 的,在编译时会被移除;而 CSS 变量是 声明式 的,会包含在最终输出中。
在 Odoo 中,我们兼取两者之长:使用 SCSS 变量定义设计系统,而在需要上下文适配时选用 CSS 变量。
上一个示例的实现应通过添加 SCSS 变量来改进,以便在顶层进行控制,并确保与其他组件保持一致。
:root 伪类¶
在 :root 伪类上定义 CSS 变量,是我们在 Odoo 界面中通常 不使用 的一种做法。这种做法常用于全局访问和修改 CSS 变量,而在 Odoo 中我们改用 SCSS 实现。
此规则的例外情形应当相当明显,例如在多个资源包之间共享的模板,它们需要一定的上下文感知能力才能正确渲染。