本文档详细解释 Odoo 的
odoo/tools/convert.py
文件,这是 Odoo 数据导入和转换的核心模块。
🎯 文件的作用是什么?
想象一下,你开发了一个 Odoo 模块,里面有一些初始数据(比如菜单、视图、演示数据等)。这些数据都保存在 XML 或 CSV 文件中。当模块安装时,Odoo 需要把这些文件中的数据读取出来,并保存到数据库里。
convert.py****就是负责这个转换工作的核心文件!
它就像一个"翻译官",把人类可读的 XML/CSV 文件翻译成数据库能理解的格式。
📊 完整功能总结表
表1:核心类和方法
|------------------------------|---------|-------------|----------|
| 类/方法 | 行号 | 作用 | 重要程度 |
| ParseError
| 38-39 | 解析异常类 | ⭐ |
| _get_eval_context()
| 42-55 | 创建安全执行环境 | ⭐⭐⭐ |
| _fix_multiple_roots()
| 57-73 | 修复多根 XML | ⭐⭐ |
| _eval_xml()
| 75-203 | 核心解析器 | ⭐⭐⭐⭐⭐ |
| str2bool()
| 205-206 | 字符串转布尔 | ⭐ |
| nodeattr2bool()
| 208-214 | 节点属性转布尔 | ⭐ |
| xml_import
| 216-617 | XML 导入器类 | ⭐⭐⭐⭐⭐ |
| xml_import.__init__()
| 596-612 | 初始化导入器 | ⭐⭐⭐ |
| xml_import.make_xml_id()
| 233-236 | 生成完整 XML ID | ⭐⭐⭐ |
| xml_import._test_xml_id()
| 238-246 | 验证 XML ID | ⭐⭐ |
| xml_import.get_env()
| 217-231 | 获取特定环境 | ⭐⭐ |
| xml_import._tag_delete()
| 248-267 | 删除记录 | ⭐⭐⭐ |
| xml_import._tag_function()
| 269-273 | 调用函数 | ⭐⭐⭐ |
| xml_import._tag_menuitem()
| 275-334 | 创建菜单 | ⭐⭐⭐⭐ |
| xml_import._tag_record()
| 336-467 | 创建/更新记录 | ⭐⭐⭐⭐⭐ |
| xml_import._tag_template()
| 469-537 | 处理 QWeb 模板 | ⭐⭐⭐⭐⭐ |
| xml_import._tag_root()
| 549-580 | 处理根标签 | ⭐⭐⭐⭐ |
| xml_import.id_get()
| 539-543 | 获取记录 ID | ⭐⭐⭐ |
| xml_import.model_id_get()
| 545-547 | 获取模型和 ID | ⭐⭐ |
| convert_file()
| 620-650 | 主入口函数 | ⭐⭐⭐⭐⭐ |
| convert_xml_import()
| 715-746 | XML 导入 | ⭐⭐⭐⭐⭐ |
| convert_csv_import()
| 657-712 | CSV 导入 | ⭐⭐⭐⭐ |
| convert_sql_import()
| 653-654 | SQL 导入 | ⭐⭐ |
表2:支持的 XML 标签
|--------------|-------------------|------------|----------|
| 标签 | 处理器 | 作用 | 常用程度 |
| <record>
| _tag_record()
| 创建/更新记录 | ⭐⭐⭐⭐⭐ |
| <template>
| _tag_template()
| 定义 QWeb 模板 | ⭐⭐⭐⭐⭐ |
| <menuitem>
| _tag_menuitem()
| 创建菜单 | ⭐⭐⭐⭐⭐ |
| <field>
| _eval_xml()
| 定义字段值 | ⭐⭐⭐⭐⭐ |
| <delete>
| _tag_delete()
| 删除记录 | ⭐⭐⭐ |
| <function>
| _tag_function()
| 调用方法 | ⭐⭐⭐ |
| <odoo>
| _tag_root()
| 根标签 | ⭐⭐⭐⭐⭐ |
| <data>
| _tag_root()
| 数据容器 | ⭐⭐⭐⭐⭐ |
| <value>
| _eval_xml()
| 列表/元组元素 | ⭐⭐ |
表3:字段属性和类型
|-----------------|------------|------------------------------------------------|----------|
| 属性/类型 | 说明 | 示例 | 使用频率 |
| name
| 字段名(必需) | <field name="email">
| ⭐⭐⭐⭐⭐ |
| ref
| 引用 XML ID | ref="
base.cn"
| ⭐⭐⭐⭐⭐ |
| eval
| Python 表达式 | eval="datetime.now()"
| ⭐⭐⭐⭐ |
| search
| 搜索记录 | search="[('code', '=', 'CN')]"
| ⭐⭐⭐ |
| type
| 字段类型 | type="xml"
| ⭐⭐⭐⭐ |
| model
| 关联模型 | model="res.country"
| ⭐⭐⭐ |
| file
| 文件路径 | file="static/img/logo.png"
| ⭐⭐ |
| use
| 返回字段 | use="name"
| ⭐ |
| type="char"
| 字符串(默认) | - | ⭐⭐⭐⭐⭐ |
| type="int"
| 整数 | - | ⭐⭐⭐⭐ |
| type="float"
| 浮点数 | - | ⭐⭐⭐ |
| type="xml"
| XML 内容 | 用于视图 | ⭐⭐⭐⭐⭐ |
| type="html"
| HTML 内容 | 用于邮件模板 | ⭐⭐⭐ |
| type="base64"
| Base64 编码 | 用于文件 | ⭐⭐⭐ |
| type="file"
| 文件路径 | - | ⭐⭐ |
| type="list"
| 列表 | - | ⭐ |
| type="tuple"
| 元组 | - | ⭐ |
表4:record 标签属性
|---------------|--------|-------------------------------|--------|
| 属性 | 说明 | 示例 | 必需 |
| id
| XML ID | id="partner_demo"
| ✓ |
| model
| 模型名 | model="res.partner"
| ✓ |
| forcecreate
| 强制创建 | forcecreate="True"
| ✗ |
| context
| 上下文 | context="{'lang': 'zh_CN'}"
| ✗ |
| uid
| 执行用户 | uid="base.user_admin"
| ✗ |
表5:template 标签属性
|------------------|---------|---------------------------------|--------|
| 属性 | 说明 | 示例 | 常用 |
| id
| 模板 ID | id="my_page"
| ⭐⭐⭐⭐⭐ |
| name
| 模板名称 | name="My Page"
| ⭐⭐⭐ |
| inherit_id
| 继承模板 | inherit_id="website.layout"
| ⭐⭐⭐⭐⭐ |
| priority
| 优先级 | priority="20"
| ⭐⭐⭐ |
| groups
| 权限组 | groups="base.group_user"
| ⭐⭐ |
| active
| 是否激活 | active="False"
| ⭐⭐ |
| customize_show
| 显示在定制菜单 | customize_show="True"
| ⭐ |
| track
| 跟踪修改 | track="True"
| ⭐ |
| website_id
| 限定网站 | website_id="website.website1"
| ⭐ |
| primary
| 主模板模式 | primary="True"
| ⭐ |
表6:menuitem 标签属性
|------------|--------|-----------------------------------|--------|
| 属性 | 说明 | 示例 | 常用 |
| id
| 菜单 ID | id="menu_sale"
| ⭐⭐⭐⭐⭐ |
| name
| 菜单名称 | name="销售"
| ⭐⭐⭐⭐⭐ |
| parent
| 父菜单 | parent="base.menu_main"
| ⭐⭐⭐⭐⭐ |
| action
| 关联动作 | action="action_sale_order"
| ⭐⭐⭐⭐⭐ |
| sequence
| 排序 | sequence="10"
| ⭐⭐⭐⭐ |
| groups
| 权限组 | groups="base.group_user"
| ⭐⭐⭐⭐ |
| web_icon
| 图标 | web_icon="sale,static/icon.png"
| ⭐⭐⭐ |
| active
| 是否激活 | active="True"
| ⭐⭐ |
表7:data 标签属性
|-----------------|--------|-------------------------------|------------------|
| 属性 | 说明 | 示例 | 作用 |
| noupdate
| 升级时不更新 | noupdate="1"
| 保护用户修改的数据 |
| auto_sequence
| 自动序列号 | auto_sequence="True"
| 自动设置 sequence 字段 |
| uid
| 执行用户 | uid="base.user_admin"
| 以特定用户执行 |
| context
| 上下文 | context="{'lang': 'zh_CN'}"
| 设置上下文 |
表8:转换模式对比
|------------------|-------------|---------------|
| 特性 | init 模式 | update 模式 |
| 触发时机 | 模块安装 | 模块升级 |
| noupdate="1" 的行为 | 创建记录 | 跳过更新 |
| noupdate="0" 的行为 | 创建记录 | 更新记录 |
| CSV 文件 id 列 | 可选 | 必需 |
| 典型用途 | 首次安装 | 版本升级 |
表9:文件类型处理
|------------|---------|------------------------|----------|--------|
| 文件类型 | 扩展名 | 处理器 | 用途 | 性能 |
| XML | .xml
| convert_xml_import()
| 视图、数据、菜单 | 中等 |
| CSV | .csv
| convert_csv_import()
| 批量数据 | 快速 |
| SQL | .sql
| convert_sql_import()
| 直接 SQL | 最快 |
| JavaScript | .js
| 无(忽略) | 前端代码 | - |
表10:常用字段命令(Command)
|-----------------------------|---------|--------------------------------------|---------------------|
| 命令 | 说明 | 示例 | 用于 |
| Command.create({...})
| 创建关联记录 | [Command.create({'name': 'A'})]
| One2many, Many2many |
| Command.link(id)
| 关联已存在记录 | [Command.link(5)]
| Many2many |
| Command.unlink(id)
| 取消关联 | [Command.unlink(5)]
| Many2many |
| Command.delete(id)
| 删除关联记录 | [Command.delete(5)]
| One2many |
| Command.set([ids])
| 替换全部关联 | [Command.set([1,2,3])]
| Many2many |
| Command.clear()
| 清空全部关联 | [Command.clear()]
| One2many, Many2many |
| Command.update(id, {...})
| 更新关联记录 | [Command.update(5, {'name': 'B'})]
| One2many |
📦 第一部分:导入和基础定义(第 1-40 行)
1.1 导入的库
import base64 # 处理 base64 编码(比如图片)
import csv # 处理 CSV 文件
import io # 输入输出操作
import logging # 日志记录
import os.path # 文件路径操作
import pprint # 漂亮地打印数据
import re # 正则表达式
import subprocess # 运行外部命令
import warnings # 警告信息
from datetime import datetime, timedelta # 日期时间处理
通俗解释:这些都是 Python 的工具包,就像工具箱里的不同工具,每个都有特定的用途。
1.2 类型定义
ConvertMode = Literal['init', 'update']
IdRef = dict[str, int | Literal[False]]
通俗解释:
ConvertMode
:定义了两种转换模式- init(初始化模式):第一次安装模块时使用,所有数据都会被创建
- update(更新模式):模块升级时使用,会根据设置决定是否更新数据
IdRef
:一个字典,用来记住"XML ID" 和 "数据库 ID" 的对应关系- XML ID :比如
base.partner_admin
(人类可读) - 数据库 ID :比如
3
(数据库中的实际 ID)
- XML ID :比如
1.3 异常类
class ParseError(Exception):
...
通俗解释:当解析 XML 文件出错时,会抛出这个异常。就像你看不懂一本书时说"我读不懂"。
🔧 第二部分:辅助函数
2.1 _get_eval_context()
- 创建安全的执行环境(第 42-55 行)
作用:当 XML 文件中需要执行 Python 代码时,提供一个安全的环境。
def _get_eval_context(self, env, model_str):
context = dict(
Command=fields.Command, # 用于操作关系字段
time=time, # 时间模块
DateTime=datetime, # 日期时间类
datetime=datetime, # 日期时间类
timedelta=timedelta, # 时间差
relativedelta=relativedelta,# 相对时间差
version=release.major_version, # Odoo 版本
ref=self.id_get, # 引用其他记录的函数
pytz=pytz # 时区处理
)
if model_str:
context['obj'] = env[model_str].browse
return context
通俗例子:
假设你在 XML 中写了这样的代码:
<field name="date" eval="datetime.now()"/>
这个函数就确保 datetime.now()
能够正确执行,并且只能使用安全的函数(不能执行危险操作)。
为什么需要这个?
- 安全性:防止 XML 文件中执行恶意代码
- 方便性:提供常用的工具函数
2.2 _fix_multiple_roots()
- 修复多根节点(第 57-73 行)
作用:确保 XML 只有一个根节点。
def _fix_multiple_roots(node):
real_nodes = [x for x in node if not isinstance(x, SKIPPED_ELEMENT_TYPES)]
if len(real_nodes) > 1:
data_node = etree.Element("data")
for child in node:
data_node.append(child)
node.append(data_node)
通俗例子:
错误的 XML(两个根):
<field name="arch">
<div>第一个元素</div>
<div>第二个元素</div>
</field>
自动修复后:
<field name="arch">
<data>
<div>第一个元素</div>
<div>第二个元素</div>
</data>
</field>
为什么需要? XML 规范要求一个文档只能有一个根元素,这个函数自动修复不规范的 XML。
2.3 _eval_xml()
- 核心解析器(第 75-203 行)
作用:这是整个文件最核心的函数!它负责解析 XML 节点并返回对应的 Python 值。
支持的节点类型:
A. <field>
和 <value>
节点
1) search 属性 - 搜索记录
<field name="country_id" model="res.country" search="[('code', '=', 'CN')]"/>
解释 :在 res.country
模型中搜索代码为 'CN' 的国家,返回中国的记录 ID。
2) eval 属性 - 执行 Python 表达式
<field name="date" eval="datetime.now()"/>
<field name="price" eval="100 * 1.13"/>
解释:执行 Python 代码,第一个得到当前时间,第二个计算价格。
3) ref 属性 - 引用其他记录
<field name="partner_id" ref="base.partner_admin"/>
解释 :引用 XML ID 为 base.partner_admin
的记录。
4) file 属性 - 读取文件
<field name="image" type="base64" file="static/img/logo.png"/>
解释:读取图片文件并转换为 base64 编码。
B. 支持的字段类型
|----------|-----------|--------------------------------------------------|
| 类型 | 说明 | 例子 |
| char
| 字符串(默认) | <field name="name">张三</field>
|
| int
| 整数 | <field name="age" type="int">25</field>
|
| float
| 浮点数 | <field name="price" type="float">99.99</field>
|
| xml
| XML 内容 | 视图定义 |
| html
| HTML 内容 | 邮件模板 |
| base64
| Base64 编码 | 图片、文件 |
| list
| 列表 | 多个 <value>
元素 |
| tuple
| 元组 | 多个 <value>
元素 |
| file
| 文件路径 | 指向模块内的文件 |
C. <function>
节点 - 调用方法
<function model="ir.module.module" name="update_list"/>
解释 :调用 ir.module.module
模型的 update_list()
方法(更新模块列表)。
带参数的例子:
<function model="res.partner" name="write" eval="[[1, 2, 3], {'name': '新名字'}]"/>
解释:对 ID 为 1, 2, 3 的伙伴记录,将名字改为"新名字"。
2.4 str2bool()
和 nodeattr2bool()
- 字符串转布尔值(第 205-214 行)
def str2bool(value):
return value.lower() not in ('0', 'false', 'off')
def nodeattr2bool(node, attr, default=False):
if not node.get(attr):
return default
val = node.get(attr).strip()
if not val:
return default
return str2bool(val)
通俗解释:
把字符串转换为 True 或 False。
|------------------------------|-----------|
| 输入字符串 | 结果 |
| "True", "true", "1", "on" | True |
| "False", "false", "0", "off" | False |
| 空字符串或不存在 | default 值 |
例子:
<record id="view_1" active="true">
<record id="view_2" active="false">
<record id="view_3" active="1">
🏗️ 第三部分:xml_import 类(第 216-617 行)
这是整个文件的核心类!它负责解析 XML 文件并将数据导入数据库。
3.1 初始化方法 __init__()
(第 596-612 行)
def __init__(self, env, module, idref, mode, noupdate=False, xml_filename=''):
self.mode = mode # 'init' 或 'update'
self.module = module # 当前模块名,如 'sale'
self.envs = [env(...)] # 环境栈
self.idref = {} if idref is None else idref # XML ID 映射
self._noupdate = [noupdate] # 是否跳过更新
self._sequences = [] # 序列号栈
self.xml_filename = xml_filename # 当前文件名
self._tags = { # 标签处理器映射
'record': self._tag_record,
'delete': self._tag_delete,
'function': self._tag_function,
'menuitem': self._tag_menuitem,
'template': self._tag_template,
**dict.fromkeys(self.DATA_ROOTS, self._tag_root)
}
通俗解释:
创建一个 XML 导入器对象,设置好所有必要的配置。就像开始工作前,先准备好所有工具。
参数说明:
env
:Odoo 环境,可以访问数据库module
:当前模块名(如sale
,purchase
等)idref
:记住哪些记录已经创建了mode
:'init'(安装)或 'update'(升级)noupdate
:如果是 True,升级时不更新数据xml_filename
:当前处理的文件名
3.2 工具方法
A. make_xml_id()
- 生成完整 XML ID(第 233-236 行)
def make_xml_id(self, xml_id):
if not xml_id or '.' in xml_id:
return xml_id
return "%s.%s" % (self.module, xml_id)
通俗例子:
# 当前模块是 'sale'
make_xml_id('order_form') # 返回 'sale.order_form'
make_xml_id('base.partner_form') # 返回 'base.partner_form'(已经有模块前缀)
为什么需要? 确保每个 XML ID 都是唯一的,不会和其他模块冲突。
B. _test_xml_id()
- 验证 XML ID(第 238-246 行)
作用:检查 XML ID 格式是否正确,引用的模块是否已安装。
规则:
- 最多只能有一个点
.
- 引用其他模块时,该模块必须已安装
例子:
_test_xml_id('sale.order_form') # ✓ 正确
_test_xml_id('base.res.partner.form') # ✗ 错误(两个点)
_test_xml_id('uninstalled.record') # ✗ 错误(模块未安装)
C. get_env()
- 获取特定环境(第 217-231 行)
作用:根据 XML 节点的属性创建特定的 Odoo 环境。
<record id="partner_1" model="res.partner" uid="base.user_admin" context="{'lang': 'zh_CN'}">
解释:
uid="base.user_admin"
:以管理员身份创建记录context="{'lang': 'zh_CN'}"
:设置语言为简体中文
3.3 标签处理方法
A. _tag_delete()
- 删除记录(第 248-267 行)
作用:从数据库中删除记录。
方式 1:通过搜索条件删除
<delete model="res.partner" search="[('is_demo', '=', True)]"/>
解释:删除所有演示伙伴记录。
方式 2:通过 XML ID 删除
<delete model="res.partner" id="partner_demo"/>
解释 :删除 XML ID 为 partner_demo
的记录。
方式 3:两者结合
<delete model="res.partner" search="[('type', '=', 'temp')]" id="partner_temp_1"/>
解释:删除搜索到的记录 + 指定 ID 的记录。
安全机制:
- 如果搜索失败,只记录警告,不会中断
- 如果 ID 不存在,也只记录警告
B. _tag_function()
- 调用函数(第 269-273 行)
作用:执行模型的方法。
例子 1:无参数方法
<function model="ir.module.module" name="update_list"/>
解释:更新可用模块列表。
例子 2:带参数方法
<function model="res.partner" name="create" eval="[{'name': '张三', 'email': 'zhang@example.com'}]"/>
解释:创建一个新的伙伴记录。
例子 3:使用子节点传参
<function model="res.partner" name="write">
<value eval="[1, 2, 3]"/> <!-- 第一个参数:记录 IDs -->
<value name="name">新名字</value> <!-- kwargs 参数 -->
</function>
noupdate 检查:
如果 noupdate=True
且 mode='update'
,这个函数调用会被跳过。
C. _tag_menuitem()
- 创建菜单(第 275-334 行)
作用:创建或更新 Odoo 菜单项。
基础例子:
<menuitem id="menu_sale"
name="销售"
sequence="10"/>
完整例子:
<menuitem id="menu_sale_orders"
name="销售订单"
parent="menu_sale"
action="action_sale_order"
sequence="20"
groups="sales_team.group_sale_user"
web_icon="sale,static/description/icon.png"/>
属性说明:
|------------|-------------|----------------------------------------------|
| 属性 | 说明 | 例子 |
| id
| XML ID(必需) | menu_sale
|
| name
| 菜单名称 | 销售
|
| parent
| 父菜单 XML ID | base.menu_main
|
| action
| 关联的动作 | action_sale_order
|
| sequence
| 排序(数字越小越靠前) | 10
|
| groups
| 权限组(逗号分隔) | sales_team.group_sale_user,base.group_user
|
| web_icon
| 图标 | sale,static/description/icon.png
|
| active
| 是否激活 | True
或 False
|
嵌套菜单:
<menuitem id="menu_sale" name="销售">
<menuitem id="menu_sale_orders" name="订单"/>
<menuitem id="menu_sale_customers" name="客户"/>
</menuitem>
权限组的特殊用法:
<!-- 添加权限组 -->
<menuitem id="menu_1" groups="base.group_user"/>
<!-- 移除权限组(注意前面的减号)-->
<menuitem id="menu_2" groups="-base.group_user,sales_team.group_sale_manager"/>
D. _tag_record()
- 创建/更新记录(第 336-467 行)
这是最重要、最复杂的方法! 它负责创建或更新数据库记录。
基础结构:
<record id="partner_demo" model="res.partner">
<field name="name">演示伙伴</field>
<field name="email">demo@example.com</field>
</record>
工作流程:
1. 读取 model 和 id 属性
2. 检查 noupdate 标志
3. 遍历所有 <field> 子节点
4. 解析每个字段的值
5. 处理关系字段(many2one, one2many, many2many)
6. 调用 model._load_records() 保存
7. 更新 idref 映射
8. 处理嵌套记录(one2many)
字段值的设置方式:
1) 直接文本
<field name="name">张三</field>
2) 引用其他记录
<field name="country_id" ref="base.cn"/>
3) 搜索记录
<field name="country_id" model="res.country" search="[('code', '=', 'CN')]"/>
4) 执行 Python 代码
<field name="date" eval="datetime.now()"/>
<field name="price" eval="100 * 1.13"/>
5) Many2many 字段(多对多)
<field name="category_id" eval="[Command.set([ref('category_1'), ref('category_2')])]"/>
解释:设置分类为 category_1 和 category_2。
6) One2many 字段(一对多)- 嵌套记录
<record id="partner_1" model="res.partner">
<field name="name">公司A</field>
<field name="child_ids">
<record id="partner_1_contact_1" model="res.partner">
<field name="name">联系人1</field>
<field name="type">contact</field>
</record>
<record id="partner_1_contact_2" model="res.partner">
<field name="name">联系人2</field>
<field name="type">contact</field>
</record>
</field>
</record>
解释:创建一个公司,同时创建两个联系人。
7) Reference 字段(引用字段)
<field name="res_id" ref="sale.order_1"/>
解释 :Reference 字段存储的格式是 model,id
,如 sale.order,5
。
noupdate 机制:
<data noupdate="1">
<record id="demo_data" model="res.partner">
<field name="name">演示数据</field>
</record>
</data>
行为:
- init 模式(安装):创建记录
- update 模式(升级) :
- 如果
noupdate="1"
:跳过,不更新 - 如果
noupdate="0"
:更新记录
- 如果
为什么需要 noupdate?
用户可能修改了演示数据,升级模块时不希望被覆盖。
forcecreate 属性:
<record id="other_module.record_1" model="res.partner" forcecreate="True">
解释:允许在当前模块中创建/修改其他模块的记录。
E. _tag_template()
- 处理 QWeb 模板(第 469-537 行)
作用 :将 <template>
标签转换为 ir.ui.view
记录。
为什么要转换?
- QWeb 模板本质上就是特殊的视图
- 存储在
ir.ui.view
表中,类型为 'qweb' - 统一管理所有视图和模板
基础例子:
<template id="my_page">
<div class="container">
<h1>欢迎</h1>
</div>
</template>
转换后的等价形式:
<record id="my_page" model="ir.ui.view">
<field name="name">my_page</field>
<field name="key">module_name.my_page</field>
<field name="type">qweb</field>
<field name="arch" type="xml">
<t t-name="module_name.my_page">
<div class="container">
<h1>欢迎</h1>
</div>
</t>
</field>
</record>
转换过程详解:
1. 提取 template 的 id 属性
2. 生成完整的模板名称(module.template_id)
3. 将 <template> 标签改为 <t> 标签
4. 添加 t-name 属性
5. 创建 <record> 元素
6. 设置字段:name, key, type='qweb', arch
7. 处理其他属性(inherit_id, priority 等)
8. 调用 _tag_record() 保存
模板继承:
<template id="my_page_extended" inherit_id="module.my_page">
<xpath expr="//h1" position="replace">
<h1>新标题</h1>
</xpath>
</template>
支持的属性:
|------------------|----------------|---------------------|
| 属性 | 说明 | 例子 |
| id
| 模板 XML ID | my_template
|
| name
| 模板名称 | My Template
|
| inherit_id
| 继承的模板 | website.layout
|
| priority
| 优先级(数字越小优先级越高) | 16
|
| groups
| 访问权限组 | base.group_user
|
| active
| 是否激活 | True
或 False
|
| customize_show
| 是否在定制菜单显示 | True
或 False
|
| track
| 是否跟踪修改 | True
或 False
|
| website_id
| 限定网站 | website.website_1
|
| primary
| 主模板模式 | True
|
主题模块特殊处理:
if self.module.startswith('theme_'):
model = 'theme.ir.ui.view'
else:
model = 'ir.ui.view'
解释 :主题模块的模板存储在 theme.ir.ui.view
表中。
F. _tag_root()
- 处理根标签(第 549-580 行)
作用 :处理 <odoo>
, <data>
, <openerp>
根标签,遍历所有子标签。
支持的根标签:
<odoo>...</odoo>
<data>...</data>
<openerp>...</openerp> <!-- 旧版本兼容 -->
noupdate 属性:
<odoo>
<data noupdate="1">
<!-- 这里的数据在升级时不会更新 -->
</data>
<data noupdate="0">
<!-- 这里的数据在升级时会更新 -->
</data>
</odoo>
auto_sequence 属性:
<data auto_sequence="True">
<record id="view_1" model="ir.ui.view">...</record>
<record id="view_2" model="ir.ui.view">...</record>
<record id="view_3" model="ir.ui.view">...</record>
</data>
解释:自动为每条记录设置递增的 sequence 值(10, 20, 30...)。
错误处理:
- 捕获所有异常
- 提供详细的错误信息(文件名、行号、上下文)
- 使用 ParseError 包装原始异常
3.4 ID 管理方法
id_get()
- 获取记录 ID(第 539-543 行)
def id_get(self, id_str, raise_if_not_found=True):
id_str = self.make_xml_id(id_str)
if id_str in self.idref:
return self.idref[id_str]
return self.model_id_get(id_str, raise_if_not_found)[1]
工作流程:
1. 将相对 ID 转换为完整 ID
2. 先查缓存(idref)
3. 缓存没有则查数据库(model_id_get)
4. 返回数据库 ID
例子:
# 在 sale 模块中
id_get('order_1') # 查找 sale.order_1,返回如 42
id_get('base.partner_admin') # 查找 base.partner_admin,返回如 3
model_id_get()
- 获取模型和记录 ID(第 545-547 行)
def model_id_get(self, id_str, raise_if_not_found=True):
id_str = self.make_xml_id(id_str)
return self.env['ir.model.data']._xmlid_to_res_model_res_id(id_str, raise_if_not_found=raise_if_not_found)
返回值 :(model_name, record_id)
例子:
model_id_get('base.partner_admin')
# 返回:('res.partner', 3)
3.5 属性方法
env
属性(第 582-584 行)
@property
def env(self):
return self.envs[-1]
解释:返回当前环境。支持嵌套环境栈(可以临时切换用户或上下文)。
noupdate
属性(第 586-588 行)
@property
def noupdate(self):
return self._noupdate[-1]
解释 :返回当前的 noupdate 状态。支持嵌套(<data>
标签可以嵌套)。
next_sequence()
方法(第 590-594 行)
def next_sequence(self):
value = self._sequences[-1]
if value is not None:
value = self._sequences[-1] = value + 10
return value
解释:
- 如果启用了 auto_sequence,返回递增的序列号(10, 20, 30...)
- 否则返回 None
📄 第四部分:文件转换函数
4.1 convert_file()
- 主入口函数(第 620-650 行)
作用:根据文件扩展名,调用相应的转换器。
def convert_file(env, module, filename, idref, mode='update', noupdate=False, kind=None, pathname=None):
ext = os.path.splitext(filename)[1].lower()
with file_open(pathname, 'rb', env=env) as fp:
if ext == '.csv':
convert_csv_import(...)
elif ext == '.sql':
convert_sql_import(...)
elif ext == '.xml':
convert_xml_import(...)
elif ext == '.js':
pass # 忽略
else:
raise ValueError("Can't load unknown file type %s.", filename)
支持的文件类型:
|---------|------------------------|---------------|
| 扩展名 | 处理器 | 用途 |
| .xml
| convert_xml_import()
| 视图、数据、菜单等 |
| .csv
| convert_csv_import()
| 批量数据导入 |
| .sql
| convert_sql_import()
| 直接执行 SQL |
| .js
| 无(忽略) | JavaScript 文件 |
使用场景:
# 在模块安装时,Odoo 会自动调用
convert_file(env, 'sale', 'views/sale_views.xml', {}, mode='init')
convert_file(env, 'sale', 'data/demo.csv', {}, mode='init')
4.2 convert_xml_import()
- XML 导入(第 715-746 行)
作用:导入 XML 文件数据。
def convert_xml_import(env, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
# 1. 解析 XML 文件
doc = etree.parse(xmlfile)
# 2. 验证 XML 格式(使用 RelaxNG schema)
schema = os.path.join(config.root_path, 'import_xml.rng')
relaxng = etree.RelaxNG(etree.parse(schema))
relaxng.assert_(doc)
# 3. 创建 xml_import 对象并解析
obj = xml_import(env, module, idref, mode, noupdate=noupdate, xml_filename=xmlfile.name)
obj.parse(doc.getroot())
工作流程:
1. 解析 XML 文件
2. 使用 Schema 验证格式
3. 创建 xml_import 实例
4. 调用 parse() 处理
5. 遍历所有标签并调用相应处理器
Schema 验证:
- 使用
import_xml.rng
文件定义 XML 格式规范 - 检查标签名、属性是否合法
- 如果格式错误,提供详细的错误信息
错误处理:
try:
relaxng.assert_(doc)
except Exception:
# 使用 jingtrang 工具提供更友好的错误信息
if jingtrang:
subprocess.run(['pyjing', schema, xmlfile.name])
else:
# 显示 RelaxNG 错误日志
for e in relaxng.error_log:
_logger.warning(e)
4.3 convert_csv_import()
- CSV 导入(第 657-712 行)
作用:批量导入 CSV 数据。
CSV 文件要求:
- 分隔符 :逗号
,
- 引号 :双引号
"
- 编码:UTF-8
- 第一行:字段名(必须)
- update 模式 :必须包含
id
列
CSV 文件命名规则:
模型名-任意名称.csv
例如:
res.partner-customers.csv → 导入到 res.partner 模型
product.product-demo.csv → 导入到 product.product 模型
CSV 示例:
res.partner-demo.csv:
id,name,email,phone,country_id:id
partner_demo_1,张三,zhang@example.com,13800138000,base.cn
partner_demo_2,李四,li@example.com,13900139000,base.cn
partner_demo_3,王五,wang@example.com,13700137000,base.us
字段名格式:
|------------------|----------------------------|--------------------------------------|
| 格式 | 说明 | 例子 |
| name
| 普通字段 | name,email,phone
|
| country_id:id
| Many2one 引用 XML ID | base.cn |
| country_id
| Many2one 引用数据库 ID | 42
|
| category_id:id
| Many2many 引用 XML ID(多个用逗号) | category_1,category_2
|
| name@zh_CN
| 翻译字段(会被忽略) | 在翻译导入时处理 |
工作流程:
1. 从文件名提取模型名
2. 读取 CSV 第一行作为字段名
3. 过滤掉翻译字段(包含 @ 的)
4. 读取所有数据行
5. 调用 model.load() 批量导入
6. 检查错误消息
翻译字段处理:
id,name,name@zh_CN,name@en_US
product_1,Product A,产品A,Product A
解释 :name@zh_CN
和 name@en_US
在普通导入时会被忽略,在翻译导入时才处理。
错误处理:
result = env[model].with_context(**context).load(fields, datas)
if any(msg['type'] == 'error' for msg in result['messages']):
warning_msg = "\n".join(msg['message'] for msg in result['messages'])
raise Exception("导入失败:" + warning_msg)
导入上下文:
context = {
'mode': mode,
'module': module,
'install_mode': True,
'install_module': module,
'install_filename': fname,
'noupdate': noupdate,
}
4.4 convert_sql_import()
- SQL 导入(第 653-654 行)
作用:直接执行 SQL 文件中的语句。
def convert_sql_import(env, fp):
env.cr.execute(fp.read())
使用场景:
- 复杂的数据迁移
- 性能优化的批量操作
- 直接操作数据库结构
SQL 文件示例:
data/init.sql:
-- 创建索引
CREATE INDEX idx_partner_name ON res_partner(name);
-- 批量更新
UPDATE res_partner SET active = true WHERE type = 'contact';
-- 插入数据
INSERT INTO res_country (code, name) VALUES ('XX', 'Test Country');
注意事项:
- ⚠️ 危险操作:直接执行 SQL,绕过 ORM
- 不会触发计算字段、约束检查
- 不会记录审计日志
- 需要确保 SQL 兼容不同数据库(PostgreSQL)
- 建议只用于简单、必要的操作
💡 实际应用示例
示例1:创建演示数据
<odoo>
<data noupdate="1">
<!-- 创建一个公司 -->
<record id="company_demo" model="res.partner">
<field name="name">演示公司</field>
<field name="company_type">company</field>
<field name="email">demo@company.com</field>
<field name="phone">010-12345678</field>
<field name="country_id" ref="base.cn"/>
<field name="child_ids">
<!-- 嵌套创建联系人 -->
<record id="contact_demo_1" model="res.partner">
<field name="name">张经理</field>
<field name="type">contact</field>
<field name="email">zhang@company.com</field>
</record>
</field>
</record>
</data>
</odoo>
示例2:创建菜单结构
<odoo>
<data>
<!-- 顶级菜单 -->
<menuitem id="menu_sale"
name="销售"
sequence="10"
web_icon="sale,static/description/icon.png"/>
<!-- 子菜单 -->
<menuitem id="menu_sale_orders"
name="销售订单"
parent="menu_sale"
action="sale.action_orders"
sequence="10"/>
<menuitem id="menu_sale_customers"
name="客户"
parent="menu_sale"
action="base.action_partner_form"
sequence="20"/>
</data>
</odoo>
示例3:定义网页模板
<odoo>
<data>
<!-- 基础模板 -->
<template id="my_website_page" name="My Page">
<t t-call="website.layout">
<div class="container">
<h1>欢迎来到我的页面</h1>
<p>这是内容</p>
</div>
</t>
</template>
<!-- 继承并修改模板 -->
<template id="my_website_page_extended" inherit_id="my_website_page">
<xpath expr="//h1" position="replace">
<h1 class="text-primary">新标题</h1>
</xpath>
</template>
</data>
</odoo>
示例4:批量导入 CSV
res.partner-customers.csv:
id,name,email,phone,country_id:id,category_id:id
customer_1,北京公司,beijing@example.com,010-11111111,base.cn,"base.res_partner_category_0,base.res_partner_category_1"
customer_2,上海公司,shanghai@example.com,021-22222222,base.cn,base.res_partner_category_0
customer_3,深圳公司,shenzhen@example.com,0755-33333333,base.cn,base.res_partner_category_1
示例5:调用方法
<odoo>
<data noupdate="1">
<!-- 更新模块列表 -->
<function model="ir.module.module" name="update_list"/>
<!-- 创建记录 -->
<function model="res.partner" name="create">
<value eval="[{
'name': '自动创建的伙伴',
'email': 'auto@example.com'
}]"/>
</function>
<!-- 批量更新 -->
<function model="res.partner" name="write">
<value eval="[[ref('partner_1'), ref('partner_2')]]"/>
<value eval="{'active': True}"/>
</function>
</data>
</odoo>
🎓 学习建议
对于初学者:
- 先理解基础概念:XML ID、模型、字段
- 从简单开始 :先学习
<record>
标签 - 多看示例:查看 Odoo 标准模块的数据文件
- 逐步深入 :掌握
<record>
后再学习<template>
和<menuitem>
关键理解点:
- XML ID 系统:理解 XML ID 是如何映射到数据库 ID 的
- noupdate 机制:理解为什么需要保护演示数据
- eval 表达式:学习如何在 XML 中使用 Python 代码
- 关系字段:掌握 Many2one、One2many、Many2many 的处理
调试技巧:
- 查看日志:
--log-level=debug
- 使用
pprint
打印数据 - 检查
ir.model.data
表了解 XML ID 映射 - 使用 Schema 验证 XML 格式
📝 总结
convert.py
是 Odoo 数据加载的核心:
- 统一接口:处理 XML、CSV、SQL 三种格式
- 安全性:使用 safe_eval 防止恶意代码
- 灵活性:支持多种数据定义方式
- 智能转换 :自动将
<template>
转换为ir.ui.view
记录 - 错误处理:提供详细的错误信息
理解这个文件,就理解了 Odoo 模块是如何加载数据的!