Odoo代码开发规范指南
构建强大稳健的Odoo应用程序,优化及完善代码规范化问题
本文介绍 Odoo 的代码指南。旨在提升应用代码的质量。适当的编码提升可读性、缓解可维护性、有助于调试工、降低复杂度并提升可靠性。这些指南应当用于每个新模块及新的开发。

模块组织在重要的目录中。包含业务逻辑;查看一下应当会了解模块的用途。

  • data/ : 演示和数据xml
  • models/ : 模型定义
  • controllers/ : 包含控制器(HTTP 路由)
  • views/ : 包含视图和模板
  • static/ : 包含网页资源,分离为 css/, js/, img/, lib/, …

其它组成模块的可选目录。

  • wizard/ : 重组临时模型(models.TransientModel) 及其视图
  • report/ : 包含基于SQL视图的可打印报表和模型。Python对象和XML视图也在该目录中
  • tests/ : 包含Python测试类

文件命名对于在odoo插件中快速查找信息非常重要。本节讲解如何在标准odoo模块中进行文件的命名。作为示例我们使用 育苗 应用。它包含两个主要模型plant.nursery 和 plant.order

有关models, 通过属于相同主模型的模型组来分离逻辑。每组处于基于主模型命名的给定文件中。如果仅有一个模型,其名称与模块名相同。每个继承模型应放在自己的文件中来有助于理解所受影响的模型。

有关security 和访问权限及规则时应使用两个主要文件。第一个是访问权限应在 ir.model.access.csv 文件中进行定义。用户组在<module>_groups.xml中进行定义。访问规则在 <model>_security.xml中定义。

有关 views, 后台视图应类似模型进行分离并由_views.xml进行后缀。后台视图有列表、表单、看板、活动、图表、透视表… 视图。 要缓解视图中按模型的分离不关联具体动作的主菜单可提取为可选的 <module>_menus.xml 文件。模板 (QWeb页面主要用于门户/网站展示)和资源包 (JS 和 CSS资源的导入) 放在单独的文件中。它们分别为<model>_templates.xml 和 assets.xml 文件。

有关data, 按照用途(演示或数据)和主模型分离它们。用户名将为由_demo.xml 或 _data.xml后缀的main_model名称。例如具有带有演示和数据的应用,针对与mail模型相关的主模型及子类型、活动和邮件模板:

有关controllers, 通常属于单个控制器的所有控制器包含在一个名为<module_name>.py的文件中。Odoo中老的惯例是将该文件命名为main.py ,但现在认为已过时。如果需要从另一个模块中继承已有控制器应在 <inherited_module_name>.py中进行。例如,在应用中添加门户控制器在portal.py中完成。

有关静态文件, Javascript文件全局遵循与python模型相同的逻辑。每个组件应使用一个有意义的名称放在自己的文件中。 例如,活动控件位于mail模块的activity.js 中。也可以创建子目录来架构‘package’ (参见web模块获取更多详情)。相同的逻辑应对JS控件的模板(静态XML文件)及它们的样式(scss文件)进行应用。不要在Odoo之外链接数据 (图像、库):不要使用图片的URL而又在基代码中拷贝它。

有关向导, 命名惯例与python模型相同: <transient>.py 和 <transient>_views.xml。两者都放在wizard目录中。来自老的odoo应用的w 命名对临时模型使用wizard关键字。

有关统计报表 python / SQL视图和经典视图命名如下:

有关可打印报表 主要包含数据准备和Qweb模板命名如下:

因此我们的Odoo模块的完整目录树如下:

要在XML中声明记录,推荐使用 record 标记符 (使用 <record>) :

  • model前放置id属性
  • 对于字段声明,首先为name 属性。然后将值要么放到 field 标签中,要么放到eval 属性中,最终其它属性 (widget, options, …)按重要性排序。
  • 尝试通过模型对记录分组。在动作/菜单/视图之间依赖的情况下,可能无法应用这一惯例。
  • 使用在下一点中定义的命名规范
  • 标签<data>仅用于通过noupdate=1设置不可更新的数据。如果在文件中仅存在不可更新数据,可对<odoo>标签设置noupdate=1且不设置 <data> 标签。

Odoo支持自定义标签来作为语法糖:

  • menuitem: 使用它来作为声明ir.ui.menu的快捷方式
  • template: 使用它来声明仅要求视图中arch版块的 QWeb视图。
  • report: 用于声明报表动作
  • act_window: 在record标记符无法实现时使用它

前4个标签的推荐度高于 record 标记。

