跳至内容

ODOO数据文件(XML、CSV、SQL)是如何转换并加载到 Odoo 数据库

本文档详细解释 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)

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>

🎓 学习建议

对于初学者:

  1. 先理解基础概念:XML ID、模型、字段
  2. 从简单开始:先学习 <record> 标签
  3. 多看示例:查看 Odoo 标准模块的数据文件
  4. 逐步深入:掌握 <record> 后再学习 <template> 和 <menuitem>

关键理解点:

  • XML ID 系统:理解 XML ID 是如何映射到数据库 ID 的
  • noupdate 机制:理解为什么需要保护演示数据
  • eval 表达式:学习如何在 XML 中使用 Python 代码
  • 关系字段:掌握 Many2one、One2many、Many2many 的处理

调试技巧:

  1. 查看日志:--log-level=debug
  2. 使用 pprint 打印数据
  3. 检查 ir.model.data 表了解 XML ID 映射
  4. 使用 Schema 验证 XML 格式

📝 总结

convert.py 是 Odoo 数据加载的核心:

  1. 统一接口:处理 XML、CSV、SQL 三种格式
  2. 安全性:使用 safe_eval 防止恶意代码
  3. 灵活性:支持多种数据定义方式
  4. 智能转换:自动将 <template> 转换为 ir.ui.view 记录
  5. 错误处理:提供详细的错误信息

理解这个文件,就理解了 Odoo 模块是如何加载数据的!

ODOO模板组件
微信 masterjmz