编码规范

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

警告

当修改 稳定版本 中的现有文件时,原始文件的样式严格优先于任何其他样式指南。换句话说,请不要修改现有文件以应用这些指南。这样可以避免破坏代码行的修订历史。差异应保持最小。有关更多详细信息,请参阅我们的 拉取请求指南 <https://odoo.com/submit-pr> _。

警告

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

模块结构

目录

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

  • data/ : 演示和数据 xml

  • models/ : 模型定义

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

  • views/ :包含视图和模板

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

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

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

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

  • tests/ :包含Python测试

文件命名

文件命名对于快速通过所有odoo插件找到信息非常重要。本节介绍了如何在标准odoo模块中命名文件。我们以一个 植物苗圃<https://github.com/tivisse/odoodays-2018/tree/master/plant_nursery> _应用为例。它包含两个主要模型 plant.nurseryplant.order

关于 models,将业务逻辑按照属于同一个主模型的模型集合进行拆分。每个集合位于一个以其主模型命名的给定文件中。如果只有一个模型,则其名称与模块名称相同。每个继承的模型应该在自己的文件中,以帮助理解受影响的模型。

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

关于 data,按目的(演示或数据)和主模型进行拆分。文件名将是主模型名称后缀为 _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 模型基本相同。每个组件都应该有自己的文件,文件名有意义。例如,活动小部件位于邮件模块的 activity.js 中。还可以创建子目录来组织“包”(有关更多详细信息,请参见 web 模块)。对于 JS 小部件的模板(静态 XML 文件)和样式(scss 文件),应该采用相同的逻辑。不要链接 Odoo 外部的数据(图像、库),而是将其复制到代码库中。

关于 wizards ,命名约定与 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> ):

  • model 之前放置 id 属性

  • 在字段声明中,首先是 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 视图。

这些标签优先于 记录 符号。

XML ID和命名

安全、视图和操作

请使用以下模式:

  • 对于菜单: <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>

新的主视图不需要使用inherit后缀,因为它们是基于第一个记录的新记录。

<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. Imports of 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 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]
  • Collections are booleans too : 在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)
  • 作为一名优秀的开发者,应该为你的代码编写文档(方法的文档字符串,对于代码中棘手的部分编写简单注释)。

  • 除了这些指南,您还可能会发现以下链接很有趣:http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html(有点过时,但仍然相关)

在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

警告

在上下文中传递参数可能会产生危险的副作用。

由于值是自动传播的,可能会出现一些意外的行为。在上下文中使用 default_my_field 键调用模型的 create() 方法将为相关模型设置 my_field 的默认值。但是,如果在此创建期间创建了其他对象(例如,在销售订单创建时创建了销售订单行),它们的字段名称为 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() ,很有可能会以各种方式破坏系统,因为您将导致部分提交,从而导致部分和不干净的回滚,其中包括:

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

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

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

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

你应该 永远不要 自己调用 cr.commit()除非 你明确地创建了自己的数据库游标!而且需要这样做的情况是异常的!

顺便提一下,如果您创建了自己的游标,则需要处理错误情况和适当的回滚,并在完成后正确关闭游标。

与普遍观点相反,在以下情况下,您甚至不需要调用 cr.commit() :- 在 models.Model 对象的 _auto_init() 方法中:这由插件初始化方法或创建自定义模型时的 ORM 事务处理,- 在报告中: commit() 也由框架处理,因此您甚至可以从报告中更新数据库- 在 models.Transient 方法中:这些方法与常规的 models.Model 方法完全相同,在事务中调用,并在结尾处使用相应的 cr.commit()/rollback() - 等等(如果您有疑问,请参见上面的一般规则!)

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

正确使用翻译方法

Odoo uses a GetText-like method named “underscore” _() to indicate that a static string used in the code needs to be translated at runtime. That method is available at self.env._ using the language of the environment.

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

基本上,这种方法只应用于在代码中手动编写的静态字符串,它无法用于翻译字段值,例如产品名称等。必须使用相应字段上的翻译标志来代替。

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.partnerSsaleS.orderS

    • 当定义一个Odoo Transient(向导)时:使用 <related_base_model>.<action> ,其中 related_base_model 是与该transient相关的基本模型(在 models/ 中定义), action 是transient所做的操作的简称。避免使用 wizard 这个词。例如: account.invoice.makeproject.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_<field_name>

    • 搜索方法:搜索方法模式为 _search_<field_name>

    • 默认方法:默认方法模式是 _default_<field_name>

    • 选择方法:选择方法模式为 _selection_<field_name>

    • Onchange 方法:onchange 方法的模式是 _onchange_<field_name>

    • 约束方法:约束方法模式为 _check_<constraint_name>

    • 动作方法:对象的动作方法以 action_ 为前缀。由于它只使用一条记录,在方法的开头添加 self.ensure_one()

  • 在模型属性中,顺序应该是
    1. Private attributes (_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: this is the place where js libs should be located, in a sub folder. So, for example, all files from the jquery library are in 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 files

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

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

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

Javascript 编码指南

  • 所有 JavaScript 文件都建议使用 use strict;

  • 使用代码检查工具 (jshint, …)

  • 永远不要添加压缩过的 JavaScript 库

  • 类声明使用驼峰命名法

更详细的JS指南详见 github wiki。您也可以查看现有的Javascript API,通过查看Javascript参考文档。

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个字符宽;

  • opening brace ({): 选择器后面的空格;

  • 闭合大括号 (}):独立一行;

  • 每个声明占一行;

  • 有意义地使用空格。

"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",
    }
},

属性顺序

Order properties from the “outside” in, starting from position and ending with decorative rules (font, filter, etc.).

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中的命名约定非常有用,可以使您的代码更加严格、透明和信息丰富。

Avoid id selectors, and prefix your classes with o_<module_name>, where <module_name> is the technical name of the module (sale, im_chat, …) or the main route reserved by the module (for website modules mainly, i.e. : o_forum for the website_forum module).
唯一的例外是 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-

    前缀。

  • [root]

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

  • [element]

    内部元素的可选标识符。

  • [property]

    变量定义的属性/行为。

  • [modifier]

    一个可选的修饰符。

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 …)。

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

在Odoo的用户界面中,我们通常 不使用:root 伪类上定义CSS变量的技术。这种做法通常用于全局访问和修改CSS变量。我们使用SCSS来执行此操作。

这个规则的例外应该是相当明显的,比如跨 bundle 共享的模板需要一定程度的上下文意识才能正确渲染。