使用如下模式:

  • 对于菜单:<model_name>_menu, 或是什么子菜单用 <model_name>_menu_do_stuff 。
  • 对于视图: <model_name>_view_<view_type>, 其中的view_type 为 kanbanformtreesearch, …
  • 对于动作: 主动作为 <model_name>_action。其它使用_<detail>作为后缀,其中detail 为简洁地解释动作的小写字符串。仅用于多个动作对模型进行声明时。
  • 对于窗口动作: 通过具体的视图信息如 <model_name>_action_view_<view_type>对动作名进行后缀。
  • 对于组: <model_name>_group_<group_name> 其中 group_name 是组的名称,通常为‘user’, ‘manager’, …
  • 对于规则: <model_name>_rule_<concerned_group> 其中concerned_group 是相关组的短名称 (‘user’对应‘model_name_group_user’, ‘public’对应公共用户, ‘company’ 对应多租户规则, …).

名称应与xml id相同,使用点号替换下划线。动作应有一个真实命名,因为它用作显示名。

继承视图的Xml Id应使用与原记录相同的ID。它有助于一眼看到所有的继承。因最终的 Xml Id会使用所创建的模块作为前缀,所以不会产生重叠。

命名应包含一个 .inherit.{details} 后缀来有且于在查看名称时理解重载的目的。

新的主要视图不要求继承后缀,因为它们是基于第一个的新记录。

使用linter可帮助显示语法及句法警告或错误。Odoo源代码尽力遵守Python标准,但其中有一些可予以忽略。

  • E501: 行内容过长
  • E301: 应有一个空行,但无空行
  • E302: 应有两个空行,但仅有一个

导入的顺序为

  1. 外部库 (每行一个并在python stdlib中分离)
  2.  odoo的导入
  3. 来自Odoo模块的导入 (很少见,仅在需要时进行)

在以上3组中,导入的行按字母排序。

  • 每个python文件的第一行应带有# -*- coding: utf-8 -*- .
  • 易读性高于简洁性或使用语法或惯例。
  • 不要使用 .clone()
  • Python字典 : 创建及更新
  • 使用有意义的变量/类/方法名
  • 无用变量 : 临时变量可通常为对象给定名称来让代码更清晰,但那并不表示应当总是创建临时变量:
  • 多个返回点在更为简化时是OK的

同时, if 'key' in my_dict 和 if my_dict.get('key') 的含义大相径庭,确保要正确使用。

  • 学习列表推导式:使用列表推导式、字段解析式及使用 mapfiltersum, … 的基本操作。它们会让代码更易于阅读。
  • 集合也是布尔型:在python中,很多对象在布尔上下文(如if)中运行时具有“类布尔型”的值。其中有集合(列表、字典、集合…) ,在为空时为 “假”,在包含内容时为“真”:

因此可以编写 if some_collection: 来代替 if len(some_collection):.

  • 对可遍历内容进行遍历:
  • 使用 dict.setdefault
  • 避免创建生成器和装饰器:仅使用Odoo API所自带的
  • 和在python中一样,使用 filteredmappedsorted, … 方法来改善可读性、优化性能。

在添加函数时,确保其可通过遍历self的每条记录来处理多条记录。

对于性能问题,(例如)在开发‘stat 按钮’时,不要在循环中执行 search 或 search_count。推荐使用 read_group 方法来在一个请求中计算所有值。

上下文是无法修改的 frozendict 。 要通过不同的上下文调用方法,应使用 with_context 方法:

如需创建一个影响对象行为的关键上下文,选择一个好名称,并最终使用模块的名称作为前缀来隔离影响。一个好例子是mail模块的键:mail_create_nosubscribemail_notrackmail_notify_user_signature, …

在ORM可完成相应任务时不应直接使用数据库游标!这样可以传递所有的ORM功能,可能是事务、访问权限等等。

并且很有可能会让代码更难以阅读、安全性更低。

在手动使用SQL查询时要注意不要引入SQL注入漏洞。在用户输入没有正确进行过滤或引用有问题时就会出现漏洞,让攻击者可以使用预期外的SQL查询语句y (如绕过过滤或执行 UPDATE或DELETE命令)。

最好的方式是永远永远不要使用Python字符串拼接 (+) 或字符串参数插值 (%) 来传递变量到SQL查询字符串中。

第二个原因,也同样重要,决定如何格式化查询参数是数据库抽象层(psycopg2)的任务,而不是你的任务!例如psycopg2知道在你传递值列表时需要将其格式化为逗号分隔列表,使用括号包裹!

这非常重要,请注意在重构时,最重要的是不要拷贝这些模式!

以下是一个易记忆的示例, 帮助我们记住问题所在 (但不要拷贝其中的代码)。在继续之前,请确保阅读pyscopg2的在线文档在学习正确的使用方式:

