最近在公司 IT 部接触 Odoo 开发,从模型定义到视图布局、再到权限和记录规则,整套链路走下来发现 Odoo 这套"声明式 ERP 框架"的套路还是挺值得整理一份的。这篇就把我在内部知识库里沉淀的内容搬到博客上,权当一份自己以后翻、新人也能照着抄的速查手册。
一、Odoo 开发基础架构
Odoo 采用 MVC 架构(模型-视图-控制器),开发主要围绕三个核心部分:
- 后端模型:定义数据结构和业务逻辑
- 前端视图:定义用户界面展示
- 权限系统:控制数据访问权限
二、后端开发详解
1. 模型定义基础
python
from odoo import models, fields, api
class EstateProperty(models.Model):
_name = "estate.property"
_description = "房产信息"_name:模型唯一标识符,必须全局唯一,格式通常为小写字母加下划线_description:模型描述,用于文档和界面显示_inherit:可选属性,用于继承现有模型(如_inherit = "res.partner")
2. 字段类型与参数详解
基本字段类型
fields.Char():字符串字段string:字段标签(界面显示名称)required:是否必填(True/False)default:默认值(可为函数)size:最大长度限制
pythonname = fields.Char(string="房产名称", required=True, size=100)fields.Integer():整数字段string:字段标签default:默认值digits:显示位数(元组格式,如(16, 2)表示总 16 位,小数 2 位)
pythonliving_area = fields.Integer(string="居住面积(㎡)", default=0)fields.Float():浮点数字段string:字段标签digits:精度控制(如digits=(16, 2)表示保留两位小数)
pythonprice = fields.Float(string="价格(万元)", digits=(16, 2))fields.Boolean():布尔字段string:字段标签default:默认值(True/False)
pythonis_available = fields.Boolean(string="是否可用", default=True)fields.Date()/fields.Datetime():日期/日期时间字段string:字段标签default:默认值(可为fields.Date.today等函数)
pythonsale_date = fields.Date(string="销售日期", default=fields.Date.today)fields.Text():多行文本字段string:字段标签default:默认值
pythondescription = fields.Text(string="详细描述")fields.Html():HTML 格式文本字段string:字段标签sanitize:是否自动清理 HTML(默认 True)
pythonnotes = fields.Html(string="备注信息")
关系字段类型
fields.Many2one():多对一关系comodel_name:关联模型名称string:字段标签ondelete:删除关联记录时的行为('cascade'/'set null'/'restrict')
pythonowner_id = fields.Many2one(comodel_name="res.partner", string="业主", ondelete="cascade")fields.One2many():一对多关系comodel_name:关联模型名称inverse_name:关联模型中指向当前模型的字段名string:字段标签
pythonproperty_line_ids = fields.One2many(comodel_name="estate.property.line", inverse_name="property_id", string="房产明细")fields.Many2many():多对多关系comodel_name:关联模型名称relation:关系表名称(可选,自动生成)column1:当前模型 ID 列名(可选)column2:关联模型 ID 列名(可选)string:字段标签
pythontag_ids = fields.Many2many(comodel_name="estate.property.tag", string="标签")
3. 计算字段详解
python
total_area = fields.Integer(compute="_compute_total_area", store=True, string="总面积(㎡)")compute:计算方法名(必须使用@api.depends装饰器)store:是否存储计算结果到数据库(True/False)depends:计算字段依赖的其他字段(通过@api.depends指定)search:可选参数,用于定义搜索方法
python
@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for record in self:
record.total_area = record.living_area + record.garden_area@api.depends:装饰器,指定计算字段依赖的字段self:当前记录集(Recordset),包含所有待计算的记录record:单条记录对象,可访问所有字段
4. 方法类型详解
@api.model:模型级方法,不操作具体记录python@api.model def create_default_properties(self): # 创建默认房产记录 pass@api.returns:返回指定模型的记录python@api.returns("self") def get_available_properties(self): return self.search([("is_available", "=", True)])@api.constrains:字段约束验证python@api.constrains("price") def _check_price(self): for record in self: if record.price < 0: raise ValueError("价格不能为负数")@api.onchange:字段变化时触发python@api.onchange("living_area") def _onchange_living_area(self): if self.living_area > 100: self.price = self.price * 1.1 # 面积大于 100㎡ 时价格上浮 10%
三、前端开发详解
1. 视图 XML 结构基础
xml
<record id="view_estate_property_form" model="ir.ui.view">
<field name="name">estate.property.form</field>
<field name="model">estate.property</field>
<field name="arch" type="xml">
<form>
<sheet>
<!-- 字段定义 -->
</sheet>
</form>
</field>
</record>id:视图唯一标识符model:关联的模型名称arch:视图结构定义,使用 XML 格式type:视图类型(form/tree/calendar 等)
2. 表单视图(form)详解
基本结构
xml
<form string="房产信息">
<header>
<!-- 状态栏、按钮等 -->
</header>
<sheet>
<!-- 主要内容区域 -->
</sheet>
<div class="oe_chatter">
<!-- 讨论区 -->
</div>
</form>string:表单标题<header>:顶部区域,通常放置状态和操作按钮<sheet>:主要内容区域<div class="oe_chatter">:讨论区,显示评论和活动
字段定义参数
xml
<field name="name" required="1" placeholder="请输入房产名称"/>name:关联模型中的字段名required:是否必填(1 表示是)readonly:是否只读(1 表示是)invisible:是否隐藏(1 表示是)placeholder:输入框提示文字widget:指定小部件(如statusbar、image等)
布局控制
分组布局
xml<group> <field name="living_area"/> <field name="garden_area"/> </group><group>:创建一个分组框,包含相关字段
选项卡布局
xml<notebook> <page string="基本信息"> <field name="name"/> </page> <page string="详细信息"> <field name="description"/> </page> </notebook><notebook>:创建选项卡式布局<page>:定义一个选项卡,string属性为选项卡标题
列布局
xml<group col="2"> <field name="living_area"/> <field name="garden_area"/> </group>col:指定列数,自动将字段分配到指定列中
3. 列表视图(tree)详解
xml
<tree>
<field name="name"/>
<field name="living_area"/>
<field name="price"/>
</tree><tree>:定义列表视图<field>:定义列表中显示的字段decoration-xxx:根据条件设置行样式(如decoration-success="is_available")
4. 视图继承机制
xml
<record id="view_estate_property_form_inherit" model="ir.ui.view">
<field name="name">estate.property.form.inherit</field>
<field name="model">estate.property</field>
<field name="inherit_id" ref="estate_property.view_estate_property_form"/>
<field name="arch" type="xml">
<field name="price" position="after">
<field name="total_area" readonly="1"/>
</field>
</field>
</record>inherit_id:指定要继承的视图 IDposition:指定插入位置(after/before/replace/inside)ref:引用其他视图的 ID
四、权限与安全配置
1. 用户组定义
xml
<record id="group_estate_user" model="res.groups">
<field name="name">房产用户</field>
<field name="category_id" ref="base.module_category_sales"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>name:用户组名称category_id:所属应用类别implied_ids:隐含的其他用户组(4 表示添加)
2. 模型权限配置
xml
<record id="access_estate_property" model="ir.model.access">
<field name="name">estate.property</field>
<field name="model_id" ref="model_estate_property"/>
<field name="group_id" ref="group_estate_user"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="0"/>
</record>perm_read:读取权限(1 表示有权限)perm_write:写入权限perm_create:创建权限perm_unlink:删除权限
3. 记录规则配置
xml
<record id="rule_estate_property" model="ir.rule">
<field name="name">房产记录规则</field>
<field name="model_id" ref="model_estate_property"/>
<field name="domain_force">[('owner_id','=',user.partner_id.id)]</field>
<field name="groups" eval="[(4, ref('group_estate_user'))]"/>
</record>domain_force:强制应用的搜索域,限制用户只能看到自己的房产groups:应用此规则的用户组
五、完整实战示例:房产管理模块
1. 模型定义(models/estate_property.py)
python
from odoo import models, fields, api
class EstateProperty(models.Model):
_name = "estate.property"
_description = "房产信息"
# 基本信息
name = fields.Char(string="房产名称", required=True, size=100)
description = fields.Text(string="详细描述")
is_available = fields.Boolean(string="是否可用", default=True)
# 面积信息
living_area = fields.Integer(string="居住面积(㎡)", default=0)
garden_area = fields.Integer(string="花园面积(㎡)", default=0)
total_area = fields.Integer(compute="_compute_total_area", store=True, string="总面积(㎡)")
# 价格信息
price = fields.Float(string="价格(万元)", digits=(16, 2))
# 关系字段
owner_id = fields.Many2one(comodel_name="res.partner", string="业主", ondelete="cascade")
tag_ids = fields.Many2many(comodel_name="estate.property.tag", string="标签")
# 计算字段
@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for record in self:
record.total_area = record.living_area + record.garden_area
# 约束验证
@api.constrains("price")
def _check_price(self):
for record in self:
if record.price < 0:
raise ValueError("价格不能为负数")
# 字段变化触发
@api.onchange("living_area")
def _onchange_living_area(self):
if self.living_area > 100:
self.price = self.price * 1.1 # 面积大于 100㎡ 时价格上浮 10%2. 视图定义(views/estate_property_views.xml)
xml
<odoo>
<!-- 表单视图 -->
<record id="view_estate_property_form" model="ir.ui.view">
<field name="name">estate.property.form</field>
<field name="model">estate.property</field>
<field name="arch" type="xml">
<form string="房产信息">
<header>
<button name="action_make_available" type="object" string="标记为可用" class="oe_highlight"/>
<button name="action_make_unavailable" type="object" string="标记为不可用" class="oe_highlight"/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="description"/>
<field name="is_available" widget="statusbar"/>
</group>
<group col="2">
<field name="living_area"/>
<field name="garden_area"/>
<field name="total_area" readonly="1"/>
<field name="price"/>
</group>
<group>
<field name="owner_id"/>
<field name="tag_ids" widget="many2many_tags"/>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<!-- 列表视图 -->
<record id="view_estate_property_tree" model="ir.ui.view">
<field name="name">estate.property.tree</field>
<field name="model">estate.property</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="living_area"/>
<field name="garden_area"/>
<field name="total_area"/>
<field name="price"/>
<field name="is_available" decoration-success="is_available"/>
</tree>
</field>
</record>
<!-- 菜单定义 -->
<menuitem id="menu_estate_property" name="房产管理" parent="sale.menu_sale_root"/>
<menuitem id="menu_estate_property_list" name="房产列表" parent="menu_estate_property" action="estate_property.action_estate_property"/>
<!-- 动作定义 -->
<record id="action_estate_property" model="ir.actions.act_window">
<field name="name">房产管理</field>
<field name="res_model">estate.property</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>3. 权限配置(security/ir.model.access.csv)
csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_estate_property,estate.property,model_estate_property,group_estate_user,1,1,1,04. 模块清单(__manifest__.py)
python
{
"name": "房产管理",
"version": "1.0",
"summary": "房产信息管理模块",
"description": "管理房产信息,包括面积、价格和业主信息",
"category": "Sales",
"author": "Your Name",
"depends": ["base", "sale"],
"data": [
"security/ir.model.access.csv",
"views/estate_property_views.xml",
],
"installable": True,
"application": True,
}六、关键开发技巧与最佳实践
模型设计原则
- 单一职责原则:每个模型应有明确的业务含义
- 避免过度规范化:适当冗余可提高查询效率
- 合理使用计算字段:平衡计算性能与存储空间
性能优化技巧
- 批量操作:使用
browse()和search()代替循环查询 - 字段选择:使用
fields参数指定只需的字段 - 避免 N+1 查询:使用
read()或browse()一次性获取关联数据
- 批量操作:使用
前端体验优化
- 合理布局:将相关字段分组显示
- 适当使用只读:对计算字段设置
readonly="1" - 状态可视化:使用
decoration-xxx属性直观显示状态
安全最佳实践
- 最小权限原则:只授予用户完成工作所需的最小权限
- 记录规则:使用
ir.rule实现数据隔离 - 字段权限:对敏感字段设置字段级权限
调试技巧
- 日志记录:使用
_logger记录关键操作 - 断点调试:在 Python 代码中使用
import pdb; pdb.set_trace() - 浏览器开发者工具:检查前端元素和网络请求
- 日志记录:使用
掌握这些参数和技巧,就能比较顺手地开发 Odoo 模块,搭出功能完整、用起来也不别扭的业务应用。后续我会再补一些实际项目里踩到的坑(比如继承链冲突、compute 字段与 store 的取舍、onchange 在批量场景下失效之类)。
