本文介绍了新版Odoo编码指南。那些旨在提高代码的质量 (例如更好的可读性)和Odoo应用程序。实际上,适当的代码简化了维护、调试,降低了复杂性,提高了可靠性。
这些指导原则应适用于每一个新的模块和新的程序。只有在代码重构(迁移到新API、大重构、……)的情况下,这些准则才适用于旧模块。
警告
这些指南是用新的模块和新的文件编写的。当修改现有文件时,文件的原始样式严格地取代任何其他样式指南。 换句话说,不要修改现有文件以应用这些指南,以避免破坏每一行的修订历史。有关更多细节,请参见pull request guide
在重要目录中组织一个模块。这些包含业务逻辑;查看它们应该了解模块的用途。
其他可选目录组成模块。
对于视图的声明,从(前端)模版分裂后端视图到2个不同的文件中。
对于models,通过一组模型将业务逻辑拆分,在每一个集合中选择一个主模型,该模型将其名称赋予该集合。如果只有一个模型,则其名称与模块名相同。或每一组命名为
models/ .py
models/ .py
views/ _templates.xml
views/ _views.xml
例如,销售模块介绍sale_order
和sale_order_line
,在它们中sale_order
为主。所以
文件将命名为models/sale_order.py
和views/sale_order_views.py
对于数据,按目的拆分:demo或data。文件名将是主模型的名字,使用 _demo.xml 或 _data.xml作为后缀
对于controllers, 唯一的文件应该命名为main.py。否则,如果需要从另一模块继承现有控制器,它的名字将是
对与static文件,由于资源可以在不同的上下文中使用 (前端,后端,两者都是),它们将包含在仅有的一个bundle中。因此,CSS/Less, JavaScript和XML文件使用bundle类型名字作为后缀。即: im_chat_common.css, im_chat_common.js在'assets_common' bundle中, 且im_chat_backend.css, im_chat_backend.js在'assets_backend' bundle中。如果模块只拥有一个文件,则该约定将是
关于data, 按目的拆分它们:data或demo。文件名将是主模型的名字,以_data.xml或 _demo.xml作为后缀
关于向导,命名约定是:
.py
_views.xml
对于统计报告,它们的名字应该看起来像:
_report.py
_report_views.py
(通常是pivot和graph视图)对于可打印报表,您应该具有:
_reports.py
(报表动作,表格格式定义, ...) _templates.xml
(xml报表模版)完整的树应该看起来像
addons//
|-- __init__.py
|-- __manifest__.py
|-- controllers/
| |-- __init__.py
| |-- .py
| `-- main.py
|-- data/
| |-- _data.xml
| `-- _demo.xml
|-- models/
| |-- __init__.py
| |-- .py
| `-- .py
|-- report/
| |-- __init__.py
| |-- .py
| |-- _views.xml
| |-- _reports.xml
| `-- _templates.xml
|-- security/
| |-- ir.model.access.csv
| `-- _security.xml
|-- static/
| |-- img/
| | |-- my_little_kitten.png
| | `-- troll.jpg
| |-- lib/
| | `-- external_lib/
| `-- src/
| |-- js/
| | `-- .js
| |-- css/
| | `-- .css
| |-- less/
| | `-- .less
| `-- xml/
| `-- .xml
|-- views/
| |-- _templates.xml
| |-- _views.xml
| |-- _templates.xml
| `-- _views.xml
`-- wizard/
|-- .py
|-- _views.xml
|-- .py
`-- _views.xml
注
文件命名应该仅包含[a-z0-9_]
(小写字母数字和_
)
警告
使用正确的文件权限 : 文件夹755和文件644(针对非windows系统)
为了在XML中声明记录,建议使用record 记号(使用
model
属性前放id
属性name
属性。然后将值放在field标记中,或者在 eval
属性中,最后根据重要性排序其他属性(widget, options, ...)
view.name
object_name
Odoo支持自定义标签充当语法糖:
4个首选的标记在record标记之前是优先考虑的
安全, 视图和操作
使用以下模式:
_menu
_view_
, view_type 是kanban
, form
, tree
, search
, ... _action
。另一些则用 _
后缀,其中细节是一个小写字符串,简要解释操作。只有在为模型声明多个操作时才使用此方法 _group_
group_name是组的名称,通常是'user', 'manager', ... _rule_
concerned_group是连接的组的简洁名称('user' 对于'model_name_group_user', 'public' 对于公共用户, 'company'对于多公司规则, ...)
...
...
注
视图名称使用点标记my.model.view_type
或者my.model.view_type.inherit
而不是"这是我的模型的表单视图".
继承的XML
继承视图的命名模式是
。模块只能扩展视图一次。用_inherit_
加后缀到原始的名称,其中current_module_name 是扩展视图的模块的技术名称
...
使用linter可以帮助显示语法和语义警告或错误。Odoo源代码试图尊重Python标准,但其中一些可以被忽略。
导入顺序如下
在这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
from odoo.tools.translate import _
# 3 : imports from odoo modules
from odoo.addons.website.models.website import slug
from odoo.addons.web.controllers.main import login_redirect
# -*- 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)
# -- 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
和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]
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..."
# creates a temporary list
for key, value in my_dict.items():
"do something..."
# only iterates
for key, value in my_dict.iteritems():
"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)
filtered
, mapped
, sorted
, ... 简化代码阅读和性能的方法让你的方法批量工
添加函数时,确保它可以处理多个记录。典型的,这种方法使用api.multi装饰器进行装饰(或者在旧API中写入id列表)。然后,您必须迭代self来处理每个记录
@api.multi
def my_method(self)
for record in self:
record.do_cool_stuff()
避免使用api.one
装饰器:这可能不会达到您所期望的,并且扩展这样的方法并不像 api.multi 方法那么简单,因为它返回一个结果列表(由记录集ID排序)。
对于性能问题,当开发一个“状态按钮”(例如)时,不要在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)
传播上下文
在新的API中,上下文是不能修改的 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 。但是,如果固化此创建,其他对象 (例如sale.order.line, 在sale.order创建上) 具有字段名e my_field,它们的默认值也将被设置。
如果需要创建影响某个对象行为的关键上下文,则选择一个好名称,并最终通过模块的名称对其添加前缀以隔离其影响。一个很好的例子是 mail
模块的键:mail_create_nosubscribe, mail_notrack, mail_notify_user_signature, ...
不要绕过ORM
当ORM可以做同样的事情时,你不应该直接使用数据库光标!如果这样做,您将绕过所有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 的在线文档,以了解如何正确使用它:
在可能的情况下保持你的方法简短/简单
功能和方法不应该包含太多的逻辑: 有很多小的和简单的方法比用很少的大而复杂的方法更可取。一个好的经验法则是: - 它有不止一种责任 (详见http://en.wikipedia.org/wiki/Single_responsibility_principle) -它太大,不能装在一个屏幕上。
同时,相应地命名您的函数: 小的和正确命名的函数是可读/可维护代码的起点和更严格的文档。
此建议也与类、文件、模块和包有关 (也可参见 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(),任何地方都有很高的机会,您将以不同的方式中断系统,因为您将导致部分提交,从而部分和不干净的回滚,导致其他问题:
这里有一条非常简单的规则
除非您已显式创建了自己的数据库游标,否则应该从不调 cr.commit()
! 而你需要做的事情是例外的!
顺便说一下,如果您确实创建了自己的游标,那么您需要处理错误情况和正确的回滚,以及在完成该游标时正确关闭光。
与流行的信条相反,在下面的情况下,您甚至不需要调用cr.commit()
: - 在一个i额models.Model对象的_auto_init()方法中:在创建自定义模型时,通过addons初始化方法或ORM事务来处理这一点 - 在报表中:该commit()也由框架处理,因此您甚至可以从报表中更新数据库 - 在models.Transient 方法中:这些方法被准确的称为 models.Model 中的一个, 在事务中和在结束时使用相应的cr.commit()/rollback()
等。 (如果您有疑问,请参见上面的一般规则!)
所有的cr.commit()
从现在起调用服务器框架之外的内容,必须有一个明确的注释来解释为什么它们是绝对必要的,为什么它们确实是正确的,以及为什么它们不中断事务。 否则,他们可以并将被删除!
正确使用翻译方法
Odoo使用一个名为"下划线" _( )
的像文本一样的方法来指示代码中使用的静态字符串需要在运行时使用上下文语言进行翻译。 通过导入如下代码,可以在代码中访问此伪方法:
from odoo.tools.translate import _
当使用它时,必须遵循一些非常重要的规则,以避免使用无用的垃圾。
基本上,这种方法只适用于在代码中手工编写的静态字符串,它不适用于翻译字段值,例如产品名称等。必须使用相应字段上的平移标志来完成此操作。
该规则非常简单:对下划线方法的调用应该总是以 _('literal string')
的形式出现,而无其他:
# 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: 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,换行符等。需要保存,但重要的是用一种明智和明显的方式:
# 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)
而不是位置(当需要替换多个变量时)。这使得社区翻译者的翻译变得更容易。
模型名称(使用点标记,模块名前缀):
.
,其中related_base_model 是和瞬态关联的基本模型(定义在models/中), 而操作是瞬态完成的事情的短暂的名字。例如: account.invoice.make
, project.task.delegate.batch
, ....report.
,基于瞬态约定class AccountInvoice(models.Model):
...
class account_invoice(osv.osv):
...
变量名:
ResPartner = self.env['res.partner']
partners = ResPartner.browse(ids)
partner_id = partners[0].id
One2Many
和 Many2Many
字段应该总是有 _ids作为后缀(例子:sale_order_line_ids)Many2One
字典应该有 _id 做为后缀(例子 : partner_id, user_id, ...)方法约定
@api.multi
, 但是因为它只使用一个记录,在方法的开始添加self.ensure_one()
模型中的属性顺序应该是
_name
, _description
, _inherit
, ...)_default_get
@api.constrains
) 和onchange方法(@api.onchange
)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')
# 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):
...
# 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 :
use strict;
建议所有的JavaScript文件odoo.website.if_dom_contains('.jquery_class_selector', function () {
/*your code here*/
});
对于CSS :
使用如下作为你提交的前缀
然后,在消息本身中,指定由您的更改(模块名称, 库, 转换对象, ...) 影响的代码的一部分以及更改的描述。
[FIX] website, website_mail: remove unused alert div, fixes look of input-group-btn
Bootstrap's CSS depends on the input-group-btn
element being the first/last child of its parent.
This was not the case because of the invisible
and useless alert.
[IMP] fields: reduce memory footprint of list/set field attributes
[REF] web: add module system to the web client
This commit introduces a new module system for the javascript code.
Instead of using global ...
注
用长的描述来解释为什么不是什么,什么可以看到的差异
ps:有翻译不当之处,欢迎留言指正。
原文地址:https://www.odoo.com/documentation/10.0/reference/guidelines.html