Odoo 17 开发实战:集成 ECharts 打造客户分析看板

发布于:2025-07-12 ⋅ 阅读:(20) ⋅ 点赞:(0)

在现代企业管理中,数据可视化是洞察业务、驱动决策的关键。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',
}

核心解读:

  1. depends: 我们依赖 web 模块,因为所有前端相关的功能都构建于其上。
  2. 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

核心解读:

  1. @http.route(...): 装饰器定义了一个新的路由。
    • '/customer_dashboard/get_salesperson_data': 这是前端将要请求的 URL。
    • type='json': 指定这是一个 JSON 接口。Odoo 会自动处理请求和响应的序列化/反序列化。
    • auth='user': 要求请求者必须是已登录的 Odoo 用户。
  2. read_group: 这是一个非常强大的 Odoo ORM 方法,它直接在数据库层面执行 GROUP BY 操作,比搜索所有记录再在 Python 中循环计数要高效得多。
  3. 数据格式化: 我们将 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>

核心解读:

  1. t-name: 定义了模板的唯一标识符,JS 组件将通过这个名称引用它。
  2. Layout: 使用 Bootstrap 5 的 Flexbox (d-flex, flex-row) 创建了一个左右布局,每个图表容器占据 50% 的宽度 (w-50)。
  3. 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);

核心解读:

  1. setup(): 组件的初始化入口。
    • useService("rpc"): 获取 Odoo 的 RPC 服务,用于与后端进行通信。
    • useRef("chart"): 创建对模板中 t-ref="chart" 元素的引用。
    • onWillStart: 这是一个异步的生命周期钩子。它在组件渲染到 DOM 之前 执行,是获取初始数据的理想位置。我们在这里调用 this.rpc 来执行后端控制器的 get_salesperson_data 方法。
    • onMounted: 这个生命周期钩子在组件的 DOM 元素被完全挂载到页面上之后执行。这是初始化需要操作 DOM 的库(如 ECharts)的最佳时机,因为此时 this.chartRef.el 才能保证存在。
  2. renderChart(): 封装了饼图的渲染逻辑。它获取 onWillStart 中取回的 this.chartData,并将其作为 ECharts 的配置项 series.data 来设置图表。
  3. renderEllipseChart(): 这个方法展示了 ECharts 的强大能力,通过前端 JS 算法生成数据点,并绘制出复杂的图形。它说明了并非所有图表都必须依赖后端实时数据。
  4. 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>

核心解读:

  1. ir.actions.client: 我们创建了一个客户端动作记录。
    • <field name="tag">: 它的值 customer_dashboard.customer_chart_action 必须与我们在 JS 文件中注册组件时使用的标签完全一致。这就是 Odoo 知道点击菜单时应该加载哪个 OWL 组件的机制。
  2. menuitem: 我们创建了一个顶级菜单“客户看板”和一个子菜单“类型分布图”。子菜单的 action 属性指向我们刚刚创建的客户端动作。

总结

至此,我们已经完整地构建了一个集成 ECharts 的 Odoo 17 模块。回顾一下整个流程:

  1. 配置 __manifest__.py:通过 assets 加载 ECharts 库和我们自己的组件文件。
  2. 创建 Python Controller:提供一个 JSON API 来从 Odoo 后端获取动态数据。
  3. 编写 OWL Template (XML):定义看板的 HTML 结构,并用 t-ref 标记出图表容器。
  4. 编写 OWL Component (JS):使用 onWillStart 获取数据,onMounted 初始化图表,并通过 registry 将组件注册为一个 Action。
  5. 创建 Views XML:定义 ir.actions.client 并将其 tag 与 JS 组件关联,然后创建菜单项使其可被访问。

这个模式不仅适用于 ECharts,同样可以推广到集成任何其他前端库(如 D3.js, Chart.js 等)。它充分利用了 Odoo 17 现代化前端架构的优势,实现了前后端职责分离,使得开发复杂、美观、高性能的用户界面成为可能。现在,就去动手尝试,为你的 Odoo 系统打造属于自己的数据驾驶舱吧!

效果图

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到