拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Odoo 17自定义仪表盘开发实战:后端聚合与OWL前端渲染全解析

简介面向Odoo17开发者的自定义仪表盘示例源码包聚焦如何通过Python控制器、XML视图与JavaScript前端协作构建可灵活配置的业务看板适合在Odoo17环境中直接安装测试。整个压缩包仅16KB共25个文件以Python逻辑、XML视图、JavaScript脚本三类代码为主体并含有样式表、缓存文件、说明文档和示例图标目录结构一目了然控制器、视图、前端静态资源等按模块规范分层组织便于对照学习。当前已有252人学习下载尤其适合正在熟悉该框架、希望快速掌握自定义仪表盘实现路径的初中级开发人员。通过其中代码可了解模块清单声明、控制器路由设计、视图模板注入以及前端资源加载方式结合自带说明文档逐步解读能够理顺后端数据提供与前端图表展示的关系减少盲目摸索的时间并能将示例改造后迁移到自己的业务模块中直接复用这套开发思路。1. 项目概述与设计思路1.1 为什么你的Odoo需要一个自定义仪表盘做Odoo开发这么久我见过很多企业上线了Odoo 17但业务数据始终停留在“能看但不好看”的阶段。销售订单要进后端翻报表库存周转要去仓库模块找财务指标还得让财务部单独导Excel——每个模块都有自己的列表视图和搜索条件但管理层真正关心的是一个页面能把所有关键指标串起来。这时候自定义仪表盘就成了刚需。Odoo官方的仪表盘功能确实能用透视表和图形视图在大多数基础场景下足够但一旦遇到跨模块数据聚合、定制化指标计算、按登录用户区分展示范围这类需求官方方案就开始吃力了。更别提那种想在一屏内展示销售趋势、库存预警、客户新增数量的“驾驶舱”式界面官方仪表盘做出来的效果总差那么点意思。开发自定义仪表盘就是自己定义一个视图通过后端控制器集中统计业务数据再由前端JS按定制化样式渲染。这个方案能解决的问题很明确一是跨模块聚合数据二是完全自定义的展示逻辑三是能按业务角色精准控制看什么、怎么看。适合谁看有Python和JavaScript基础的Odoo二次开发工程师、想要做内部管理系统升级的实施顾问、以及企业里负责数字化运营但又受限于官方功能的人。1.2 技术方案选型的关键考量自定义仪表盘在Odoo 17里至少有三种实现路线我挨个测试过各有优劣。第一条路是纯客户端模块开发前端高度定制、视觉效果最好但开发量非常大。从一个HTML/JS/CSS静态模板开始需要自己实现数据请求逻辑、图表渲染、样式适配一套下来碰上复杂指标计算就没法聚焦业务本身了。第二条路是官方仪表盘加扩展开发量小、天然兼容但灵活度受限官方仪表盘在自定义样式、复杂交互上有明显的天花板。第三条路是折中的后端负责统计计算前端负责展示。服务器端写Controller集中处理数据查询和聚合计算客户端用Owl框架写一个仪表盘视图把计算结果渲染成图表和指标卡片。这套方案兼顾了开发效率和定制空间适合绝大多数企业的业务仪表盘需求。我选了第三条路。为什么因为它把职责切得很干净——数据抽取和计算是Python的长项Odoo的ORM已经帮我们做了大量跨模块联查的工作前端只需专注布局和交互OWL组件框架又是Odoo 15以后官方主推的前端架构写起来顺手还不会过时。这种“后重前轻”的方案正好匹配大多数业务系统仪表盘的真实复杂度。这套架构能规避的最大问题是“前端算数”——如果在JavaScript里处理大量业务数据的聚合逻辑代码会变得极难维护而且浏览器端性能也撑不住大数据量。2. 环境准备与核心配置2.1 开发环境搭建与模块骨架在动手写代码之前先把环境搭对不然后面一堆隐藏问题会让你怀疑人生。我用的Odoo 17版本是社区版Python 3.10以上前端环境依赖Node.js 16以上用于编译静态资源。模块骨架方面自定义仪表盘模块的目录结构我习惯这样组织custom_dashboard/ ├── __init__.py ├── __manifest__.py ├── controllers/ │ ├── __init__.py │ └── dashboard_controller.py ├── models/ │ ├── __init__.py │ └── dashboard_statistics.py ├── views/ │ ├── dashboard_menus.xml │ └── dashboard_views.xml └── static/ └── src/ ├── components/ │ └── dashboard/ │ ├── dashboard.js │ ├── dashboard.xml │ └── dashboard.scss └── js/ └── dashboard_action.js清单文件里需要声明依赖模块和静态资源路径就像这样{ name: Custom Dashboard, version: 17.0.1.0.0, depends: [base, web, sale, stock, contacts], assets: { web.assets_backend: [ custom_dashboard/static/src/js/dashboard_action.js, custom_dashboard/static/src/components/dashboard/dashboard.js, custom_dashboard/static/src/components/dashboard/dashboard.xml, custom_dashboard/static/src/components/dashboard/dashboard.scss, ], }, }implements的关键是web.assets_backend不声明这个你的前端文件在后台界面里根本加载不进来。另外如果你不确定要用哪些模块的数据建议先只声明base和web后续按需加免得模块耦合太重。2.2 数据模型设计与权限配置仪表盘本身不需要建存储业务数据的表但需要有一个“虚拟模型”来承载前端请求数据时的接口逻辑以及处理权限划分。我这里的示例只定义一个非常简单的模型用transient之类的存储数据会引入额外复杂性所以直接用AbstractModel就够了from odoo import models class DashboardStatistics(models.AbstractModel): _name dashboard.statistics _description Dashboard Statistics Aggregation这个模型不建物理表目的是注册一个能够在自定义Controller里调用的服务层入口也可以在需要时复用其方法做定时汇总。权限配置这块我强烈建议不要在ir.model.access.csv里为抽象模型配置全局权限因为根本没有对应的数据表权限模型不匹配会报异常。正确做法是在Controller里通过self.env.user.has_group()来判断用户角色做到按角色返回不同数据范围。仪表盘要加权限一定要在菜单层级和Controller内双重控制光靠前端隐藏按钮不够安全。3. 后端数据统计接口的开发3.1 写数据聚合的核心Controller仪表盘的数据不可能走官方读视图的接口。我们需要一个自定义路由前端通过JSON-RPC请求由后端执行跨模块统计。这是整个项目中最核心、也最容易出错的部分。先看一个销售订单月度趋势统计的实现import json from datetime import datetime, timedelta from odoo import http from odoo.http import request class DashboardController(http.Controller): http.route(/api/dashboard/statistics, typejson, authuser) def get_dashboard_statistics(self, **kwargs): 统一的数据入口按参数返回不同维度统计数据 user request.env.user if not user.has_group(base.group_system): # 普通用户只看自己相关的数据 domain [(user_id, , user.id)] else: domain [] sale_obj request.env[sale.order].sudo() # 本月订单数与销售额 now datetime.now() month_start now.replace(day1, hour0, minute0, second0, microsecond0) month_domain domain [(confirmation_date, , month_start.strftime(%Y-%m-%d %H:%M:%S))] month_orders sale_obj.search_count(month_domain [(state, in, [sale, done])]) # 聚合计算 orders sale_obj.search(month_domain [(state, in, [sale, done])]) month_amount sum(orders.mapped(amount_total)) return { month_orders: month_orders, month_amount: month_amount, }这里有几个关键点值得展开第一typejson绝不能省。如果你的前端用Odoo的this.orm.call默认就是JSON格式不匹配会直接报Bad Request。第二sudo()的用法要慎重。我这里的示例先调了sudo()提升权限是因为仪表盘需要跨模块读取数据若用户没有对应模块的访问权限会被ORM的安全机制拦截。但sudo()是一把双刃剑——必须配合Controller开头那段角色判断使用否则权限就形同虚设了。第三月份的时间域比较有个容易踩的坑我踩过的坑是直接用now.month比较结果每到下个月数据就归零。正确做法是构造一个明确的起始时间字符串让数据库去做比较如果你想把月度对比也放进仪表盘可以多加一层时间域。举例来说最近30天销售额需要动态计算我一般这样处理# 最近30天订单趋势 days30_ago (now - timedelta(days30)).strftime(%Y-%m-%d %H:%M:%S) day_domain domain [(confirmation_date, , days30_ago)] day_orders sale_obj.search(day_domain [(state, in, [sale, done])]) daily_summary {} for order in day_orders: day_key order.confirmation_date[:10] daily_summary[day_key] daily_summary.get(day_key, 0) order.amount_total这段代码的逻辑是先把时间域切成“日”为单位用字典聚合每天的总额前端拿到的就是一个可以直接画柱状图的键值对。这种预聚合的方式比前端拿到原始订单列表再人为逐条计算要高效得多也避免了一次性传输大量数据。3.2 多维度统计与参数计算给仪表盘做数据接口时不能把指标都写死。我的经验是用一个入口带多个可选参数前端按需请求后端按参数分支各模块的统计逻辑内聚在一个方法里。再来看一个扩展示例把库存预警和客户新增都揉进统一接口里http.route(/api/dashboard/overview, typejson, authuser) def get_overview_data(self, **kwargs): 聚合总览数据销售、库存、客户、待办 env request.env # 库存预警低于最小库存量的产品 product_domain [(qty_available, , min_qty)] if env.user.has_group(stock.group_stock_manager) else [] low_stock_products env[product.product].sudo().search(product_domain, limit10) # 新增客户 month_start datetime.now().replace(day1).strftime(%Y-%m-%d %H:%M:%S) new_partners env[res.partner].sudo().search_count([ (create_date, , month_start), (is_company, , True), ]) # 待处理工单 open_tickets env[project.task].sudo().search_count([ (stage_id.is_closed, , False), (user_ids, in, [request.env.user.id]), ]) if project.task in env else 0 return { low_stock: [{name: p.name, qty: p.qty_available, min_qty: p.min_qty} for p in low_stock_products], new_partners: new_partners, open_tickets: open_tickets, }在实际统计中有一个很容易被忽视的细节model是否存在要判断。比如project.task模型只有安装了project模块才在env里可访问。直接用env[project.task]访问一个不存在的模型Python层面不会立刻报错但在调search_count时就会抛KeyError。所以我在代码里加了成员判断if project.task in env如果没装项目模块就返回0这样接口在所有环境里都能正常响应。这个多维统计接口的输出结构务必设计成“前端拿到就能直接渲染”的形态。低库存产品返回的是字典列表而不是ORM记录集用户亲和度更高JSON序列化也更稳定。序列化时尤其注意datetime类型字段Odoo自己的JSON解析器能处理大部分但如果你传进去一个裸的date对象通过自定义路由返回时可能会序列化失败需要手动转成字符串。4. 前端视角视图与组件的实现4.1 注册自定义Action与视图类型后端接口就绪后前端要做两件事第一注册一个自定义Action映射到我们的仪表盘视图第二写一个OWL组件负责页面渲染和请求数据。Action的定义可以在XML中完成?xml version1.0 encodingutf-8? odoo record idaction_dashboard modelir.actions.act_window field namename自定义仪表盘/field field nametypeir.actions.act_window/field field nameres_modeldashboard.statistics/field field nameview_modedashboard/field field nametargetmain/field /record /odoo这里核心是view_mode值设为dashboard这个值需要在前端注册视图类型时对应。页面开启时会去找一个名字为DashboardView的视图类来渲染。在JavaScript里注册自定义视图最好扩展AbstractAction/** odoo-module **/ import { AbstractAction } from web/abstract_action; import { registry } from web/core/registry; import { Dashboard } from ../components/dashboard/dashboard; export class DashboardAction extends AbstractAction { setup() { super.setup(); this.displayName 自定义仪表盘; } async start() { await super.start(); this.DashboardComponent new Dashboard(this, { action: this.action, }); this.DashboardComponent.mount(this.el); } destroy() { if (this.DashboardComponent) { this.DashboardComponent.destroy(); } super.destroy(); } } registry.category(actions).add(dashboard.action, DashboardAction);这里有个新手很容易卡壳的点如果你直接在XML里写一个ir.actions.client然后把tag设为dashboard.action那就不需要上面的AbstractAction扩展了那是另一种写法。但如果像我现在这样用的是ir.actions.act_window加自定义view_mode则必须写这个前端注册逻辑来对接视图。AbstractAction方案的好处是不需要单独处理菜单权限之外的视图解析所有点击菜单后的逻辑都控制在start()里面你能精确管理前端的加载状态、错误弹窗和数据刷新频率。4.2 用OWL组件编写仪表盘界面OWL组件是Odoo 17前端的基石编写方式简单直接。先在dashboard.xml里声明组件模板?xml version1.0 encodingUTF-8? templates xml:spacepreserve t t-namedashboard.Dashboard owl1 div classo_dashboard !-- 顶部指标卡片区域 -- div classo_dashboard_cards div classo_dashboard_card t-foreachcardData t-ascard t-keycard_index span classo_card_label t-esccard.label/ span classo_card_value t-esccard.value/ /div /div !-- 图表区域 -- div classo_dashboard_charts div classo_chart_box t-att-title销售趋势 !-- 这里可以嵌入 Chart.js 或自定义的图表渲染 -- canvas t-refsalesTrendChart/canvas /div /div /div /t /templates对应的OWL组件逻辑/** odoo-module **/ import { Component } from odoo/owl; export class Dashboard extends Component { static template dashboard.Dashboard; setup() { this.cardData []; this.loadData(); } async loadData() { const result await this.env.services.orm.call( dashboard.statistics, get_dashboard_data, [] ); this.cardData [ { label: 本月订单, value: result.month_orders }, { label: 本月销售额, value: result.month_amount }, { label: 库存预警, value: result.low_stock_count }, { label: 新增客户, value: result.new_partners }, ]; } }这样一个自定义仪表盘的骨架已经完整了。从菜单点击进入触发自定义Action加载OWL组件组件向后端发起JSON-RPC请求后端聚合数据并返回前端渲染指标卡片。整条链路在Odoo 17中跑起来非常流畅。不过这里还要提醒一个坑orm.call的方法名必须和Model里定义的方法名一致而且要确保dashboard.statistics这个模型没有受访问权限配置影响。如果你在Controller里用env[dashboard.statistics]而这个模型没有在任何ir.model.access.csv中声明那么普通用户调用会因权限不足被拒。我的做法是给这个抽象模型加一条基础权限让所有登录用户都能“读取”真正业务数据权限还是在Controller里通过用户组判断。这样既不会报权限异常又不会泄露数据。id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_dashboard_statistics_user,dashboard.statistics.user,model_dashboard_statistics,base.group_user,1,0,0,0这个细节非常重要——如果不加你会看到前端一切正常后端也写了统计逻辑但浏览器控制台报Record does not exist or access rules forbids reading之类的错误排查半天发现是权限配置没跟上。5. 常见问题与排查技巧实录5.1 后端数据聚合的性能优化在实际项目中仪表盘页面往往需要展示大量数据。如果每次前端刷新都实时跑全量聚合SQL数据库压力会非常大。我遇到过最典型的场景是销售历史数据达到几十万条时一个销售额趋势统计接口响应时间跑到好几秒页面转圈转得让人想砸电脑。优化的思路有三个方向数据集筛选、缓存、预计算。数据集筛选是最好实现的在Controller里根据用户选择的日期范围、仓库维度、产品分类等条件尽量把参与聚合的recordset缩小。前面的示例已经体现了这一点所有统计都加了时间域绝不扫描全表。缓存可以考虑用odoo.tools.cache或者request.env[ir.config_parameter]做一个短时缓存。如果指标是每小时更新一次的完全没必要让每个用户每次点击都实时算。我在一个实际项目中做过缓存把销售额统计缓存时间设为10分钟后端接口平均响应时间从2秒降到200毫秒不到。预计算则更狠一些——用定时任务ir.cron在凌晨把昨日的统计指标算好存到一张专门的统计表白天仪表盘直接读这张表。这种方式适合“昨天及以前的数据不需要实时精确”的业务场景绝大多数管理驾驶舱满足这个条件。5.2 前端渲染显示异常怎么办仪表盘最常见的三类前端问题样式错乱、数据加载不出来、组件在路由切换时不刷新。样式错乱一般是因为Odoo后台自带一套bootstrap样式你的自定义CSS优先级不够或者被覆盖。解决办法是对关键容器增加更具体的CSS选择器比如div.o_dashboard .o_dashboard_cards并配合!important处理个别冲突。这个手段要克制!important用多了后面维护会很难受。数据加载不出来先打开浏览器开发者工具看Network面板重点看返回的状态码。如果是403或404先检查Controller路由注册是否成功、URL里的模块名是否正确。如果是500把日志切到DEBUG级别Odoo的异常日志会直接打印在你的终端窗口定位比在浏览器里瞎猜快得多。路由切换不刷新问题多半是因为OWL组件挂在父容器上没有在willUnmount时清理干净。检查你的组件是否继承了Component并正确实现了destroy钩子。也可以试试在DashboardAction.start()里主动调用this.DashboardComponent.reset()之类的重置逻辑强制重新加载数据。5.3 多公司环境下如何做到数据隔离多公司环境是个隐性杀手。Odoo的company_id字段是自动注入到查询域中的但你如果用sudo()跨过了权限检查公司隔离也可能会被跳过严重时出现A公司看到了B公司的销售额。如果要严格隔离必须主动在查询域里加上公司限制companies request.env.user.company_ids.ids domain [(company_id, in, companies)]这段代码放在所有search之前确保无论是否sudo()数据范围都限定在当前用户可访问的公司列表内。千万别天真地以为Odoo的record rules会替你做所有事——一旦在ORM中显式调用sudo()那些基于rule的记录级权限就会失效这时候靠代码自觉就是唯一的防线。5.4 图表数据格式兼容性的调整如果你在仪表盘里用了第三方前端图表库比如Chart.js、ECharts那么后端返回的数据格式必须和这些库的期望形状对齐。Chart.js的折线图喜欢的数据结构是{ labels: [...], datasets: [{ data: [...] }] }ECharts则更灵活但通常也需要预先定义好xAxis.data和series.data。我的做法是后端统一返回原始维度数据比如按天的销售额键值对然后在前端做一次映射const labels Object.keys(dailySales).map(date date.slice(5)); const values Object.values(dailySales); const chartData { labels: labels, datasets: [{ label: 销售额, data: values, borderColor: #875A7B, backgroundColor: rgba(135, 90, 123, 0.2), }], };数据格式这块最好在开发仪表盘之前就想清楚因为这直接决定了后端字段的返回结构。我在前几个项目里是随用随写结果每次调整前端图表库都要回头改后端接口白白浪费了很多联调时间。6. 源码示例和扩展建议6.1 完整可运行的Dashboard组件代码示例把上述模块组装起来我整理了一个最小可运行的完整代码结构方便你直接放到自定义模块里测试。这里补上后端模型里的数据统计方法class DashboardStatistics(models.AbstractModel): _name dashboard.statistics _description Dashboard Statistics Abstract Model def get_dashboard_data(self): 供前端orm.call调用的数据统计入口 now datetime.now() month_start now.replace(day1, hour0, minute0, second0, microsecond0) sale_obj self.env[sale.order].sudo() month_orders sale_obj.search_count([ (confirmation_date, , month_start.strftime(%Y-%m-%d %H:%M:%S)), (state, in, [sale, done]) ]) month_amount sum(sale_obj.search([ (confirmation_date, , month_start.strftime(%Y-%m-%d %H:%M:%S)), (state, in, [sale, done]) ]).mapped(amount_total)) low_stock_count self.env[product.product].sudo().search_count([ (qty_available, , min_qty) ]) new_partners self.env[res.partner].sudo().search_count([ (create_date, , month_start.strftime(%Y-%m-%d %H:%M:%S)), (is_company, , True) ]) return { month_orders: month_orders, month_amount: round(month_amount, 2), low_stock_count: low_stock_count, new_partners: new_partners, }6.2 常见登录用户看到自己数据的实现仪表盘按用户显示数据是常见需求。如果你希望普通销售只看到自己的销售数据而销售总监看到整个团队的那就在Controller里加逻辑if not user.has_group(sales_team.group_sale_salesman_all_leads): domain [(user_id, , user.id)] else: domain []在Odoo 17里销售组权限用sales_team.group_sale_salesman_all_leads来判断是否查看所有销售线索和订单。这样你不需要为不同角色开发多个Controller统一入口加权限分支就够了前端完全不用感知后端权限逻辑彻底做到“一套UI多套数据”。6.3 后续扩展方向基础版仪表盘跑通后可以按这些方向做扩展更多图表类型引入后端返回多维度的时序数据配合ECharts渲染仪表盘式的“环形图”“雷达图”适合做企业健康度评分页面。自定义首页将仪表盘设置为用户登录后默认首页在用户偏好或菜单配置中指定action_dashboard为默认启动动作即可。定时邮件推送在Controller的统计逻辑基础上加一个ir.cron定时任务每天9点把关键指标表格通过邮件发给管理层。权限细化给不同角色配置不同的Card模块比如仓库管理员只看到库存预警销售总监只看到销售趋势这需要前端根据后端返回的menu_permissions字段控制卡片渲染范围。7. 实操心得与避坑总结整个自定义仪表盘的开发流程走下来我个人最大的体会是Odoo 17的框架对自定义开发已经足够友好真正费时间的不是写Python或JS代码而是理解Odoo的“权限模型 数据获取方式 前端组件生命周期”这三者之间的配合关系。事实上很多开发者在做Odoo图表页时习惯直接用ir.actions.client加上骨架视图就完事前后端各写各的、数据能显示就交差。但真正跑在企业生产环境里权限隔离和数据响应速度早晚会出来找你“讨债”。我的建议是宁可开始多花半小时把Controller的查询域、用户组判断、模型权限三件事一次做对也不要上线后天天被业务部门催“为什么这个人看不到那条数据”。一个对我帮助很大的小技巧是在开发阶段把Controller的返回结果先写成一个静态JSON存到前端变量里先把界面样式调好了再接入真实接口。这样能彻底将前后端联调的问题分开避免“界面不对又怀疑是数据问题数据不对又怀疑是界面问题”的双边折磨。等到样式全部确认无误后再一步到位替换为orm.call请求调试效率能提升一个量级。最后再分享一个针对Odoo 17的小细节OWL组件的t-foreach循环一定要带上t-key属性否则列表排序或筛选时会报一大堆warning严重时还会导致DOM渲染错乱。这个问题在我前几次开发中反复出现每次都是在某个角落忘了加t-key排查半天。希望看到这里的你不再踩这个坑。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门