编码规范¶
本页面介绍了 Odoo 编码规范。这些规范旨在提高 Odoo 应用代码的质量。良好的代码可以提升可读性、简化维护、帮助调试、降低复杂性并提高可靠性。这些规范应应用于每个新模块以及所有新开发。
警告
在修改 稳定版本 中的现有文件时,原始文件的风格严格优先于任何其他样式指南。换句话说,请勿为了应用这些指南而修改现有文件。这样可以避免破坏代码行的修订历史记录,差异应尽量保持最小化。更多详细信息,请参阅我们的 拉取请求指南 。
警告
在修改 master(开发)版本 中的现有文件时,仅对已修改的代码或大部分文件正在修订的情况下应用这些指南。换句话说,仅在文件结构需要重大更改时才对其进行修改。在这种情况下,首先提交一个 移动 提交,然后应用与功能相关的更改。
模块结构¶
目录¶
一个模块被组织成重要的目录。这些目录包含业务逻辑;查看它们应该能让您理解模块的用途。
data/ :演示和数据 XML
models/ :模型定义
controllers/ :包含控制器(HTTP 路由)
views/ :包含视图和模板
static/ :包含 Web 资源,分为 css/, js/, img/, lib/, …
其他可选目录组成了模块。
wizard/ :包含临时模型(
models.TransientModel
)及其视图report/ :包含基于 SQL 视图的可打印报表和模型。Python 对象和 XML 视图包含在此目录中
tests/ :包含 Python 测试
文件命名¶
文件命名对于快速查找所有 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.model.access.csv
文件中。用户组定义在
<module>_groups.xml
中。记录规则定义在
<model>_security.xml
中。
addons/plant_nursery/
|-- security/
| |-- ir.model.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
后缀。例如,对于一个应用程序,其主模型具有演示和数据,还包括子类型、活动和邮件模板,所有这些都与邮件模块相关:
addons/plant_nursery/
|-- data/
| |-- plant_nursery_data.xml
| |-- plant_nursery_demo.xml
| |-- mail_data.xml
关于 控制器 ,通常所有控制器都属于一个包含在名为 <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
|-- 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/
| |-- 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 等)。尝试按模型对记录进行分组。如果操作/菜单/视图之间存在依赖关系,则此约定可能不适用。
使用下一节定义的命名约定
<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’ 等。对于规则:
<model_name>_rule_<concerned_group>
,其中 concerned_group 是相关组的简称(’user’ 表示 ‘model_name_group_user’,’public’ 表示公共用户,’company’ 表示多公司规则等)。
名称应与 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 选项¶
使用 linter 可以帮助显示语法和语义警告或错误。Odoo 源代码尽量遵循 Python 标准,但其中一些可以忽略。
E501:行过长
E301:期望有 1 行空行,但未找到
E302:期望有 2 行空行,但只找到 1 行
导入¶
导入顺序如下
外部库(每行一个,按 Python 标准库排序并分组)
导入
odoo
从 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 Command, _, api, fields, models # alphabetically ordered
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)
作为一名优秀的开发者,请为您的代码编写文档(方法的 docstring,复杂代码部分的简单注释)。
除了这些指南外,您可能还会对以下链接感兴趣: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
警告
在上下文中传递参数可能会产生危险的副作用。
由于值会自动传播,可能会出现一些意外行为。调用模型的 create()
方法时,如果上下文中包含 default_my_field 键,则会为该模型设置 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 调用提供事务上下文。其原理是,在每次 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:
cr.rollback() # error, rollback everything atomically
raise
finally:
cr.close() # always close cursor opened manually
return res
如果在 RPC 调用执行期间发生任何错误,事务将原子性回滚,从而保持系统的状态。
类似地,系统在测试套件执行期间也提供了专用事务,因此可以根据服务器启动选项决定是否回滚。
其结果是,如果您在任何地方手动调用 cr.commit()
,极有可能以各种方式破坏系统,因为您会导致部分提交,从而引发部分且不干净的回滚,导致以下问题:
不一致的业务数据,通常会导致数据丢失
工作流不同步,文档永久卡住
无法干净回滚的测试,将开始污染数据库并触发错误(即使在事务期间没有发生错误,也是如此)
- 以下是非常简单的规则:
您 绝不应该 自行调用
cr.commit()
,除非 您明确创建了自己的数据库游标!需要这样做的情况非常罕见!顺便提一下,如果您确实创建了自己的游标,那么您需要处理错误情况并正确回滚,同时在完成操作后正确关闭游标。
与普遍的看法相反,在以下情况下甚至不需要调用 cr.commit()
:- 在 models.Model 对象的 _auto_init()
方法中:这由插件初始化方法或 ORM 事务在创建自定义模型时处理;- 在报表中:框架也会处理 commit()
,因此即使在报表中也可以更新数据库;- 在 models.Transient 方法中:这些方法的调用方式与普通的 models.Model 方法完全相同,事务结束时会自动调用相应的 cr.commit()/rollback()
。如有疑问,请参见上述通用规则!
从现在起,所有在服务器框架之外的 cr.commit()
调用都必须附带 明确的注释 ,解释它们为何绝对必要、为何正确以及为何不会破坏事务。否则,它们将会被移除!
正确使用翻译方法¶
Odoo 使用一种类似 GetText 的方法,称为“下划线” _()
,用于指示代码中使用的静态字符串需要在运行时进行翻译。该方法可通过 self.env._
调用,并使用环境的语言。
使用此方法时,必须遵循一些非常重要的规则,以确保其正常工作并避免翻译中充斥无用的内容。
基本上,此方法仅适用于手动编写在代码中的静态字符串,无法用于翻译字段值(如产品名称等)。这种情况应通过在相应字段上设置翻译标志来实现。
该方法接受可选的位置参数或命名参数。规则非常简单:调用下划线方法时,形式应始终为 self.env._('字面字符串')
,不得有其他形式:
_ = 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 类:使用驼峰式命名(面向对象风格)。
class AccountInvoice(models.Model):
...
- 变量名称:
模型变量使用驼峰命名法
普通变量使用下划线小写命名法。
如果变量包含记录 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
、_sql_constraints
等)默认值方法和
default_get
字段声明
计算、逆向和搜索方法的顺序应与字段声明顺序一致
选择方法(用于返回选择字段计算值的方法)
约束方法(
@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
def create(self, values):
...
# 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 :这是存放 JavaScript 库的地方,通常位于子文件夹中。例如,所有来自 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 :所有将在 JavaScript 中渲染的 QWeb 模板
static/tests :这是存放所有测试相关文件的地方。
static/tests/tours :这是存放所有引导测试文件的地方(非教程)。
JavaScript 编码规范¶
建议在所有 JavaScript 文件中使用
use strict;
使用代码检查工具(如 jshint 等)
切勿添加压缩版的 JavaScript 库
类声明使用驼峰命名法
更精确的 JavaScript 规范详见 GitHub Wiki 。您还可以通过查看 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;
}
使用四个空格缩进,不使用制表符;
每行最多 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_
前缀。避免创建过于具体的类名和变量名。在为嵌套元素命名时,采用“孙级”方法。
Example
Don’t
<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>
Do
<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]
组件名称或模块名称(优先使用组件名称)。
[元素]
内部元素的可选标识符。
[属性]
变量定义的属性或行为。
[修饰符]
可选的修饰符。
Example
$o-block-color: value;
$o-block-title-color: value;
$o-block-title-color-hover: value;
SCSS 变量(作用域)¶
这些变量在块内声明,外部无法访问。我们的标准约定是 $-[变量名]
。
Example
.o_element {
$-inner-gap: compute-something;
margin-right: $-inner-gap;
.o_element_child {
margin-right: $-inner-gap * 0.5;
}
}
SCSS 混合宏和函数¶
我们的标准约定是 o-[名称]
。使用描述性名称。命名函数时,使用祈使形式的动词(例如: get
、make
、apply
等)。
在 作用域变量表单 中命名可选参数,即 $-[参数]
。
Example
@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]
组件名称或模块名称(优先使用组件名称)。
[元素]
内部元素的可选标识符。
[属性]
变量定义的属性或行为。
[修饰符]
可选的修饰符。
Example
.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 的 UI 中通常 不会使用 的技术。这种做法通常用于全局访问和修改 CSS 变量,而我们则是通过 SCSS 来实现这一点。
此规则的例外情况应该非常明显,例如跨捆绑包共享的模板,这些模板需要一定程度的上下文感知才能正确渲染。