编码规范

本页面介绍了 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.nurseryplant.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_typekanbanformlistsearch 等。

  • 对于操作:主操作遵循 <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 行

导入

导入顺序如下

  1. 外部库(每行一个,按 Python 标准库排序并分组)

  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 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
value = my_dict.get('key', None) # very very redundant
value = my_dict.get('key') # good

此外, if 'key' in my_dictif my_dict.get('key') 的含义非常不同,请确保您使用的是正确的表达式。

  • 学习列表推导式:使用列表推导式、字典推导式以及通过 mapfiltersum 等进行的基本操作。它们使代码更易于阅读。

# 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)

Odoo 编程

  • 避免创建生成器和装饰器:仅使用 Odoo API 提供的生成器和装饰器。

  • 与 Python 类似,使用 filteredmappedsorted 等方法来简化代码阅读并提高性能。

传递上下文

上下文是一个不可修改的 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_nosubscribemail_notrackmail_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() ,极有可能以各种方式破坏系统,因为您会导致部分提交,从而引发部分且不干净的回滚,导致以下问题:

  1. 不一致的业务数据,通常会导致数据丢失

  2. 工作流不同步,文档永久卡住

  3. 无法干净回滚的测试,将开始污染数据库并触发错误(即使在事务期间没有发生错误,也是如此)

以下是非常简单的规则:

绝不应该 自行调用 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.partnersale.order ,而不是 res.partnerSsaleS.orderS

    • 定义 Odoo 临时模型(向导)时:使用 <related_base_model>.<action> ,其中 related_base_model 是与临时模型相关的基础模型(定义在 models/ 中),action 是临时模型所执行操作的简短名称。避免使用 wizard 一词。例如: account.invoice.makeproject.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
  • One2ManyMany2Many 字段应始终以 _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()

  • 在模型属性中,顺序应为:
    1. 私有属性( _name_description_inherit_sql_constraints 等)

    2. 默认值方法和 default_get

    3. 字段声明

    4. 计算、逆向和搜索方法的顺序应与字段声明顺序一致

    5. 选择方法(用于返回选择字段计算值的方法)

    6. 约束方法( @api.constrains )和 onchange 方法( @api.onchange

    7. CRUD 方法(ORM 覆盖)

    8. 动作方法

    9. 最后是其他业务方法。

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;
}
  • 使用四个空格缩进,不使用制表符;

  • 每行最多 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 开始,以装饰性规则(如 fontfilter 等)结束。

作用域 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> 是模块的技术名称(如 saleim_chat 等),或者是模块保留的主要路由(主要用于网站模块,例如: website_forum 模块的 o_forum )。
此规则的唯一例外是 Web 客户端:它仅使用 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-[名称] 。使用描述性名称。命名函数时,使用祈使形式的动词(例如: getmakeapply 等)。

作用域变量表单 中命名可选参数,即 $-[参数]

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 相关,这意味着它们用于 上下文化地 适应设计和布局,而不是管理全局设计系统。通常在组件的属性在特定上下文或其他情况下可能变化时使用。

我们在组件的主块中定义这些属性,并提供默认回退值。

Example

my_component.scss
.o_MyComponent {
   color: var(--MyComponent-color, #313131);
}
my_dashboard.scss
.o_MyDashboard {
   // Adapt the component in this context only
   --MyComponent-color: #017e84;
}

CSS 和 SCSS 变量

尽管表面上看起来相似, CSSSCSS 变量的行为却有很大不同。主要区别在于, SCSS 变量是 命令式的 并且会被编译掉,而 CSS 变量是 声明式的 并包含在最终输出中。

在 Odoo 中,我们取两者之长:使用 SCSS 变量定义设计系统,而在需要上下文化适配时选择 CSS 变量。

前一个示例的实现应通过添加 SCSS 变量加以改进,以在顶层获得控制并确保与其他组件的一致性。

Example

secondary_variables.scss
$o-component-color: $o-main-text-color;
$o-dashboard-color: $o-info;
// [...]
component.scss
.o_component {
   color: var(--MyComponent-color, #{$o-component-color});
}
dashboard.scss
.o_dashboard {
   --MyComponent-color: #{$o-dashboard-color};
}

:root 伪类

:root 伪类上定义 CSS 变量是一种我们在 Odoo 的 UI 中通常 不会使用 的技术。这种做法通常用于全局访问和修改 CSS 变量,而我们则是通过 SCSS 来实现这一点。

此规则的例外情况应该非常明显,例如跨捆绑包共享的模板,这些模板需要一定程度的上下文感知才能正确渲染。