第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, fieldsclass 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 datetimeclass MyModel(models.Model):_name = 'my.model'my_field = fields.Char(default=lambda self: datetime.now().strftime('%Y-%m-%d %H:%M:%S'))@api.modeldef default_get(self, fields):res = super().default_get(fields)if 'user_id' in fields:res['user_id'] = self.env.user.idreturn 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 = Falsedef 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, modelsclass MyModel(models.Model):_name = 'my.model'price = fields.Float('Price', company_dependent=True)def my_method(self):company = self.env.companylang = self.env.langraise 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:# 业务逻辑passexcept 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_roundamount = 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 fieldsnow = fields.Datetime.now()
结语:开发不是“快改”,是“稳改”
Odoo 的二次开发并非简单的代码堆砌,而是对底层机制和规范的深度理解。只有避免这些常见陷阱,才能打造稳定、可维护、可升级的企业系统。