编码指南

本页介绍了 Odoo 编码指南。这些旨在提高 Odoo Apps 代码的质量。事实上,正确的代码可以提高可读性、简化维护、帮助调试、降低复杂性并提高可靠性。这些指南应适用于每个新模块和所有新开发。

警告

当修改**稳定版本**中的现有文件时,原始文件样式严格取代任何其他样式指南。换句话说,请勿为了应用这些准则而修改现有文件。它避免破坏代码行的修订历史记录。差异应保持最小。有关更多详细信息,请参阅我们的`pull request guide <https://www.chinaodoo.com/submit-pr>`_。

警告

当修改**主(开发)版本**中的现有文件时,仅针对修改后的代码或大部分文件正在修订时,将这些准则应用于现有代码。换句话说,仅当现有文件结构发生重大变化时才修改它。在这种情况下,首先执行 移动 提交,然后应用与该功能相关的更改。

模块结构

警告

对于社区开发的模块,强烈建议使用类似于您公司名称的前缀来命名您的模块。

目录

模块被组织在重要的目录中。这些包含业务逻辑;查看它们应该会让您了解该模块的用途。

  • data/ : 演示和数据 xml

  • models/ : 模型定义

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

  • views/ :包含视图和模板

  • static/ :包含 Web 资源,分为 css/, js/, img/, lib/, …

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

  • 向导/:重新组合瞬态模型(models.TransientModel)及其视图

  • report/ :包含基于 SQL 视图的可打印报告和模型。 Python 对象和 XML 视图包含在此目录中

  • tests/ :包含Python测试

文件命名

文件命名对于通过所有 odoo 插件快速查找信息非常重要。本节介绍如何在标准 odoo 模块中命名文件。作为示例,我们使用 plant nursery 应用程序。它拥有两个主要模型 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``. 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

关于*数据*,按用途(演示或数据)和主要模型将它们分开。文件名将是 main_model 名称,后缀为“_demo.xml` or ``_data.xml`”。例如,对于一个应用程序,其主模型以及子类型、活动和邮件模板都有演示和数据,所有这些都与邮件模块相关:

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

关于*控制器*,通常所有控制器都属于包含在名为“<module_name>.py`. An old convention in Odoo is to name this file main.py but it is considered as outdated. If you need to inherit an existing controller from another module do it in <inherited_module_name>.py. For example adding portal controller in an application is done in ``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 模块)。相同的逻辑应该应用于 JS 小部件的模板(静态 XML 文件)及其样式(scss 文件)。不要链接 Odoo 外部的数据(图像、库):不要使用图像的 URL,而是将其复制到代码库中。

关于*向导*,命名约定与 python 模型相同:<transient>.py and <transient>_views.xml。两者都放在向导目录中。此命名来自旧的 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_]`` (lowercase alphanumerics and _

警告

使用正确的文件权限:文件夹 755 和文件 644。

XML 文件

格式

要在 XML 中声明记录,建议使用 record 表示法(使用 <record>):

  • 放置“id` attribute before ``model`”

  • 对于字段声明,name attribute is first. Then place the value either in the field tag, either in the ``eval``属性,最后是其他属性(小部件、选项…)按重要性排序。

  • 尝试按型号对记录进行分组。如果操作/菜单/视图之间存在依赖关系,则此约定可能不适用。

  • 使用下一点定义的命名约定

  • 标签*<data>*仅用于设置带有``noupdate=1``. If there is only not-updatable data in the file, the noupdate=1 can be set on the <odoo> tag and do not set a ``<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``的快捷方式

  • 模板:使用它来声明仅需要视图的“arch”部分的 QWeb 视图。

这些标签优于 record 符号。

XML ID 和命名

安全、视图和操作

使用以下模式:

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

  • 对于视图:<model_name>_view_<view_type>,其中 view_typekanban, form, list, search,…

  • 对于操作:主要操作遵循 <model_name>_action。其他的则以 _<detail> 为后缀,其中 detail 是一个小写字符串,简要解释操作。仅当为模型声明了多个操作时才使用此选项。

  • 对于窗口操作:在操作名称后添加特定视图信息,例如 <model_name>_action_view_<view_type>

  • 对于组: <module_name>_group_<group_name> 其中 group_name 是组的名称,通常为“用户”、“经理”…

  • 对于规则: <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

警告

不要忘记阅读 Security Pitfalls 部分以编写安全代码。

PEP8 选项

使用 linter 可以帮助显示语法和语义警告或错误。 Odoo 源代码尝试尊重 Python 标准,但其中一些可以忽略。

  • E501:线路太长

  • E301:预期 1 个空行,发现 0 个

  • E302:预期有2个空行,发现有1个

进口

进口订单为

  1. 外部库(在 python stdlib 中每行排序和拆分一个)

  2. 导入 odoo 子模块

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

另外,“if 'key' in my_dict` and ``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:` instead of ``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 中一样,使用 filtered, mapped, sorted, … 方法来简化代码阅读和性能。

传播上下文

上下文是应该使用``frozendict`` that cannot be modified. To call a method with a different context, the ``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 的默认值。但如果在此创建过程中,创建了具有字段名称 my_field 的其他对象(例如 sale.order.line、sale.order 创建时),它们的默认值也会被设置。

如果您需要创建影响某些对象行为的关键上下文,请选择一个好的名称,并最终在其前面加上模块名称的前缀以隔离其影响。一个很好的例子是 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 调用提供事务上下文。服务器框架之外的所有“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()` ,您很有可能会以各种方式破坏系统,因为您将导致部分提交,从而导致部分和不干净的回滚,从而导致:

  1. 业务数据不一致,通常是数据丢失

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

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

