odoo开发规范,囊括模块视图后端前端规范,以odoo12为基准,
-
作为全球第一的开源ERP,开源的作用就是让您根据本企业业务特性,灵活的定制,实现动态的IT系统,可随着业务变化优化您的ERP,进而优化管理运营。
对中大型企业而言,odoo必须进行开发才可落地,所以一个良好的开发规范,将更有助于团队协助,敏捷开发上线。
本文内容亦是我们进行odoo开发的内部规范,主要参考官方开发教程翻译,原文在此:
英文: https://www.sunpop.cn/documentation/12.0/reference/guidelines.html
中文: https://www.sunpop.cn/odoo_develop_guidelines_python_widget_javascript/odoo开发规范
本文介绍 Odoo 编码指南。其目的是提高 Odoo 应用程序代码的质量。事实上,正确的代码提高了可读性、简化维护、有助于调试、降低复杂性并提高可靠性。这些准则应该应用于每个新模块的开发和所有新开发。警告
在stable(稳定)版本中修改现有文件时,原始文件样式将严格取代任何其他样式准则。换句话说,请不要修改odoo正式发布的已有文件或代码,以应用这些准则。它避免了中断代码行的修订历史记录。差异应保持在最小。有关详细信息,请参阅odoo官方的 pull request guide指南。
警告
修改master(主开发)版本中的现有文件时,仅可针对 revision(正在修订时)版本中的代码或大多数文件,将这些准则应用于现有代码。换句话说,仅当现有文件结构发生重大更改时,才修改它。在这种情况下,首先执行 move commit,然后才应用与其相关的更改。
模块结构
目录及对应作用
data/:演示和数据文件
models/:模型定义
controllers/:控制器(包含HTTP路由)
views/:视图和模版
static/:web资源,包括css/, js/, img/, lib/, ...
wizard/:向导及其视图
report/:报表
tests/:单元测试代码
文件命名
业务model放置在一个文件里,如果模块只包含一个model,它的名字就与模块名一致。如:models/<main_model>.py
models/<inherited_main_model>.py
views/<main_model>_templates.xml
views/<main_model>_views.xml
data/<main_model>_demo.xml
data/<main_model>_data.xml
例如:销售模块包含sale_order和sale_order_line两个模型,并且sale_order是主模型,所以文件将被命名为models/sale_order.py和 views/sale_order_views.py。
对于数据文件命名,按用途进行命名:demo或者data。例如:data/sale_order_demo.xml和data/sale_order_data.xml
每个模块的控制器都放在一个文件中,命名为main.py。如果是从另一个模块继承的,则将其命名为<module_name>.py。与模型不同,每个控制器类应包含在一个分离的文件中。例如,采购模块在控制器/portal.py 中实现其客户门户的一部分,并在控制器/购买.py 上实现其自身的典型路由。
对于静态文件,由于资源可以在不同的上下文中使用(前端、后端、两者),因此它们将仅包含在一个捆绑包中。因此,CSS/Less、JavaScript 和 XML 文件应生捆绑一个其类型的后缀名称。
即:im_chat_common.css,用于"资产_公共"捆绑的 im_chat_common.js,以及用于"资产_后端"捆绑的 im_chat_backend.js。如果模块只拥有一个文件,则约定将<module_name>.ext(即:project.js)。不要在 Odoo 外部链接数据(图像、库):不要使用图像的 URL,而应将其复制到我们的代码库中。
关于数据,按用途拆分它们:数据或演示。文件名将是主模型名称,后缀为 _data.xml 或 _demo.xml。
关于向导,命名约定是 :
<main_transient>.py
<main_transient>_views.xml
其中<main_transient>是占主导地位的瞬态模型的名称,就像模型一样。<main_transient>.py 可以包含模型"transient_model.action"和"transient_model.action.line"。统计报表命名:
report/<report_name>_report.py
report/<report_name>_report_views.py
可打印报表:report/<print_report_name>_reports.py
report/<print_report_name>_templates.xml
完整的文件列表如下:addons/<my_module_name>/
|-- init.py
|-- manifest.py
|-- controllers/
| |-- init.py
| |-- <inherited_module_name>.py
|-- <my_module_name>.py |-- data/ | |-- <main_model>_data.xml |
-- <inherited_main_model>_demo.xml
|-- models/
| |-- init.py
| |-- <main_model>.py
|-- <inherited_main_model>.py |-- report/ | |-- __init__.py | |-- <main_stat_report_model>.py | |-- <main_stat_report_model>_views.xml | |-- <main_print_report>_reports.xml |
-- <main_print_report>_templates.xml
|-- security/
| |-- ir.model.access.csv
|-- <main_model>_security.xml |-- static/ | |-- img/ | | |-- my_little_kitten.png | |
-- troll.jpg
| |-- lib/
| |-- external_lib/ |
-- src/
| |-- js/
| | |-- <my_module_name>.js
-- <my_widget_A>.js | |-- css/ | |
-- <my_module_name>.css
| |-- scss/
| |-- <my_module_name>.scss |
-- xml/
| |-- <my_module_name>.xml
-- <my_widget_A>.xml |-- views/ | |-- <main_model>_templates.xml | |-- <main_model>_views.xml | |-- <inherited_main_model>_templates.xml |
-- <inherited_main_model>_views.xml
-- wizard/ |-- <main_transient_A>.py |-- <main_transient_A>_views.xml |-- <main_transient_B>.py
-- <main_transient_B>views.xml
文件名应仅包含 [a-z0-9] (小写字母数字和 _)警告
使用正确的文件权限 : 文件夹 755 和文件 644。
XML文件
格式
当定义一个记录的xml时,需要标记:id属性放在model属性前
字段(field)定义中,name属性放在第一个。然后,将值放在标签内,或者放在eval属性中。最后将其他属性(widget、options...)按重要性排序。
尝试按模型对记录进行分组。如果操作/菜单/视图之间存在依赖关系,则此约定可能不适用。
使用在下一章节中定义的命名约定
标签仅用于设置不可更新的数据noupdate=1,如果整个xml文件都是不可更新数据,则noupdate=1属性可以设置在标签上,而不需要标签。
view.name
object_name
Odoo支持一些自定义标签作为快捷方式:menuitem:作为ir.ui.menu的快捷方式
template: 表示只需要arch视图部分的QWeb视图
report: 用于定义报表report action
act_window:当record用不了的时候用它
我们更推荐上述4个标签类型,相对于 record 标签。
xml_id命名
权限(Security)、视图(View)和动作(Action)使用的命名规则:菜单(menu): <model_name>_menu
... ... ... ... ... ... ... 视图名称(name)使用点表示法:my.model.view_type 或者 my.model.view_type.inherit
视图(view): <model_name>view<view_type>,view_type可能的取值有:kanban, form, tree, search
动作(action): 主动作命名为<model_name>_action,其他的动作命名为<model_name>action,其中使用小写字母简单描述动作
组(group): <model_name>group<group_name>,group_name可能的取值包括:user, manager,...
规则(rule): <model_name>rule<concerned_group>,concerned_group可能的取值包括: 模型用户规则user, 公共用户规则public,多公司用户规则company继承XML的命名
继承视图的命名规则为#. Extension 扩展模式:使用与要扩展的原始视图相同的 xml id,并使用 _inherit 后缀 :如 视图view project.project_view_form 被扩展 为 project_forecast.project_view_form_inherit.
primary ... Python PEP8 选项 Odoo源代码基本准守Python标准PEP8,但是忽略其中一些规则:
#. Primary 主模式:保持原有 xml id。
...
E501:行太长了
E301:预计有1个空行,找到0
E302:预计有2个空行,找到1
E126:延长线过度缩进以用于悬挂缩进
E123:关闭支架与开口支架线的压痕不匹配
E127:延伸线过度缩进以进行视觉缩进
E128:用于视觉缩进的缩进的延续线
E265:阻止评论应以'#'开头
Import 导入
import 的顺序导入外部库
导入odoo
导入odoo的模块
在每组中的导入按字母顺序排序1 : imports of python lib
import base64
import re
import time
from datetime import datetime2 : imports of odoo
import odoo
from odoo import api, fields, models # alphabetically ordered
from odoo.tools.safe_eval import safe_eval as eval
from odoo.tools.translate import _3 : imports from odoo addons
from odoo.addons.website.models.website import slug
from odoo.addons.web.controllers.main import login_redirect
编程习惯
每个python文件都应该以 # -- coding: utf-8 -- 作为第一行。
简单易读的代码
不要使用.clone()bad
new_dict = my_dict.clone()
new_list = old_list.clone()good
new_dict = dict(my_dict)
new_list = list(old_list)
Odoo中编程
避免创建生成器和装饰器:仅使用Odoo API已有的
使用filtered, mapped, sorted, … 方法来提升代码可读性和性能。
让你的方法可以批量处理
当添加一个函数时,确保它可以处理多条数据,典型用法是通过 api.multi 装饰器,可以在self上进行循环处理@api.multi
def my_method(self)
for record in self:
record.do_cool_stuff()
避免使用 api.one 装饰器,因为它可能不会像预想中一样工作。
为了更好的性能,比如当定义一个状态按钮时,不在 api.multi 循环里用search 和 search_count 方法,要用 read_group 来实现一次请求中全部计算。@api.multi
def _compute_equipment_count(self):
""" Count the number of equipement per category """
equipment_data = self.env['hr.equipment'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id'])
mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in equipment_data])
for category in self:
category.equipment_count = mapped_data.get(category.id, 0)
context上下文环境
context是一个 frozendict 不能修改。当需要使用不同的 context 来调用一个方法时,可以通过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
在 context 中传递参数可能会产生危险的副作用。由于值是自动传播的,因此可能会出现某些不可预期行为。在 context 调用具有 default_my_field 键的模型的 create() 方法将为相关模型设置 my_field 的默认值。但是,如果固化此创建,其他对象(如 sale.order.line,在sale.order 创建中)具有一个相同字段 my_field,其默认值也将设置。如果需要创建影响某些对象行为的关键 context,请选择一个好名称,并最终按模块的名称为其前缀,以隔离其影响。一个很好的例子是 mail 模块的密钥:mail_create_nosubscribe, mail_notrack, mail_notify_user_signature, …
不要绕过ORM
当ORM可以实现的时候尽量使用ORM而不要直接写sql,因为它可能会绕过orm的一些规则如权限、事务等,还会让代码变得难读且不安全。very very wrong
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in (' + ','.join(map(str, ids))+') AND state=%s AND obj_price > 0', ('draft',))
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]no injection, but still wrong
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in %s '
'AND state=%s AND obj_price > 0', (tuple(ids), 'draft',))
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]better
auction_lots_ids = self.search([('auction_id','in',ids), ('state','=','draft'), ('obj_price','>',0)])
防止SQL注入!
使用 SQL 代码查询时,必须注意不要引入 SQL 注入漏洞。当用户输入被错误地过滤或引用错误时,存在此漏洞,从而允许攻击者向 SQL 查询引入不需要的子句(例如绕过筛选器或执行 UPDATE 或 DELETE 命令)。安全的最佳方法是永远不要使用 Python 字符串串联 (+) 或字符串参数插值 (%)将变量传递到 SQL 查询字符串。
几乎同样重要的第二个原因,应该由数据库抽象层 (psycopg2) 来决定如何格式化查询参数,而不是您的代码!例如,psycopg2 知道,当您传递一个值列表时,它需要将它们格式化为逗号分隔列表,包含在括号中!
the following is very bad:
- it's a SQL injection vulnerability
- it's unreadable
- it's not your job to format the list of ids
self.env.cr.execute('SELECT distinct child_id FROM account_account_consol_rel ' +
'WHERE parent_id IN ('+','.join(map(str, ids))+')')better
self.env.cr.execute('SELECT DISTINCT child_id '
'FROM account_account_consol_rel '
'WHERE parent_id IN %s',
(tuple(ids),))
这些都是非常重要的,所以在重构时也要小心,最重要的是不要复制这类错误!下面是一些令人难忘的示例,可帮助您记住问题的内容(但不要在那里复制代码)。在继续之前,请务必阅读 pyscopg2 的在线文档,以了解正确使用它:
The problem with query parameters (http://initd.org/psycopg/docs/usage.html#the-problem-with-the-query-parameters)
How to pass parameters with psycopg2 (http://initd.org/psycopg/docs/usage.html#passing-parameters-to-sql-queries)
Advanced parameter types (http://initd.org/psycopg/docs/usage.html#adaptation-of-python-values-to-sql-types)
扩展的思考
函数和方法不应包含太多的逻辑:使用大量小而简单的方法比使用少量大型和复杂的方法更可取。一条好的经验法则是,只要方法具有多个责任,就将其拆分(参考 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 调用开始时,打开一个新的数据库游标,并在调用return返回时commit,即在commit前先将结果返回至 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() :
在模型的 _auto_init() 方法中。模型对象:这是由加载项初始化方法,或由 ORM 事务在创建自定义模型时处理
在报表中: commit() 也由框架处理,因此您甚至可以在报表内更新数据库
在模型中。瞬态方法:这些方法与常规模型完全一样。模型,在事务中,并在末尾使用相应的 cr.commit()/rollback()
其它情况(如果您有疑问,请参阅上面的一般规则!
所以,服务器框架之外的所有 cr.commit() 调用都必须有一个明确的注释,解释它们为什么是绝对必要的,为什么它们确实是正确的,以及为什么它们不中断事务。否则,他们应该被删除!正确的使用翻译方法
Odoo 使用类似于 GetText 的方法名 _( ) 来指示代码中使用的静态字符串需要在运行时使用上下文的语言进行翻译。通过在代码中通过导入访问此伪方法:from odoo.tools.translate 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!') % recordbad: 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: 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)
bad: field values are automatically translated by the framework
This is useless and will not work the way you think:
error = _("Product %s is not available!") % _(product.name)
and the following will of course not work as already explained:
error = _("Product %s is not available!" % 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、newline 换行等),以明智和明显的方式使用这些格式模式非常重要:Bad: makes the translations hard to work with
error = "'" + question + _("' nPlease enter an integer value ")
Better (pay attention to position of the brackets too!)
error = _("Answer to question %s is not valid.n"
"Please enter an integer value.") % question
通常,在 Odoo 中,在操作字符串时,更多用 % 而不是用 .format()(当字符串中只有一个变量替换),更多用 %(varname) 而不是指定位置(当多个变量必须替换时)。这使得社区翻译人员更容易翻译。符号和习惯
模型名-使用.分隔,模块名做前缀
定义odoo模型时,使用单数形式的名字如res.partner,sale.order 而不是 res.partnerS,saleS.orderS
定义odoo Transient / wizard时,命名格式为<related_base_model>.,此瞬态模型是 related_base_model是基础模型(在目录 models/ 中定义)进行的关联,action是功能简称,避免使用 wizard 关键字. 例如: account.invoice.make, project.task.delegate.batch, …
定义报表模型(或者SQL View)时,使用<related_base_model>.report.,和瞬态模型一样
python类-使用驼峰命名
class AccountInvoice(models.Model):
...
变量命名
模型变量使用驼峰命名方式
普通变量用小写字母 + 下划线关联来命名
如果变量包含记录 id 或 id 列表则将变量名称后缀为 _id 或 _ids。不要使用 partner_id 来包含一条 res.partner 的记录(id 只记录id这个数字,而非对象)
Partner = self.env['res.partner']
partners = Partner.browse(ids)
partner_id = partners[0].id
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<field_name>
Constraint 约束方法: check<constraint_name>
Action动作方法: 一个对象的动作方法一般以action_开头,它的装饰器是 @api.multi,如果它只使用单条计算,可在方法头添加self.ensure_one()
模型中属性顺序
私有属性:(_name, _description, _inherit, …)
默认方法和 _default_get
Field 字段声明
Compute, inverse and search 等计算和搜索方法和字段声明顺序一致
Selection 方法(返回 selection 字段的列表值)
Constrains 约束方法(@api.constrains) and onchange 字段值变更方法 (@api.onchange)
CRUD方法(ORM 覆盖与继承)
Action方法
最后是其他业务方法
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(oldname='register_current', string='Reserved Seats', store=True, readonly=True, compute='_compute_seats') seats_available = fields.Integer(oldname='register_avail', 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.multi @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_get, name_search, ...) overrides def create(self, values): ... # Action methods @api.multi def action_validate(self): self.ensure_one() ... # Business methods def mail_user_confirm(self): ...
Javascript和CSS
Static files 静态文件目录
Odoo 模块有一些关于如何构造各种文件的约定。在这里,我们将更详细地解释 Web 资源应该如何组织。首先要知道的是,Odoo 服务器将提供(静态)web服务给位于 static/ folder 文件夹中的所有文件,注意仅针对指定后缀的文件类型。一个例子是,如果文件位于 addons/web/static/src/js/some_file.js , 那么此静态文件可以如此访问 url
your-odoo-server.com/web/static/src/js/some_file.js
请按如下约定结构组织你的代码:
static: 所有一般静态资源文件
static/lib: 库文件,如: jquery 库可放在 addons/web/static/lib/jquery
static/src: 源文件
static/src/css
static/src/fonts
static/src/img
static/src/js
static/src/scss
static/src/xml: 所有qweb templates 文件
static/tests: 所有测试相关文件
Javascript 编码规范
use strict; 建议用于所有 javascript 文件
使用代码格式化脚本 linter (jshint, …)
不要使用压缩编译过的 Javascript 库
类名使用驼峰命名
除非你的javascript代码需要全局(即在所有页面)运行,否则必须在website模块中声明一个 if_dom_contains 。对 dom 元素的操作要使用 JQuery 代码
odoo.website.if_dom_contains('.jquery_class_selector', function () {
/your code here/
});
CSS 编码规范
用 o_<module_name> foras forase,其中 module_name 是模块的技术名称("sale","im_chat",...)或模块的主路由(主要为 website 网站相关模块,如网站/论坛模块的"o_forum")。此规则的唯一例外是 webclient:它只使用 o_ 前缀。
避免使用 id 标签
使用Bootstrap 原生的 classes
使用 underscore 小写加 _ 连接来命名 class 类
Git 操作规范
这个按各自喜好,习惯方便就好,关键在于把 Git 用好!