函数和方法不应包含太多逻辑: 更推荐使用一些简短简单的方式,而不是使用几个大而复杂的方法。一个黄金准则是在方法有一个以上任务时尽快进行分割(参见 http://en.wikipedia.org/wiki/Single_responsibility_principle).

应避免在方法中硬编码业务逻辑,因此妨碍了轻易地通过子模块进行继承:

以上代码是出于示例是对可扩展函数进行,但必须考虑可读性并进行权衡。

同样相应地对函数命名:简短、适当命名的函数是可读/可维护代码和紧致文档的起点。

这一推荐对类、文件、模块和包也同样适用。(还可参见http://en.wikipedia.org/wiki/Cyclomatic_complexity)

Odoo框架负责为所有的RPC调用提供事务性上下文。原则是新的数据库游标在每个RPC调用的开始时打开,并在调用返回时、传送RPC客户端回复前执行,大概是这样:

如果在RPC调用执行时发生任何错误,事务会自动进行原子级回滚,保留系统状态。

相似地,系统还在测试套装执行过程中提供独立的事务,因此它可以进行回滚或不依赖于服务端启动选项。

结果是如果手动在任何地方调用 cr.commit(),大机率会将系统分割成各种方式,因为你会产生部分提交,因而有部分和不利落的回滚,导致其它问题:

  1. 不连续的业务数据,通常会有数据丢失
  2. 工作流去同步,永久文档卡死
  3. 无法利落地回滚测试,并会开始污染数据库,以及导致错误 (即使在事务中未发生错误也会这样)
以下是一些非常简单的规则:
永不自己调用call cr.commit() ,除非你显式地创建了自己的数据库游标!并非需要这么做的场景非常罕见!

顺便说一下如果你真的创建了自己的游标,那么需要处理错误用例及适当的回滚,以及在完成时恰当地关闭游标。

和通常认为的不同,你甚至不需要在以下场景中调用 cr.commit() : - 在models.Model 对象的 _auto_init() 方法中:这由插件初始化方法处理,或者在创建自定义模型由ORM事务处理; - 在报表中: commit()也由框架处理,你甚至可以在报表中更新数据库 - 在models.Transient方法中:这些方法和models.Model ones的调用完全一致,在事务中及在结束处相应的cr.commit()/rollback() - 等等(如果存在疑虑查看通用规则 !)

此后服务端框架之外的所有 cr.commit() 调用必须有显式注释说明它们绝对有必要,为什么它们确实正确,以及为什么它们没有毁坏事务。否则可以并应当删除它们!

Odoo使用了一个 GetText样式的方法,名为“下划线” _( ) ,来代码中使用的静态字符串需要在运行时使用上下文的语言翻译。这种伪方法在代码中通过如下导入进行访问:

在使用时必须遵循一些非常重要的规则来让其生效并避免使用一些无用的垃圾填充翻译。

基本上,这一方法应公用在代码中手动编写的静态字符串,它不会翻译字段值,如商品名等。这时必须要在相应字段上使用翻译标记。

规则非常简单:调用下划线方法应总是以 _('literal string') 的形式而不能是其它形式:

同时,记住翻译器要具有传递给下划线函数的字面量值才能生效,因此请尽量让它们更容易理解并保持杂散字符和格式化最小化。翻译器必须知道%s 或 %d这样的格式化模式,新行等需要被保留,但以有意义和明显地方式使用它们很重要:

通常在Odoo中, 在操作字符串时更推荐使用% 而非 .format() (在字符串中仅有一个变量供替换时),且推荐使用 %(varname) 而非位置参数 (在有多个变量供替换时)。这会让社区翻译翻译者更易于翻译。

  • 模型名 (使用点号标记符,前缀模块名) :
    •  在定义Odoo模型时:使用名称的单数形式 (res.partner 和 sale.order 来取代 res.partnerS 和 saleS.orderS)
    • 在定义Odoo 临时模型 (向导)时:  使用 <related_base_model>.<action> ,其中 related_base_model 是与临时模型相关的基模型 (在 models/中定义) ,action 是临时模型所做内容的短名称。避免使用 wizard 一词。例如: account.invoice.makeproject.task.delegate.batch, …
    • 在定义report 模型(如SQL视图) 时: 按照临时模型规范使用 <related_base_model>.report.<action>
  • Odoo Python类 : 使用驼峰(面向对象样式)。
  • 变量名 :
    • 对模型变量使用驼峰
    • 对普通变量使用下划线小写字母标记。
    • 在包含记录id或id列表时对变量名使用后缀 _id 或 _ids 。不要使用 partner_id 来包含res.partner的记录
  • One2Many 和 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. 计算、后向和搜索方法的排序与声明顺序相同
    5. 选择方法 (用于返回针对选择字段计算值的方法)
    6. 约束方法 (@api.constrains) 和 onchange方法 (@api.onchange)
    7. CRUD方法 (ORM 重载)
    8. s动作方法
    9. 最后是其它业务方法。

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文件
      • static/src/fonts
      • static/src/img
      • static/src/js
        • static/src/js/tours: 终端用户导览文件 (教程,非测试)
      • static/src/scss: scss 文件
      • static/src/xml: 所有会在JS中渲染的qweb模板
    • static/tests: 这里放置所胡测试相关的文件。
      • static/tests/tours: 这里放置所有导览测试文件 (非教程)。
  • use strict; 推荐在所有javascript文件中使用
  • 使用一种linter (jshint, …)
  • 永远不要添加最小化混淆 Javascript库
  • 对类声明使用驼峰名

更精确的JS指南在github wiki中进行详细讲解。还可以通过查看Javascript手册来查看Javascript中已有的API。

  • 对所有类添加 o_<module_name> 前缀,其中module_name 是模块的技术名称(‘sale’, ‘im_chat’, …) 或模块所保留的主路由(主要针对website模块,如‘o_forum’ 对应 website_forum 模块)。 这一规则的唯一例外是网页客户端:它仅使用o_ 前缀。
  • 避免使用id 标签
  • 使用Bootstrap原生类
  • 使用下划线小写字母标记来命名类

根据既往体验及口述传统,如下内容经过了很长时间沉淀来让提供更为有益:

  • 确保在本地git中同时定义user.email 和 user.name
  • 确保在Github个人资料中添加完整姓。请自由添加团队名、头像、喜爱的名言等等 

提交信息有4个部分: 标签、模块、短描述和完整描述。尽量让你的消息遵循如下推荐结构

标签用作提交的前缀。应当为如下内容之一

  • [FIX] 用于bug修复:多用于稳定版本但在开发版本中修复近期漏洞也同样有效;
  • [REF] 用于重构:在功能进行重度重写时;
  • [ADD] 用于添加新模块;
  • [REM] 用于删除资源:删除无用代码、删除视图、删除模块, …;
  • [REV] 用于恢复提交:如果提交导致问题或不需要使用这一标签进行恢复;
  • [MOV] 用于移动文件:使用git move并且不修改所移动文件的内容,否则Git 会丢失文件的追踪和历史信息;也在将代码从一个文件移动到另一个文件时使用;
  • [REL] 用于发行版提交;新的主版本或小稳定版本;
  • [IMP] 用于改进:大部分在开发版本中做的修改是与其它标签无关的增量改进;
  • [MERGE] 用于合并提交:在漏洞修改的包转发中使用,但在用作包含一些分离提交的功能的提交;
  • [CLA] 用于签署Odoo个人贡献者证书;
  • [I18N] 用于在翻译文件中的修改;

标签后为修改的模块名。使用技术名称作为函数名称可能会随时间改变。如果修改了一些模块,列举它们或使用多个名称来说明它是跨模块的。除非实在必要或更为简单避免在同一次提交中对多个模块修改代码。掌握模块的历史修改可能会变得困难。

在标签和模块名之后有一个有意义的提交信息头。 应当有清晰的含义并包含修改的原因。不要使用“bugfix” 或 “improvements”这样的单个单词。尽量限制头部长度为约50个字符来保持可读性。

提交消息头在拼接了if applied, this commit will <header>时应该会成为有效的句子。例如 [IMP] base: prevent to archive users linked to active partners 是正确的,因其成为了有效的兔子if applied, this commit will prevent users to archive....

在消息描述中指明修改所影响的代码部分 (模块名, lib, 切面对象, …) 及修改的描述。

首先说明为什么要修改代码。对于数十年(或3天)后回顾你的提交的人最重要的是你为什么这么做。这是修改的目的。

所做的修改可在提交本身中看到。如果包含一些技术选择,在提交信息的原因后进行说明也是一个好主意。对于 Odoo R&D 开发人员来说“PO 团队要求我这么做”并不是有效原因。

请避免同时进行影响多个模块的提交。尽量将影响不同的模块分割为不同的提交。如果需要对给定模块的修改进行恢复的话这会很有用。

在略显啰嗦时不要犹豫。大部分人只会看提交信息来根据几句话来判断你所做的所有修改。不要有任何压力。

你对功能花费了几个小时、几天或几周。花一点时间来平静下来,编写清晰、易于理解的提交信息。

如果你是Odoo R&D 开发人员,原因应当是你所处理的任务的用途。完成的详情是提交信息的核心部分。如果你执行的任务缺乏目的性和确定性请考虑在继续之前让其变得更为清晰。

最后以下是一些正确提交信息的示例:

Odoo代码开发规范指南
Odoo中文网
6 七月, 2020
存档
Odoo免费开源ERP的最强补充:供应链订单合并解决方案