在现代企业管理中,数据可视化是洞察业务、驱动决策的关键。Odoo 作为一个功能强大的 ERP 系统,存储了海量的业务数据。如何将这些数据以直观、美观的方式呈现给用户?集成第三方图表库无疑是最佳选择。
本文将以一个客户分析看板模块为例,手把手带您走过在 Odoo 17 中集成流行图表库 Apache ECharts 的全过程。我们将创建一个包含动态数据饼图和复杂静态图形的仪表盘,最终效果大致如下:
我们将学习到:
- 如何在 Odoo 17 的
assets
中正确引入外部 JavaScript 库。 - 如何使用 Python 控制器创建后端数据 API 接口。
- 如何构建 OWL 组件来承载和渲染 ECharts 图表。
- 如何通过 RPC 服务实现前后端数据交互。
- 如何将 OWL 组件注册为 Odoo 的客户端动作 (Client Action) 并创建菜单访问。
第一步:模块结构与清单文件 (__manifest__.py
)
万事开头难,但 Odoo 模块的开头总是从 __manifest__.py
开始。这个文件是模块的身份证,定义了它的名称、依赖、以及最重要的——需要加载的静态资源(Assets)。
这是我们集成 ECharts 的第一步,也是最关键的一步。
customer_dashboard/__manifest__.py
# customer_dashboard/__manifest__.py
{
'name': '客户分析看板 (ECharts)',
'version': '17.0.1.0',
'category': 'Sales',
'summary': '使用ECharts在Odoo 17中展示客户类型分布看板',
'author': 'Your Name',
'depends': ['web', 'base'], # 依赖web和base模块
'data': [
'views/dashboard_menus.xml',
],
'assets': {
'web.assets_backend': [
# 1. 优先引入 ECharts 库
'customer_dashboard/static/src/lib/echarts.min.js',
# 2. 引入我们的 OWL 组件JS文件
'customer_dashboard/static/src/components/customer_chart_dashboard.js',
# 3. 引入我们的 OWL 组件XML模板
'customer_dashboard/static/src/components/customer_chart_dashboard.xml',
],
},
'installable': True,
'application': True,
'license': 'LGPL-3',
}
核心解读:
depends
: 我们依赖web
模块,因为所有前端相关的功能都构建于其上。assets
: 这是集成的核心。- 我们在
web.assets_backend
这个资源包中添加文件。这表示这些资源将在 Odoo 的后端(即登录后看到的主界面)加载。 - 加载顺序至关重要:我们必须先加载
echarts.min.js
库文件,然后再加载使用该库的我们自己的组件customer_chart_dashboard.js
。否则,当我们的 JS 代码尝试调用echarts
对象时,它会因为尚未定义而报错。 - 我们还需要下载 ECharts 的
echarts.min.js
文件,并放置在customer_dashboard/static/src/lib/
目录下。
- 我们在
第二步:创建后端数据接口 (Python Controller)
我们的第一个图表需要展示“各销售员名下的客户数量”。这些数据存储在 Odoo 数据库中,因此我们需要创建一个后端 API 接口,让前端可以通过网络请求获取这些数据。在 Odoo 中,这通常通过 HTTP Controller 实现。
customer_dashboard/controllers/dashboard_controller.py
# customer_dashboard/controllers/dashboard_controller.py
from odoo import http
from odoo.http import request
import random # 这个例子中未使用,可以忽略
class CustomerDashboardController(http.Controller):
@http.route('/customer_dashboard/get_salesperson_data', type='json', auth='user')
def get_salesperson_data(self):
"""
获取不同销售员名下的客户数量。
"""
# 使用 Odoo ORM 的 read_group 方法高效地分组聚合数据
partners = request.env['res.partner'].read_group(
domain=[('is_company', '=', True)], # 只统计公司类型的伙伴
fields=['user_id'],
groupby=['user_id']
)
echarts_data = []
for group in partners:
# user_id 是一个 (id, name) 的元组
if group['user_id']:
name = group['user_id'][1]
else:
name = '未分配'
# 构造成 ECharts 饼图需要的 {value, name} 格式
echarts_data.append({
'value': group['user_id_count'],
'name': name[0] + '**' if name != '未分配' else name # 简单脱敏
})
return echarts_data
核心解读:
@http.route(...)
: 装饰器定义了一个新的路由。'/customer_dashboard/get_salesperson_data'
: 这是前端将要请求的 URL。type='json'
: 指定这是一个 JSON 接口。Odoo 会自动处理请求和响应的序列化/反序列化。auth='user'
: 要求请求者必须是已登录的 Odoo 用户。
read_group
: 这是一个非常强大的 Odoo ORM 方法,它直接在数据库层面执行GROUP BY
操作,比搜索所有记录再在 Python 中循环计数要高效得多。- 数据格式化: 我们将
read_group
返回的结果处理成 ECharts 饼图series.data
所需的标准格式:[{value: 335, name: '张三'}, ...]
。
第三步:定义前端视图结构 (OWL Template - XML)
现在我们有了数据源,接下来需要构建前端的用户界面。在 OWL 中,组件的结构由 XML 模板定义。
customer_dashboard/static/src/components/customer_chart_dashboard.xml
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="customer_dashboard.customer_chart_action" owl="1">
<div class="o_customer_dashboard">
<div class="o_control_panel">
<h3>客户分析看板</h3>
</div>
<div class="o_content d-flex flex-row flex-nowrap align-items-start">
<!-- 第一个图表: 销售员客户分布 -->
<div class="o_graph_container w-50 p-2">
<div class="chart_container" style="width: 100%; height: 600px;" t-ref="chart"/>
</div>
<!-- 第二个图表: 椭圆图 -->
<div class="o_graph_container w-50 p-2">
<div class="chart_container" style="width: 100%; height: 700px;" t-ref="ellipseChart"/>
</div>
</div>
</div>
</t>
</templates>
核心解读:
t-name
: 定义了模板的唯一标识符,JS 组件将通过这个名称引用它。- Layout: 使用 Bootstrap 5 的 Flexbox (
d-flex
,flex-row
) 创建了一个左右布局,每个图表容器占据 50% 的宽度 (w-50
)。 t-ref
: 这是 OWL 的一个重要指令。t-ref="chart"
和t-ref="ellipseChart"
为这两个div
元素设置了引用名称。在 JS 代码中,我们可以通过这些名称直接获取到对应的 DOM 元素,这是初始化 ECharts 实例所必需的。
第四步:编写核心前端逻辑 (OWL Component - JavaScript)
这是所有魔法发生的地方。我们将编写一个 OWL 组件,它负责获取数据、初始化 ECharts 实例并配置图表选项。
customer_dashboard/static/src/components/customer_chart_dashboard.js
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Component, onWillStart, onMounted, useRef } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
class CustomerChartDashboard extends Component {
static template = "customer_dashboard.customer_chart_action";
setup() {
// 1. 引入服务和设置引用
this.rpc = useService("rpc");
this.chartRef = useRef("chart");
this.ellipseChartRef = useRef("ellipseChart");
this.chart = null;
this.ellipseChart = null;
// 2. 在组件挂载前获取数据
onWillStart(async () => {
// 使用 RPC 服务调用后端控制器
this.chartData = await this.rpc("/customer_dashboard/get_salesperson_data", {});
if (this.chartData.error) {
console.error("获取饼图数据失败:", this.chartData.error);
}
});
// 3. 在组件挂载到 DOM 后渲染图表
onMounted(() => {
if (this.chartData && !this.chartData.error) {
this.renderChart();
}
// 渲染第二个模拟数据的图表
this.renderEllipseChart();
});
}
renderChart() {
if (typeof echarts === 'undefined') {
console.error("ECharts library is not loaded.");
return;
}
// 通过 ref 获取 DOM 元素并初始化 ECharts
this.chart = echarts.init(this.chartRef.el);
const option = {
title: {
text: '各销售员客户数量分布',
subtext: '数据来源: Odoo',
left: 'center'
},
tooltip: { trigger: 'item' },
legend: { orient: 'vertical', left: 'left' },
series: [{
name: '销售员',
type: 'pie',
radius: '50%',
data: this.chartData, // 使用从后端获取的数据
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
};
this.chart.setOption(option);
}
// (renderEllipseChart 方法代码与原文一致,此处为简洁省略)
renderEllipseChart() {
if (typeof echarts === 'undefined') {
console.error("ECharts library is not loaded.");
return;
}
this.ellipseChart = echarts.init(this.ellipseChartRef.el);
// ... 此处是生成模拟数据和配置椭圆图的代码 ...
// 这个图表展示了如何用 ECharts 绘制复杂的、非传统的数据图形
// 它的数据是在前端直接生成的,未请求后端
const option = { /* ... 椭圆图的复杂配置 ... */ };
this.ellipseChart.setOption(option);
}
}
// 4. 将组件注册为客户端动作
registry.category("actions").add("customer_dashboard.customer_chart_action", CustomerChartDashboard);
核心解读:
setup()
: 组件的初始化入口。useService("rpc")
: 获取 Odoo 的 RPC 服务,用于与后端进行通信。useRef("chart")
: 创建对模板中t-ref="chart"
元素的引用。onWillStart
: 这是一个异步的生命周期钩子。它在组件渲染到 DOM 之前 执行,是获取初始数据的理想位置。我们在这里调用this.rpc
来执行后端控制器的get_salesperson_data
方法。onMounted
: 这个生命周期钩子在组件的 DOM 元素被完全挂载到页面上之后执行。这是初始化需要操作 DOM 的库(如 ECharts)的最佳时机,因为此时this.chartRef.el
才能保证存在。
renderChart()
: 封装了饼图的渲染逻辑。它获取onWillStart
中取回的this.chartData
,并将其作为 ECharts 的配置项series.data
来设置图表。renderEllipseChart()
: 这个方法展示了 ECharts 的强大能力,通过前端 JS 算法生成数据点,并绘制出复杂的图形。它说明了并非所有图表都必须依赖后端实时数据。registry.category("actions").add(...)
: 这是将 OWL 组件与 Odoo 框架连接起来的最后一步,也是至关重要的一步。- 我们将
CustomerChartDashboard
组件注册到actions
注册表中。 "customer_dashboard.customer_chart_action"
是我们为这个动作指定的唯一标签 (Tag)。下一步中,我们将通过这个标签来调用它。
- 我们将
第五步:创建菜单入口 (Views XML)
最后,我们需要一个方法从 Odoo 界面上访问我们的看板。这通过定义一个客户端动作 (ir.actions.client
) 和相应的菜单项 (menuitem
) 来完成。
customer_dashboard/views/dashboard_menus.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 客户端操作 (Client Action) -->
<record id="action_customer_dashboard" model="ir.actions.client">
<field name="name">客户分析看板</field>
<field name="tag">customer_dashboard.customer_chart_action</field>
</record>
<!-- 顶级菜单 -->
<menuitem
id="menu_customer_dashboard_root"
name="客户看板"
sequence="10"/>
<!-- 子菜单 -->
<menuitem
id="menu_customer_dashboard_sub"
name="类型分布图"
parent="menu_customer_dashboard_root"
action="action_customer_dashboard"
sequence="10"/>
</odoo>
核心解读:
ir.actions.client
: 我们创建了一个客户端动作记录。<field name="tag">
: 它的值customer_dashboard.customer_chart_action
必须与我们在 JS 文件中注册组件时使用的标签完全一致。这就是 Odoo 知道点击菜单时应该加载哪个 OWL 组件的机制。
menuitem
: 我们创建了一个顶级菜单“客户看板”和一个子菜单“类型分布图”。子菜单的action
属性指向我们刚刚创建的客户端动作。
总结
至此,我们已经完整地构建了一个集成 ECharts 的 Odoo 17 模块。回顾一下整个流程:
- 配置
__manifest__.py
:通过assets
加载 ECharts 库和我们自己的组件文件。 - 创建 Python Controller:提供一个 JSON API 来从 Odoo 后端获取动态数据。
- 编写 OWL Template (XML):定义看板的 HTML 结构,并用
t-ref
标记出图表容器。 - 编写 OWL Component (JS):使用
onWillStart
获取数据,onMounted
初始化图表,并通过registry
将组件注册为一个 Action。 - 创建 Views XML:定义
ir.actions.client
并将其tag
与 JS 组件关联,然后创建菜单项使其可被访问。
这个模式不仅适用于 ECharts,同样可以推广到集成任何其他前端库(如 D3.js, Chart.js 等)。它充分利用了 Odoo 17 现代化前端架构的优势,实现了前后端职责分离,使得开发复杂、美观、高性能的用户界面成为可能。现在,就去动手尝试,为你的 Odoo 系统打造属于自己的数据驾驶舱吧!