编码规范

本页面介绍了 Odoo 编码规范。这些规范旨在提高 Odoo 应用程序代码的质量。事实上,良好的代码可以提高可读性,简化维护,有助于调试,降低复杂度并提升可靠性。这些规范应应用于每个新模块以及所有新开发。

警告

在修改 稳定版本 中的现有文件时,原始文件的样式严格优先于任何其他样式指南。换句话说,请永远不要为了应用这些指南而修改现有文件。这样可以避免干扰代码行的修订历史。差异应尽可能小。了解更多详情,请参阅我们的 提交拉取请求指南

警告

在修改 master(开发版) 中的现有文件时,仅对已修改的代码部分或大部分内容正在修订的文件应用这些指南。换句话说,只有在文件将进行重大更改时,才修改现有文件的结构。在这种情况下,首先进行一次 移动 提交,然后再应用与该功能相关的更改。

模块结构

目录

一个模块由重要的目录组成。这些目录包含业务逻辑;查看它们应该能让你理解该模块的用途。

  • 数据/ : 示例数据和数据 XML

  • models/ : 模型定义

  • 控制器/ : 包含控制器(HTTP 路由)

  • views/ : 包含视图和模板

  • static/:包含网页资源,按类型分为 css/、js/、img/、lib/* 等目录。

其他可选目录组成该模块。

  • 向导/ : 用于整合临时模型(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

Concerning views, backend views should be split like models and suffixed by _views.xml. Backend views are list, form, kanban, activity, graph, pivot, .. views. To ease split by model in views main menus not linked to specific actions may be extracted into an optional <module>_menus.xml file. Templates (QWeb pages used notably for portal / website display) are put in separate files named <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

Concerning data, split them by purpose (demo or data) and main model. Filenames will be the main_model name suffixed by _demo.xml or _data.xml. For instance for an application having demo and data for its main model as well as subtypes, activities and mail templates all related to mail module:

addons/plant_nursery/
|-- data/
|   |-- plant_nursery_data.xml
|   |-- plant_nursery_demo.xml
|   |-- mail_data.xml

关于 控制器,通常所有控制器都属于一个位于名为 <模块名称>.py 的文件中的单一控制器。Odoo 中有一种旧的约定是将此文件命名为 main.py,但现在已经不推荐使用。如果您需要从其他模块继承现有的控制器,请在 <被继承的模块名称>.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 模型相同的通用逻辑。每个组件应放在单独的文件中,并使用有意义的名称。例如,活动小部件位于邮件模块的 activity.js 中。也可以创建子目录来组织“包”(有关更多详细信息,请参见 web 模块)。对于 JavaScript 小部件的模板(静态 XML 文件)和样式(SCSS 文件),也应应用相同的逻辑。不要链接 Odoo 以外的数据(图像、库):不要使用图像的 URL,而是将其复制到代码库中。

Concerning wizards, naming convention is the same of for python models: <transient>.py and <transient>_views.xml. Both are put in the wizard directory. This naming comes from old odoo applications using the wizard keyword for transient models.

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

  • Place id attribute before model

  • For field declaration, name attribute is first. Then place the value either in the field tag, either in the eval attribute, and finally other attributes (widget, options, …) ordered by importance.

  • 尝试按模型对记录进行分组。在操作/菜单/视图之间存在依赖关系的情况下,此惯例可能不适用。

  • 请按照下一点中定义的命名规范进行命名

  • 标签 <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 支持自定义标签,作为语法糖使用:

  • 菜单项:用作声明 ir.ui.menu 的快捷方式

  • 模板:用于声明一个仅需要视图的 arch 部分的 QWeb 视图。

这些标签比 record 表示法更受推荐。

XML IDs 和命名

安全、视图和操作

请使用以下格式:

  • 对于菜单:<model_name>_menu,或用于子菜单的 <model_name>_menu_do_stuff

  • For a view: <model_name>_view_<view_type>, where view_type is 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 选项

使用代码检查工具可以帮助显示语法和语义警告或错误。Odoo 源代码尽量遵循 Python 标准,但其中一些警告或错误可以被忽略。

  • E501:行过长

  • E301:期望 1 行空行,找到 0 行

  • E302:期望有 2 个空行,找到 1 个

导入

导入项按以下顺序排列

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

  2. odoo 的导入

  3. 从 Odoo 模块导入(很少使用,且仅在必要时使用)

这3个组内的导入行按字母顺序排列。

# 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
# 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') 有非常不同的含义,请确保您使用的是正确的一种。

  • Learn list comprehensions : Use list comprehension, dict comprehension, and basic manipulation using map, filter, sum, … They make the code easier to read.

# 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 等方法,以提高代码可读性和性能。

传播上下文

The context is a frozendict that cannot be modified. To call a method with a different context, the with_context method should be used :

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 设置默认值。但如果在此创建过程中,其他对象(例如在创建销售订单时的销售订单行)具有字段名 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()除非 您已经显式地创建了自己的数据库游标!在需要这样做的情况下是非常罕见的!

顺便说一句,如果您自己创建了游标,那么您需要处理错误情况并进行适当的回滚,同时在使用完游标后正确关闭它。

And contrary to popular belief, you do not even need to call cr.commit() in the following situations: - in the _auto_init() method of an models.Model object: this is taken care of by the addons initialization method, or by the ORM transaction when creating custom models - in reports: the commit() is handled by the framework too, so you can update the database even from within a report - within models.Transient methods: these methods are called exactly like regular models.Model ones, within a transaction and with the corresponding cr.commit()/rollback() at the end - etc. (see general rule above if you are in doubt!)

从现在起,所有在服务器框架之外调用的 cr.commit() 必须带有**明确的注释**,说明为什么它们是绝对必要的,为什么它们确实是正确的,以及为什么它们不会破坏事务。否则,这些调用可能会被移除!

正确使用翻译方法

Odoo 使用一种名为 “underscore” _() 的类似 GetText 的方法,用于指示代码中需要在运行时翻译的静态字符串。该方法可通过 self.env._ 调用,并使用环境的语言。

使用它时必须遵循一些非常重要的规则,以确保其正常工作,并避免在翻译中产生无用的垃圾内容。

基本上,此方法仅应用于手动编写在代码中的静态字符串,它无法用于翻译字段值,例如产品名称等。应改用相应字段上的“可翻译”标志来完成此操作。

The method accepts optional positional or named parameter The rule is very simple: calls to the underscore method should always be in the form self.env._('literal string') and nothing else:

_ = 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.partnerssale.orders

    • When defining an Odoo Transient (wizard) : use <related_base_model>.<action> where related_base_model is the base model (defined in models/) related to the transient, and action is the short name of what the transient do. Avoid the wizard word. For instance : account.invoice.make, project.task.delegate.batch, …

    • 在定义 报告 模型(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 fields should have _id as suffix (example : partner_id, user_id, …)

  • 方法约定
    • 计算字段:计算方法的模式为 _compute_<字段名称>

    • 搜索方法:搜索方法的模式是 _search_<字段名称>

    • 默认方法:默认方法的模式是 _default_<字段名称>

    • 选择方式:选择方式的模式为 _selection_<字段名称>

    • 更改方法:更改方法的模式为 _onchange_<字段名称>

    • 约束方法:约束方法的模式为 _check_<约束名称>

    • 动作方法:对象的动作方法以前缀 action_ 开头。由于该方法仅操作一条记录,请在方法开头添加 self.ensure_one()

  • 模型属性的顺序应为
    1. 私有属性(_name_description_inherit_sql_constraints,…)

    2. 默认方法和 default_get

    3. 字段声明

    4. 按字段声明的相同顺序进行计算、逆向和搜索方法

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

    6. Constrains methods (@api.constrains) and onchange methods (@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 插件遵循一些关于如何组织各类文件的约定。此处我们将更详细地说明网页资源应如何进行组织。

首先要了解的是,Odoo 服务器会(静态地)提供位于 static/ 文件夹中的所有文件,但这些文件的路径会以模块名称作为前缀。因此,例如,如果一个文件位于 addons/web/static/src/js/some_file.js,那么它将在网址 your-odoo-server.com/web/static/src/js/some_file.js 处静态可用。

惯例是按照以下结构来组织代码:

  • 静态文件:所有通用静态文件

    • static/lib:这是存放 JS 库的目录,应放在一个子文件夹中。例如,jquery 库的所有文件都位于 addons/web/static/lib/jquery 中。

    • static/src: 通用的静态源代码文件夹

      • static/src/css:所有 CSS 文件

      • 静态字体文件

      • 静态图片

      • 静态/源代码/js

        • static/src/js/tours: 用户操作引导文件(教程,非测试用例)

      • 静态资源/样式表: scss 文件

      • static/src/xml: 所有将在 JS 中渲染的 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 等),或者是模块保留的主要路由(主要用于网站模块,例如:o_forum 对应 website_forum 模块)。
此规则的唯一例外是 webclient:它仅使用 o_ 前缀。

避免创建过于具体的类名和变量名。在命名嵌套元素时,选择“孙元素”(Grandchild)的方式。

Example

不要

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

    前缀。

  • [根]

    要么是组件 要么 是模块名称(组件优先)。

  • [元素]

    用于内部元素的可选标识符。

  • [属性]

    由变量定义的属性/行为。

  • [修改]

    一个可选的修饰符。

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-[name]。使用描述性的名称。在命名函数时,使用动词的祈使形式(例如:getmakeapply…)。

作用域变量表单 中为可选参数命名,格式为 $-[argument]

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]__[element]-[property]--[modifier],其中:

  • [根]

    要么是组件 要么 是模块名称(组件优先)。

  • [元素]

    用于内部元素的可选标识符。

  • [属性]

    由变量定义的属性/行为。

  • [修改]

    一个可选的修饰符。

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 用户界面的技术。这种做法常用于全局访问和修改 CSS 变量,我们则通过 SCSS 来实现这一功能。

此规则的例外情况应该是显而易见的,例如在多个捆绑包之间共享的模板,为了正确呈现,这些模板需要一定的上下文意识。