本页介绍Odoo编码指南。这些旨在改善 Odoo应用程序代码的质量。事实上,适当的代码可以提高可读性,简化 维护,帮助调试,降低复杂性并提高可靠性。 这些准则应适用于每个新模块和所有新开发。
警告
在稳定版本中修改现有文件时,原始文件样式 严格取代任何其他风格准则。换句话说,请不要永远 修改现有文件以应用这些准则。它避免了中断 代码行的修订历史记录。差异应保持最小。查看更多 有关详细信息,请参阅我们的拉取请求指南。
警告
在主(开发)版本中修改现有文件时,应用这些 仅针对已修改代码的现有代码的指南,或者如果文件的大部分内容是 正在修订中。换言之,仅当现有文件结构符合以下条件时,才修改现有文件结构 正在发生重大变化。在这种情况下,首先执行移动提交,然后应用 与功能相关的更改。
模块被组织在重要的目录中。它们包含业务逻辑; 看一看它们应该会让你理解这个模块的目的。
data/ : 演示和数据 XML
models/ : 模型定义
controllers/ : 包含控制器(HTTP 路由)
views/ :包含视图和模板
static/ :包含 Web 资产,分为 css/、js/、img/、lib/、…
其他可选目录组成该模块。
Wizard/ :重新组合瞬态模型 () 及其视图models.TransientModel
report/ :包含基于SQL视图的可打印报表和模型。Python 对象和 XML 视图包含在此目录中
tests/ :包含 Python 测试用例
文件命名对于通过所有odoo插件快速查找信息非常重要。 本节介绍如何在标准odoo模块中命名文件。作为 例如,我们使用植物苗圃应用程序。 它包含两个主要模型 plant.nursery 和 plant.order。
关于模型,将业务逻辑拆分为属于 相同的主模型。每个集合都位于一个给定的文件中,该文件根据其主模型命名。 如果只有一个模型,则其名称与模块名称相同。每 继承的模型应位于其自己的文件中,以帮助理解受影响的模型 模型。
addons/plant_nursery/
|-- models/
| |-- plant_nursery.py (first main model)
| |-- plant_order.py (another main model)
| |-- res_partner.py (inherited Odoo model)
关于安全性,应使用三个主要文件:
第一个是在ir.model.access.csv文件中完成的访问权限定义。
用户组定义在model(命令的模型)_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_menu .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模型相同的全局逻辑。每个组件都应该有自己的文件,并具有有意义的名称。例如,活动小部件位于邮件模块的activity.js中。还可以创建子目录来构建“包”(更多详细信息请参阅Web模块)。相同的逻辑应该应用于JS小部件的模板(静态XML文件)及其样式(scss文件)。不要链接Odoo以外的数据(图像,库):不要使用图像的URL,而是将其复制到代码库中。
关于向导,命名约定与Python模型的命名约定相同:<transient>.py和<transient>_views.xml。两者都放在向导(wizard)目录中。这种命名来自旧版odoo应用程序,它使用向导关键字表示临时模型。
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
注意
警告
要在XML中声明记录,建议使用记录符号(使用):
将id属性放在模型之前
对于字段声明,name属性是首选。然后,将值放在字段标签中,或者放在eval属性中,最后按重要性排序其他属性(widget,options等)。
尽量按模型分组记录。在action/menu/views之间存在依赖关系的情况下,此约定可能不适用。
使用下一点定义的命名约定
<data>标签仅用于设置不可更新的数据,带有noupdate=1。如果文件中只有不可更新的数据,可以将noupdate=1设置为<odoo>标记,并且不设置<data>标记。
若要在 XML 中声明记录,建议使用记录表示法(使用 <record>):
<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">
<tree>
<field name="my_field_1"/>
<field name="my_field_2" string="My Label" widget="statusbar" statusbar_visible="draft,sent,progress,done" />
</tree>
</field>
</record>
Odoo支持自定义标签作为语法糖:
menuitem:将其用作声明ir.ui.menu的快捷方式
template:使用它来声明只需要视图的arch部分的QWeb视图。
这些标签优于record标签。
使用以下模式:
对于菜单(menu):<model_name>_menu,或对于子菜单使用<model_name>_menu_do_stuff。
对于视图(view):<model_name>_view_<view_type>,其中view_type是kanban,form,tree,search等。
对于操作(Action):主要操作使用<model_name>_action。其他操作以 _<detail>为后缀,其中detail是一个简短说明操作的字符串。这仅在为模型声明多个操作时使用。
对于窗口操作(window actions):操作名称后缀为特定视图信息,如<model_name>_action_view_<view_type>。
对于组(group):<module_name>_group_<group_name>,其中group_name是组的名称,通常为’user’,'manager’等。
对于规则(rule):<model_name>_rule_<concerned_group>,其中concerned_group是相关组的简短名称(对于’model_name_group_user’使用’user’,对于公共用户使用’public’,对于多公司规则使用’company’等)。
xml 的name名称应与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 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>
警告
使用linter可以帮助显示语法和语义警告或错误。Odoo源代码试图遵守Python标准,但其中一些可以忽略。
E501:行太长
E301: 预期 1 个空白行,找到 0 个
E302:预期 2 个空白行,找到 1 个
Imports按以下方式排序
外部库(每行一个,按python标准库排序和拆分)
odoo的导入
从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
总是保证可读性,而不是简洁性或使用语言特征或习语。
不要使用 .clone()
# 不好的写法
new_dict = my_dict.clone()
new_list = old_list.clone()
# 正确写法
new_dict = dict(my_dict)
new_list = list(old_list)
# -- 创建空字典
my_dict = {}
my_dict2 = dict()
# -- 字典赋值
# 不好的写法
my_dict = {}
my_dict['foo'] = 3
my_dict['bar'] = 4
# 正确写法
my_dict = {'foo': 3, 'bar': 4}
# -- 更新字典
# 不好的写法
my_dict['foo'] = 3
my_dict['bar'] = 4
my_dict['baz'] = 5
# 正确的写法
my_dict.update(foo=3, bar=4, baz=5)
my_dict = dict(my_dict, **my_dict2)
# 多余的写法
schema = kw['schema']
params = {'schema': schema}
# 简洁的写法
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) # 非常冗余
value = my_dict.get('key') # 正确写法
此外,如果 my_dict 中的“key”和 my_dict.get(‘key’) 具有非常不同的含义,请确保您使用了正确的那个。
# 不是很好的写法
cube = []
for i in res:
cube.append((i['id'],i['name']))
# 更好的写法
cube = [(i['id'], i['name']) for i in res]
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..."
# 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 API提供的那些。
与python一样,使用过滤、映射、排序等方法来简化代码阅读和提高性能。
上下文是一个不能修改的 frozendict。要使用不同的上下文调用方法,应使用 with_context 方法。
# 所有上下文都被替换
records.with_context(new_context).do_stuff()
# 附加的上下文值覆盖本地上下文值
records.with_context(**additionnal_context).do_other_stuff()
警告
在上下文中传递参数可能会产生危险的副作用。
由于值是自动传播的,因此可能会出现一些意想不到的行为。在上下文中调用带有default_my_field键的模型的create()方法将为相关模型设置my_field的默认值。但是,如果在创建过程中,其他对象(例如sale.order.
order)。排队,特价。创建一个字段名为my_field的订单,它们的默认值也将被设置。
如果您需要创建一个影响对象行为的键上下文,请选择一个好名称,并最终通过模块名称前缀来隔离其影响。邮件(mail)模块的键就是一个很好的例子:mail_create_nosubscribe、mail_notrack、mail_notify_user_signature等。
函数和方法不应包含太多的逻辑:拥有许多小而简单的方法比拥有少数大而复杂的方法更可取。一个好的经验法则是,一旦一个方法有多个职责,就应立即拆分它。
应避免在方法中硬编码业务逻辑,因为它会阻止子模块轻松扩展。
Coding guidelines
This page introduces the Odoo Coding Guidelines. Those aim to improve the quality of Odoo Apps code. Indeed proper code improves readability, eases maintenance, helps debugging, lowers complexity and promotes reliability. These guidelines should be applied to every new module and to all new development.
Warning
When modifying existing files in stable version the original file style strictly supersedes any other style guidelines. In other words please never modify existing files in order to apply these guidelines. It avoids disrupting the revision history of code lines. Diff should be kept minimal. For more details, see our pull request guide.
Warning
When modifying existing files in master (development) version apply those guidelines to existing code only for modified code or if most of the file is under revision. In other words modify existing files structure only if it is going under major changes. In that case first do a move commit then apply the changes related to the feature.
Module structure
Directories
A module is organized in important directories. Those contain the business logic; having a look at them should make you understand the purpose of the module.
data/ : demo and data xml
models/ : models definition
controllers/ : contains controllers (HTTP routes)
views/ : contains the views and templates
static/ : contains the web assets, separated into css/, js/, img/, lib/, …
Other optional directories compose the module.
wizard/ : regroups the transient models (models.TransientModel) and their views
report/ : contains the printable reports and models based on SQL views. Python objects and XML views are included in this directory
tests/ : contains the Python tests
File naming
File naming is important to quickly find information through all odoo addons. This section explains how to name files in a standard odoo module. As an example we use a plant nursery application. It holds two main models plant.nursery and plant.order.
Concerning models, split the business logic by sets of models belonging to a same main model. Each set lies in a given file named based on its main model. If there is only one model, its name is the same as the module name. Each inherited model should be in its own file to help understanding of impacted models.
addons/plant_nursery/
|-- models/
| |-- plant_nursery.py (first main model)
| |-- plant_order.py (another main model)
| |-- res_partner.py (inherited Odoo model)
Concerning security, three main files should be used:
First one is the definition of access rights done in a ir.model.access.csv file.
User groups are defined in <module>_groups.xml.
Record rules are defined in <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
Concerning controllers, generally all controllers belong to a single controller contained in a file named <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)
Concerning static files, Javascript files follow globally the same logic as python models. Each component should be in its own file with a meaningful name. For instance, the activity widgets are located in activity.js of mail module. Subdirectories can also be created to structure the ‘package’ (see web module for more details). The same logic should be applied for the templates of JS widgets (static XML files) and for their styles (scss files). Don’t link data (image, libraries) outside Odoo: do not use an URL to an image but copy it in the codebase instead.
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
Concerning statistics reports done with python / SQL views and classic views naming is the following :
addons/plant_nursery/
|-- report/
| |-- plant_order_report.py
| |-- plant_order_report_views.xml
Concerning printable reports which contain mainly data preparation and Qweb templates naming is the following :
addons/plant_nursery/
|-- report/
| |-- plant_order_reports.xml (report actions, paperformat, ...)
| |-- plant_order_templates.xml (xml report templates)
The complete tree of our Odoo module therefore looks like
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
Note
File names should only contain [a-z0-9_] (lowercase alphanumerics and _)
Warning
Use correct file permissions : folder 755 and file 644.
XML files
Format
To declare a record in XML, the record notation (using <record>) is recommended:
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.
Try to group the record by model. In case of dependencies between action/menu/views, this convention may not be applicable.
Use naming convention defined at the next point
The tag <data> is only used to set not-updatable data with 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> tag.
<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">
<tree>
<field name="my_field_1"/>
<field name="my_field_2" string="My Label" widget="statusbar" statusbar_visible="draft,sent,progress,done" />
</tree>
</field>
</record>
Odoo supports custom tags acting as syntactic sugar:
menuitem: use it as a shortcut to declare a ir.ui.menu
template: use it to declare a QWeb View requiring only the arch section of the view.
These tags are preferred over the record notation.
XML IDs and naming
Security, View and Action
Use the following pattern :
For a menu: <model_name>_menu, or <model_name>_menu_do_stuff for submenus.
For a view: <model_name>_view_<view_type>, where view_type is kanban, form, tree, search, …
For an action: the main action respects <model_name>_action. Others are suffixed with _<detail>, where detail is a lowercase string briefly explaining the action. This is used only if multiple actions are declared for the model.
For window actions: suffix the action name by the specific view information like <model_name>_action_view_<view_type>.
For a group: <module_name>_group_<group_name> where group_name is the name of the group, generally ‘user’, ‘manager’, …
For a rule: <model_name>_rule_<concerned_group> where concerned_group is the short name of the concerned group (‘user’ for the ‘model_name_group_user’, ‘public’ for public user, ‘company’ for multi-company rules, …).
Name should be identical to xml id with dots replacing underscores. Actions should have a real naming as it is used as display name.
<!-- 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>
Inheriting XML
Xml Ids of inheriting views should use the same ID as the original record. It helps finding all inheritance at a glance. As final Xml Ids are prefixed by the module that creates them there is no overlap.
Naming should contain an .inherit.{details} suffix to ease understanding the override purpose when looking at its name.
<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>
New primary views do not require the inherit suffix as those are new records based upon the first one.
<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
Warning
Do not forget to read the Security Pitfalls section as well to write secure code.
PEP8 options
Using a linter can help show syntax and semantic warnings or errors. Odoo source code tries to respect Python standard, but some of them can be ignored.
E501: line too long
E301: expected 1 blank line, found 0
E302: expected 2 blank lines, found 1
Imports
The imports are ordered as
External libraries (one per line sorted and split in python stdlib)
Imports of odoo
Imports from Odoo modules (rarely, and only if necessary)
Inside these 3 groups, the imported lines are alphabetically sorted.
# 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
Idiomatics of Programming (Python)
Always favor readability over conciseness or using the language features or idioms.
Don’t use .clone()
# bad
new_dict = my_dict.clone()
new_list = old_list.clone()
# good
new_dict = dict(my_dict)
new_list = list(old_list)
Python dictionary : creation and update
# -- 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)
Use meaningful variable/class/method names
Useless variable : Temporary variables can make the code clearer by giving names to objects, but that doesn’t mean you should create temporary variables all the time:
# pointless
schema = kw['schema']
params = {'schema': schema}
# simpler
params = {'schema': kw['schema']}
Multiple return points are OK, when they’re simpler
# 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
Know your builtins : You should at least have a basic understanding of all the Python builtins (http://docs.python.org/library/functions.html)
value = my_dict.get('key', None) # very very redundant
value = my_dict.get('key') # good
Also, if 'key' in my_dict and if my_dict.get('key') have very different meaning, be sure that you’re using the right one.
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]
Collections are booleans too : In python, many objects have “boolean-ish” value when evaluated in a boolean context (such as an if). Among these are collections (lists, dicts, sets, …) which are “falsy” when empty and “truthy” when containing items:
bool([]) is False
bool([1]) is True
bool([False]) is True
So, you can write if some_collection: instead of if len(some_collection):.
Iterate on iterables
# 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..."
Use 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)
As a good developer, document your code (docstring on methods, simple comments for tricky part of code)
In additions to these guidelines, you may also find the following link interesting: http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html (a little bit outdated, but quite relevant)
Programming in Odoo
Avoid to create generators and decorators: only use the ones provided by the Odoo API.
As in python, use filtered, mapped, sorted, … methods to ease code reading and performance.
Propagate the context
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
Warning
Passing parameter in context can have dangerous side-effects.
Since the values are propagated automatically, some unexpected behavior may appear. Calling create() method of a model with default_my_field key in context will set the default value of my_field for the concerned model. But if during this creation, other objects (such as sale.order.line, on sale.order creation) having a field name my_field are created, their default value will be set too.
If you need to create a key context influencing the behavior of some object, choose a good name, and eventually prefix it by the name of the module to isolate its impact. A good example are the keys of mail module : mail_create_nosubscribe, mail_notrack, mail_notify_user_signature, …
Think extendable
Functions and methods should not contain too much logic: having a lot of small and simple methods is more advisable than having few large and complex methods. A good rule of thumb is to split a method as soon as it has more than one responsibility (see http://en.wikipedia.org/wiki/Single_responsibility_principle).
Hardcoding a business logic in a method should be avoided as it prevents to be easily extended by a submodule.
# 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')
上面的代码为了示例而过度扩展,但必须考虑可读性,并且必须进行权衡。
此外,相应地命名您的函数:命名简短且适当的函数是可读/可维护代码和更紧密文档的起点。
此建议也适用于类、文件、模块和包。
Odoo框架负责为所有RPC调用提供事务上下文。原则是在每次RPC调用开始时打开一个新的数据库游标,并在调用返回时提交,就在将答案传输到RPC客户端之前,大致如下:
def execute(self, db_name, uid, obj, method, *args, **kw):
db, pool = pooler.get_db_and_pool(db_name)
# create transaction cursor
cr = db.cursor()
try:
res = pool.execute_cr(cr, uid, obj, method, *args, **kw)
cr.commit() # all good, we commit
except Exception:
cr.rollback() # error, rollback everything atomically
raise
finally:
cr.close() # always close cursor opened manually
return res
如果在执行RPC调用期间发生任何错误,事务将自动回滚,从而保留系统的状态。
同样,该系统还在测试套件执行期间提供专用事务,因此可以根据服务器启动选项进行回滚或不回滚。
结果是,如果你在任何地方手动调用 cr.commit(),你很有可能会以各种方式破坏系统,因为你会导致部分提交,从而导致部分和不干净的回滚,除其他外,还包括:
业务数据不一致,通常会丢失数据
工作流不同步,文件永久卡住
无法干净地回滚的测试,并且会开始污染数据库并触发错误(即使在事务期间没有发生错误也是如此)
这里有一个非常简单的规则:
除非您明确创建了自己的数据库游标,否则您不应该自己调用 cr.commit(),而且需要这样做的情况是例外!
顺便说一下,如果你创建了自己的游标,那么你需要处理错误情况和适当的回滚,以及在完成游标时正确关闭它。
与普遍看法相反,在以下情况下,你甚至不需要调用 cr.commit(): - 在 models.Model 对象的 _auto_init() 方法中:这由 addons 初始化方法或 ORM 事务在创建自定义模型时处理 - 在报告中:commit() 也由框架处理,因此即使在报告中也可以更新数据库 - 在 models.Transient 方法中:这些方法与常规 models.Model 方法完全相同,在事务中调用,并在最后使用相应的 cr.commit()/rollback() 等。(如果你有疑问,请参阅上面的通用规则!)
从现在开始,服务器框架之外的所有 cr.commit() 调用都必须有一个明确的注释,解释为什么它们是绝对必要的,为什么它们确实是正确的,为什么它们不会破坏事务。否则,它们可以而且将会被删除!
Odoo使用一个名为“underscore”_()的类似gettext的方法来指示代码中使用的静态字符串需要在运行时使用上下文的语言进行翻译。这个伪方法可以在你的代码中通过导入如下代码来访问:
from odoo import _
在使用它时必须遵循一些非常重要的规则,以便它能够正常工作,并避免在翻译中填充无用的垃圾。
基本上,这种方法只应用于在代码中手动编写的静态字符串,它不能用于翻译字段值,如产品名称等。必须使用相应字段上的翻译标记来完成。
该方法接受可选的位置参数或命名参数。规则非常简单:对下划线方法的调用应始终采用’触点名称.文本’的形式,而不能采用其他形式
# 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) 而不是 position(当必须替换多个变量时)。这使得社区翻译人员更容易进行翻译。
模型名(使用点表示法,以模块名作为前缀):
定义Odoo模型时:使用名称的单数形式(res.partner和sale.order,而不是res.partnerS和saleS.orderS)
定义Odoo临时对象(向导)时:使用<related_base_model.<action>,其中related_base_model是与临时对象相关的基本模型(在models/中定义),action是临时对象操作的简写。避免使用向导字。例如:account.invoice.make,project.task.delegate.batch,…
当定义报表模型(SQL视图等)时:使用<related_base_model>.report.<action>,基于Transient约定。
Odoo Python类:使用驼峰命名法(面向对象风格)。
class AccountInvoice(models.Model):
...
Partner = self.env['res.partner']
partners = Partner.browse(ids)
partner_id = partners[0].id
One2Many和Many2Many字段应该始终以_id作为后缀(例如:sale_order_line_ids)
Many2One字段应该以_id作为后缀(例如:partner_id, user_id,…)
方法约定
在Model属性中的顺序应该是
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):
...
Odoo插件对如何构建各种文件有一些约定。我们在这里更详细地解释了如何组织Web资源。
首先要知道的是,Odoo服务器将(静态地)提供位于static/文件夹中的所有文件,但前缀为插件名称。因此,例如,如果文件位于adds/web/static/src/js/some_file.js中,则可以在url your-odoo-server.com/web/static/src/js/some_file.js中静态访问该文件。
惯例是按照以下结构组织代码:
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;
}
CSS
.o_foo, .o_foo_bar, .o_baz {
height: 32px;
}
.o_foo .o_quux, .o_foo_bar .o_quux, .o_baz .o_qux {
height: 16px;
}
.o_corge {
background: #EAEAEA;
}
四(4)个空格缩进,没有标签;
最多80个字符宽的列;
左大括号 ({): 最后的选择器后面的空白;
闭括号(}):放在新的一行;
每份声明用一行;
有意义地使用空格。
建议的 Stylelint 设置
"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",
}
},
从“外部”开始,从位置开始,以装饰规则(字体、过滤器等)结束。
作用域 SCSS 变量和 CSS 变量必须放在最顶部,后面是空行,将它们与其他声明隔开。
.o_element {
$-inner-gap: $border-width + $legend-margin-bottom;
--element-margin: 1rem;
--element-size: 3rem;
@include o-position-absolute(1rem);
display: block;
margin: var(--element-margin);
width: calc(var(--element-size) + #{$-inner-gap});
border: 0;
padding: 1rem;
background: blue;
font-size: 1rem;
filter: blur(2px);
}
CSS中的命名约定在使代码更严格、透明和信息丰富方面非常有用。
避免使用id选择器,并将你的类前缀为o_<module_name>,其中<module_name>是模块的技术名称(sale、im_chat等)或模块保留的主路由(主要用于网站模块,例如:网站论坛模块的o_forum)。
这个规则的唯一例外是Web客户端:它只使用o_前缀。
避免创建过于具体的类和变量名。在命名嵌套元素时,选择“子孙”方法。
例子
反面例子
<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发生更改时重命名的需要。
我们的标准约定是:$o-[root]-[element]-[property]-[modifier]
$o-
前缀。
[root]
组件或模块名称(组件优先)。
[element]
内部元素的可选标识符。
[property]
变量定义的属性/行为。
[modifier]
可选修饰符。
例子
$o-block-color: value;
$o-block-title-color: value;
$o-block-title-color-hover: value;
这些变量在块内声明,无法从外部访问。 我们的标准惯例是 .$-[variable name]
例子
.o_element {
$-inner-gap: compute-something;
margin-right: $-inner-gap;
.o_element_child {
margin-right: $-inner-gap * 0.5;
}
}
我们的标准约定是o-[name]。使用描述性的名字。在命名功能时,使用祈使句形式的动词(例如:get, make, apply…)。
以作用域变量的形式命名可选参数,如$-[argument]。
例子
@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);
}
在Odoo中,CSS变量的使用与DOM严格相关。使用它们来上下文地适应设计和布局。
我们的标准约定是BEM,所以–[root]__[element]-[property]–[modifier],其中:
[root]
组件或模块名称(组件优先)。
[element]
可选的内部元素标识符。
[property]
由变量定义的属性/行为。
[modifier]
可选的修饰符。
例子
.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;
}
在Odoo中,CSS变量的使用与DOM密切相关,这意味着它们用于根据上下文调整设计和布局,而不是管理全局设计系统。这些通常在组件的属性在特定上下文或其他情况下可能发生变化时使用。
我们在组件的主块中定义这些属性,提供默认回退。
例子
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变量的行为却截然不同。主要区别在于,SCSS变量是命令式的,并且会被编译掉,而CSS变量是声明式的,并包含在最终输出中。
在Odoo中,我们采用了两全其美的做法:使用SCSS变量来定义设计系统,同时在上下文适应方面选择CSS变量。
通过添加SCSS变量来改进前一个示例的实现,以便在顶级获得控制权并确保与其他组件的一致性。
例子
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 伪类上定义 CSS 变量是我们通常不会在 Odoo 的 UI 中使用的一种技术。这种做法通常用于全局访问和修改 CSS 变量。我们使用 SCSS 来执行此操作。
此规则的例外情况应该相当明显,例如在需要一定程度的上下文感知才能正确呈现的包之间共享的模板。