如何构建自定义 Odoo 模块:OWL、ORM 和继承开发人员指南
当 Odoo 的 82 个官方模块和 40,000 多个社区模块不能满足您的具体业务需求时,自定义模块开发可以填补空白。本指南涵盖了 2026 年构建 Odoo 模块的基础知识,包括模块结构、OWL 前端框架、ORM 模式、继承机制以及符合 OCA(Odoo 社区协会)指南的最佳实践。
什么是自定义 Odoo 模块?
自定义 Odoo 模块 是一个独立的包,包含 Python 后端逻辑、XML 视图定义、JavaScript 前端组件和扩展 Odoo 功能的安全规则。模块可以添加全新功能、修改现有行为或将 Odoo 与外部系统集成。 Odoo 本身的每个部分都是一个模块,使得该架构具有本质上的可扩展性。
模块目录结构
组织良好的模块遵循以下标准结构:
my_custom_module/
├── __init__.py # Python package init
├── __manifest__.py # Module metadata and dependencies
├── models/
│ ├── __init__.py
│ └── my_model.py # Business logic and data models
├── views/
│ ├── my_model_views.xml # Form, tree, and kanban views
│ └── menu.xml # Menu items and actions
├── security/
│ ├── ir.model.access.csv # Access control list
│ └── security.xml # Record rules and groups
├── data/
│ └── data.xml # Default data records
├── static/
│ └── src/
│ ├── js/ # OWL components
│ ├── css/ # Stylesheets
│ └── xml/ # QWeb templates
├── wizard/ # Transient models for wizards
├── reports/ # QWeb report templates
└── tests/ # Unit tests
清单文件
__manifest__.py 文件定义模块的标识:
{
'name': 'My Custom Module',
'version': '18.0.1.0.0',
'category': 'Custom',
'summary': 'Short description of what the module does',
'description': """
Long description with details about
features and configuration.
""",
'author': 'ECOSIRE',
'website': 'https://ecosire.com',
'license': 'LGPL-3',
'depends': ['base', 'sale', 'stock'],
'data': [
'security/ir.model.access.csv',
'views/my_model_views.xml',
'views/menu.xml',
'data/data.xml',
],
'assets': {
'web.assets_backend': [
'my_custom_module/static/src/js/**/*',
'my_custom_module/static/src/css/**/*',
'my_custom_module/static/src/xml/**/*',
],
},
'installable': True,
'application': False,
'auto_install': False,
}
版本约定: {odoo_version}.{major}.{minor}.{patch}(例如,18.0.1.0.0)。
使用 ORM
Odoo 的对象关系映射(ORM)是所有后端开发的基础。模型映射到数据库表,ORM 提供 CRUD 操作、计算字段、约束和工作流管理。
定义模型
from odoo import models, fields, api
class ProjectTask(models.Model):
_name = 'my_module.task'
_description = 'Project Task'
_order = 'priority desc, create_date desc'
name = fields.Char(string='Task Name', required=True)
description = fields.Html(string='Description')
state = fields.Selection([
('draft', 'Draft'),
('in_progress', 'In Progress'),
('done', 'Done'),
('cancelled', 'Cancelled'),
], default='draft', tracking=True)
assigned_to = fields.Many2one('res.users', string='Assigned To')
deadline = fields.Date(string='Deadline')
priority = fields.Selection([
('0', 'Normal'),
('1', 'Important'),
('2', 'Urgent'),
], default='0')
tag_ids = fields.Many2many('my_module.tag', string='Tags')
progress = fields.Float(compute='_compute_progress', store=True)
字段类型参考
|字段类型| Python 类型 |使用案例| |---|---|---| |查尔 | STR |短文本(名称、参考文献)| |文字| STR |长纯文本 | |网页 | STR |富文本内容 | |整数|整数 |整数| |浮动 |浮动|小数 | |布尔 |布尔 |真/假标志 | |日期 |日期 |没有时间的日期 | |日期时间 |日期时间 |日期与时间 | |选择| STR |下拉选项 | |多对一 |整数 |链接到一条记录 | |一对多 |列表 | Many2one 的反向 | |多对多 |列表 |链接到多个记录 | |二进制|字节|文件附件 |
计算字段和约束
@api.depends('subtask_ids.state')
def _compute_progress(self):
for task in self:
total = len(task.subtask_ids)
done = len(task.subtask_ids.filtered(
lambda t: t.state == 'done'
))
task.progress = (done / total * 100) if total else 0
@api.constrains('deadline')
def _check_deadline(self):
for task in self:
if task.deadline and task.deadline < fields.Date.today():
raise ValidationError(
"Deadline cannot be in the past."
)
继承机制
Odoo 提供了三种类型的继承,每种类型都有不同的用途:
1.类继承(扩展)
通过添加字段或覆盖方法来扩展现有模型。这是最常见的模式。
class SaleOrderExtend(models.Model):
_inherit = 'sale.order'
custom_reference = fields.Char(string='Custom Ref')
approved_by = fields.Many2one('res.users')
def action_confirm(self):
# Add custom logic before standard confirmation
for order in self:
if order.amount_total > 10000 and not order.approved_by:
raise UserError("Orders over $10,000 require approval.")
return super().action_confirm()
2.原型继承
创建一个新模型,复制现有模型中的所有字段和方法。
class CustomPartner(models.Model):
_name = 'my_module.partner'
_inherit = 'res.partner' # Copies structure
_description = 'Custom Partner'
3.委托继承
创建一个新模型,通过 Many2one 链接委托给现有模型。父模型的字段透明地显示在子模型上。
class LibraryMember(models.Model):
_name = 'library.member'
_inherits = {'res.partner': 'partner_id'}
partner_id = fields.Many2one('res.partner', required=True,
ondelete='cascade')
membership_date = fields.Date()
member_number = fields.Char()
OWL 框架(前端)
Odoo 18 使用 OWL(Odoo Web Library)作为其前端框架。 OWL 是一个基于组件的框架,类似于 React 或 Vue,但专为 Odoo 的需求而设计。
基本 OWL 组件
/** @odoo-module */
import { Component, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
class TaskDashboard extends Component {
static template = "my_module.TaskDashboard";
setup() {
this.state = useState({
tasks: [],
filter: 'all',
});
this.loadTasks();
}
async loadTasks() {
this.state.tasks = await this.env.services.orm.searchRead(
"my_module.task",
[["state", "!=", "cancelled"]],
["name", "state", "assigned_to", "deadline"]
);
}
get filteredTasks() {
if (this.state.filter === 'all') return this.state.tasks;
return this.state.tasks.filter(
t => t.state === this.state.filter
);
}
}
QWeb 模板 (XML)
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.TaskDashboard">
<div class="o_task_dashboard">
<div class="task-filters">
<button t-on-click="() => state.filter = 'all'">All</button>
<button t-on-click="() => state.filter = 'in_progress'">
In Progress
</button>
</div>
<div class="task-list">
<t t-foreach="filteredTasks" t-as="task" t-key="task.id">
<div class="task-card">
<span t-esc="task.name"/>
</div>
</t>
</div>
</div>
</t>
</templates>
安全配置
每个模型都需要明确的访问规则。没有它们,任何用户都无法访问模型的数据。
访问控制列表 (ir.model.access.csv)
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_task_user,task.user,model_my_module_task,base.group_user,1,1,1,0
access_task_manager,task.manager,model_my_module_task,my_module.group_manager,1,1,1,1
记录规则(security.xml)
<record id="task_own_rule" model="ir.rule">
<field name="name">Own Tasks Only</field>
<field name="model_id" ref="model_my_module_task"/>
<field name="domain_force">
[('assigned_to', '=', user.id)]
</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
测试你的模块
Odoo 支持使用 unittest 框架和 Odoo 特定测试类的 Python 单元测试:
from odoo.tests.common import TransactionCase
class TestTask(TransactionCase):
def setUp(self):
super().setUp()
self.task = self.env['my_module.task'].create({
'name': 'Test Task',
'state': 'draft',
})
def test_task_creation(self):
self.assertEqual(self.task.state, 'draft')
self.assertEqual(self.task.progress, 0)
def test_deadline_constraint(self):
with self.assertRaises(ValidationError):
self.task.write({
'deadline': fields.Date.subtract(
fields.Date.today(), days=1
),
})
使用以下命令运行测试: odoo-bin -d test_db --test-enable -i my_custom_module --stop-after-init
常见问题
问:构建自定义 Odoo 模块需要多长时间? 简单的模块(新字段、基本视图)需要 1-3 天。中等模块(新模型、工作流程、报告)需要 1-3 周。复杂的模块(多模型系统、外部集成、自定义 OWL 组件)需要 4-12 周。对于需要自定义模块但缺乏内部 Odoo 开发人员的企业,请从我们的团队中聘请经验丰富的 Odoo 开发人员。
问:我应该修改核心 Odoo 代码还是创建一个单独的模块? 始终创建一个单独的模块。修改核心代码会破坏可升级性并在版本更新期间产生合并冲突。使用继承来扩展自定义模块中的现有模型和视图。
问:OCA 指南是什么? Odoo 社区协会 (OCA) 发布了模块质量的编码标准,包括命名约定、文档要求、测试覆盖率期望和代码风格规则。遵循 OCA 指南可确保您的模块可维护并与更广泛的社区生态系统兼容。
获得专业帮助
构建自定义 Odoo 模块需要 Python、JavaScript、PostgreSQL 和 Odoo 框架约定方面的专业知识。无论您需要简单的现场扩展还是复杂的多模块系统,ECOSIRE 的 Odoo 定制服务 都能提供根据 OCA 标准构建的生产就绪模块。
联系我们 讨论您的自定义模块需求并获得开发估算。
作者
ECOSIRE Research and Development Team
在 ECOSIRE 构建企业级数字产品。分享关于 Odoo 集成、电商自动化和 AI 驱动商业解决方案的洞见。
相关文章
Amazon.de Odoo 集成:使用 Odoo ERP 在德国最大的市场上销售
如何针对德国市场将 Amazon.de 与 Odoo ERP 集成。涵盖德国亚马逊物流、泛欧履行、德国增值税、VerpackG 合规性和结算对账。
通过 Odoo 进入德国电子商务市场:国际卖家分步指南
国际卖家进入德国电子商务市场的完整指南。涵盖市场分析、法律要求、增值税注册、市场选择以及向德国消费者销售的 Odoo ERP 设置。
使用 Odoo 管理德国电子商务退货:高回报市场策略
如何使用 Odoo ERP 处理德国较高的电子商务退货率。涵盖 Zalando、Otto、Amazon.de 和 Kaufland 的退货处理工作流程、原因代码分析、补货自动化以及市场特定政策。