第23讲、Odoo18 二开常见陷阱
Odoo 是一套功能强大的企业级开源 ERP 系统,但在实际的二次开发过程中,常常“暗藏玄机”。尤其对于刚接触 Odoo 开发的工程师来说,稍有不慎就容易“踩坑”。
本文系统梳理了 Odoo 二次开发中最常见的陷阱,并配以详细的应对策略和代码案例,助你在开发之路上少走弯路!
一、直接修改官方模块代码(强烈避免!)
问题描述:许多初学者在遇到需求变更时,直接修改 addons 目录下的官方模块代码,导致后续升级或迁移时出现冲突或功能丢失。
正确做法:
- 使用继承机制(class/record)扩展已有功能;
- 通过
_inherit或_inherits实现模型继承; - 使用 XML
xpath或position="replace"修改视图。
错误案例(直接修改官方代码):
# 错误:直接在 odoo/addons/sale/models/sale_order.py 里加字段
class SaleOrder(models.Model):
_inherit = 'sale.order'
new_field = fields.Char('New Field')
正确案例(自定义模块继承):
# 正确:在自定义模块 my_sale_extend/models/sale_order.py
from odoo import models, fields
class SaleOrder(models.Model):
_inherit = 'sale.order'
new_field = fields.Char('New Field')
二、忽视字段默认值的动态计算
问题描述:未理解 default_* 方法或 default 字段的懒加载机制,导致默认值异常或不可控。
解决方案:
- 使用
@api.model定义默认值方法; - 明确何时使用
lambda,何时使用default_*方法; - 注意
context在默认值计算中的作用。
错误案例:
# 错误:直接赋值,所有用户都一样
my_field = fields.Char(default=datetime.now())
正确案例:
from odoo import models, fields, api
from datetime import datetime
class MyModel(models.Model):
_name = 'my.model'
my_field = fields.Char(default=lambda self: datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
@api.model
def default_get(self, fields):
res = super().default_get(fields)
if 'user_id' in fields:
res['user_id'] = self.env.user.id
return res
三、错误使用 @api.onchange 和 @api.depends
问题描述:将 @api.onchange 当作业务逻辑处理手段,忽视其仅限前端逻辑的特性。
应对方法:
@api.onchange:仅适用于界面交互;@api.depends:用于计算字段依赖;- 关键业务逻辑应放在
create()和write()方法中。
错误案例:
@api.onchange('amount')
def _onchange_amount(self):
if self.amount > 10000:
self.state = 'approved' # 只在前端生效,保存后无效
正确案例:
@api.onchange('amount')
def _onchange_amount(self):
if self.amount > 10000:
self.state = 'approved' # 仅前端预览
@api.model
def create(self, vals):
if vals.get('amount', 0) > 10000:
vals['state'] = 'approved'
return super().create(vals)
def write(self, vals):
if vals.get('amount', 0) > 10000:
vals['state'] = 'approved'
return super().write(vals)
四、忽略用户权限和访问控制规则
问题描述:未设置 ir.rule 和 access.csv,导致用户无法访问模块或数据安全缺失。
建议:
- 配置
security/ir.model.access.csv; - 使用
record rules(记录规则)控制数据可见性; - 熟悉
groups、perm_read、perm_write等机制。
代码案例:
security/ir.model.access.csv 示例:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_my_model_user,my.model user,model_my_model,base.group_user,1,0,0,0
security/my_model_rule.xml 示例:
<record id="my_model_rule" model="ir.rule">
<field name="name">My Model: Only Own Records</field>
<field name="model_id" ref="model_my_model"/>
<field name="domain_force">[("user_id", "=", user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
五、数据删除不彻底:未正确处理 active 字段
问题描述:部分模块启用了软删除机制(如 active=False),开发者误以为直接 unlink() 就可以彻底删除。
提示:
- 优先采用
active字段隐藏数据; - 删除记录前先确认其依赖和关联数据是否已清理;
unlink()慎用,建议优先使用archive()。
代码案例:
class MyModel(models.Model):
_name = 'my.model'
active = fields.Boolean(default=True)
def archive(self):
for rec in self:
rec.active = False
def unlink(self):
# 检查依赖关系
if self.env['other.model'].search([('my_model_id', 'in', self.ids)]):
raise UserError('请先删除相关依赖数据!')
return super().unlink()
六、One2many / Many2many 字段写入失败或数据异常
问题描述:不了解关系字段的写法,常出现 create() 时数据未保存、更新无效等问题。
常见写法错误:
# 错误方式
vals['line_ids'] = [1, 2, 3]
正确案例:
# 关联已有记录
vals['line_ids'] = [(6, 0, [1, 2, 3])]
# 新建子记录
vals['line_ids'] = [(0, 0, {'name': 'xxx', 'value': 123})]
# 删除所有子记录
vals['line_ids'] = [(5, 0, 0)]
完整 create 示例:
order = self.env['sale.order'].create({
'name': 'SO123',
'order_line': [
(0, 0, {'product_id': 1, 'product_uom_qty': 2}),
(0, 0, {'product_id': 2, 'product_uom_qty': 1}),
]
})
七、继承视图时滥用 xpath 或 position 错误
问题描述:错误的 xpath 路径或 position 导致视图加载失败。
解决方法:
- 使用开发者模式打开调试工具,准确定位
field、group、div等元素; - 避免多个模块重复继承同一视图的同一位置;
- 可先用 Studio 试验效果再写 XML。
代码案例:
<!-- 错误:xpath 路径不准确 -->
<xpath expr="//field[@name='wrong_field']" position="after">
<field name="my_field"/>
</xpath>
<!-- 正确:用开发者工具定位 -->
<xpath expr="//field[@name='partner_id']" position="after">
<field name="my_field"/>
</xpath>
八、模型/字段命名与 Odoo 保留关键字冲突
问题描述:字段名如 name、state、type 等被覆盖,影响内置行为。
最佳实践:
- 避免使用保留关键字段名;
- 若确实需要,用
_custom后缀或更具语义的名称替代; - 注意
_rec_name、_order等模型配置。
代码案例:
# 错误:直接用 type 字段
type = fields.Char('Type')
# 正确:用 type_custom 或更具体的名称
type_custom = fields.Char('Custom Type')
九、PostgreSQL 索引和性能问题未关注
问题描述:在大数据量场景中未建立索引,导致查询极慢。
优化建议:
- 为常用过滤字段(如
state、date、partner_id)添加索引; - 使用
@api.depends减少不必要的字段更新; - 使用
read_group()、search_read()替代循环search()+read()。
代码案例:
# 在模型字段上加 index
state = fields.Selection([...], index=True)
partner_id = fields.Many2one('res.partner', index=True)
# 使用 read_group 聚合
result = self.env['sale.order'].read_group(
[('state', '=', 'sale')],
['partner_id', 'amount_total:sum'],
['partner_id']
)
十、忽略多公司/多语言兼容性
问题描述:在开发中硬编码公司 ID 或字段翻译,导致多公司/多语言环境下出错。
建议:
- 使用
company_dependent=True字段属性; - 所有展示性文本使用
_()翻译函数; - 通过
env.company、env.lang获取当前上下文信息。
代码案例:
from odoo import _, api, fields, models
class MyModel(models.Model):
_name = 'my.model'
price = fields.Float('Price', company_dependent=True)
def my_method(self):
company = self.env.company
lang = self.env.lang
raise UserError(_('This is a translated message!'))
番外篇:更多 Odoo 二开常见陷阱与问题
十一、未正确处理多线程/并发写入
问题描述:Odoo 的 ORM 默认不是线程安全的,多个用户同时操作同一数据时,容易出现数据覆盖或丢失。
解决方案:
- 对关键业务操作加锁(如
with_for_update())。 - 在业务逻辑中增加唯一约束和乐观锁。
代码案例:
# 对记录加行级锁
record = self.env['my.model'].search([('id', '=', some_id)]).with_for_update()
十二、忽略 API 兼容性和升级风险
问题描述:直接调用私有方法或依赖 Odoo 内部未文档化的 API,升级时极易出错。
解决方案:
- 只使用官方文档推荐的 API。
- 避免 monkey patch 和直接操作底层表。
代码案例:
# 错误:直接调用 _compute_xxx 或 _name_search 等私有方法
# 正确:使用官方公开的 search, read, write, create 等方法
十三、未处理定时任务(cron)的异常和幂等性
问题描述:定时任务出错导致数据重复处理或遗漏,影响业务。
解决方案:
- 定时任务需加 try/except,记录日志。
- 设计幂等逻辑,避免重复执行带来副作用。
代码案例:
@api.model
def my_cron_job(self):
try:
# 业务逻辑
pass
except Exception as e:
_logger.error('Cron job failed: %s', e)
十四、忽略附件(ir.attachment)和大文件存储优化
问题描述:直接将大文件存入数据库,导致数据库膨胀、备份困难。
解决方案:
- 配置附件存储到文件系统(filestore)。
- 对大附件做分片或外部存储。
代码案例:
# 配置 ir_attachment.location = 'file',避免存入数据库
十五、未正确处理浮点数精度和货币换算
问题描述:直接用 float 进行金额运算,导致精度丢失或对账出错。
解决方案:
- 使用 Odoo 的
fields.Monetary字段和float_round工具。 - 货币相关运算用
currency_id.round()。
代码案例:
from odoo.tools.float_utils import float_round
amount = float_round(amount, precision_digits=2)
十六、忽略消息通知和 Chatter 的集成
问题描述:自定义模型未集成消息跟踪,用户无法收到变更通知。
解决方案:
- 继承
mail.thread和mail.activity.mixin。 - 在字段上加
track_visibility。
代码案例:
class MyModel(models.Model):
_name = 'my.model'
_inherit = ['mail.thread', 'mail.activity.mixin']
state = fields.Selection([...], track_visibility='onchange')
十七、未处理时区和日期时间的本地化
问题描述:直接用 datetime.now(),导致多时区用户看到的时间不一致。
解决方案:
- 使用 Odoo 的
fields.Datetime.now()。 - 前端展示用
context_tz转换。
代码案例:
from odoo import fields
now = fields.Datetime.now()

结语:开发不是“快改”,是“稳改”
Odoo 的二次开发并非简单的代码堆砌,而是对底层机制和规范的深度理解。只有避免这些常见陷阱,才能打造稳定、可维护、可升级的企业系统。

浙公网安备 33010602011771号