这是一个非常简单的规则:

你应该**永远**自己调用``cr.commit()`` or 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 的方法,名为“下划线”_() to indicate that a static string used in the code needs to be translated at runtime. That method is available at 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` or ``%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 中,操作字符串时,更喜欢 % over .format() (when only one variable to replace in a string), and prefer %(varname) 而不是位置(当必须替换多个变量时)。这使得社区翻译人员的翻译变得更加容易。

符号和惯例

  • 型号名称(使用点符号,模块名称前缀):
    • 定义 Odoo 模型时:使用名称的单数形式(res.partnersale.order 而不是 res.partnerSsaleS.orderS

    • 定义 Odoo Transient(向导)时:使用 <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, …

    • 定义 report 模型(SQL 视图等)时:根据 Transient 约定使用 <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 and 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()”。

  • 在模型属性顺序中应该是
    1. 私有属性(_name, _description, _inherit,…)

    2. 默认方法和``default_get``

    3. 现场申报

    4. SQL 约束和索引

    5. 计算、逆向和搜索方法的顺序与字段声明的顺序相同

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

    7. 约束方法 (@api.constrains) and onchange methods (@api.onchange)

    8. CRUD 方法(ORM 覆盖)

    9. 行动方法

    10. 最后是其他商业方法。

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 插件对于如何构建各种文件有一些约定。我们在这里更详细地解释了网络资产应该如何组织。

首先要知道的是,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文件

      • 静态/字体

      • 静态/img

      • 静态/src/js

        • static/src/js/tours:最终用户游览文件(教程,而非测试)

      • static/src/scss:scss 文件

      • static/src/xml: 所有将在 JS 中渲染的 qweb 模板

    • static/tests:这是我们放置所有测试相关文件的地方。

      • static/tests/tours:这是我们放置所有游览测试文件(不是教程)的地方。

JavaScript 编码指南

  • 建议所有 javascript 文件使用“use strict;

  • 使用 linter(jshint,…)

  • 切勿添加缩小的 Javascript 库

  • 使用 Pascal 大小写进行类声明

github wiki 中详细介绍了更精确的 JS 指南。您还可以通过查看 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;
}
  • 四 (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 开始,以装饰规则结束(fontfilter 等)。

Scoped SCSS variablesCSS variables 必须放置在最顶部,后跟一个空行将它们与其他声明分开。

.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 模块)。
此规则的唯一例外是 Web 客户端:它仅使用 o_ 前缀。

避免创建超特定的类和变量名。命名嵌套元素时,选择“孙子”方法。

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-

    前缀。

  • [root]

    组件**或**模块名称(组件优先)。

  • [element]

    内部元素的可选标识符。

  • [property]

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

  • [modifier]

    可选修饰符。

Example

$o-block-color: value;
$o-block-title-color: value;
$o-block-title-color-hover: value;

SCSS 变量(作用域)

这些变量在块内声明,并且不能从外部访问。我们的标准约定是 $-[variable name]

Example

.o_element {
   $-inner-gap: compute-something;

   margin-right: $-inner-gap;

   .o_element_child {
      margin-right: $-inner-gap * 0.5;
   }
}

SCSS 混入和函数

我们的标准约定是 o-[name]。使用描述性名称。命名函数时,请使用命令形式的动词(例如:getmakeapply…)。

将可选参数命名为 scoped variables form,即 $-[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],其中:

  • [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 的 UI 中**不使用**的技术。这种做法通常用于全局访问和修改 CSS 变量。我们使用 SCSS 来执行此操作。

此规则的例外情况应该相当明显,例如跨捆绑包共享的模板需要一定程度的上下文感知才能正确呈现。