本文目前仅提供英文版本。翻译即将推出。
属于我们的Data Analytics & BI系列
阅读完整指南Connecting Power BI to Odoo is the easy part. The hard part is modeling Odoo's normalized, multi-company, ID-keyed schema in a way that finance and operations leaders can actually slice. After deploying Power BI dashboards on top of Odoo for 60+ ECOSIRE clients running v15 through v19, the same dozen DAX patterns keep paying for themselves. This article is the field guide.
We assume you have already pulled account_move, account_move_line, sale_order, sale_order_line, stock_quant, stock_move, res_partner, res_company, product_product, product_template, and account_account into a Power BI dataset. Keys are joined, currencies converted, and the date table is marked as a date table. From there, the patterns below are the difference between a data dump and a dashboard a CFO will trust.
Key Takeaways
- DAX time intelligence works on Odoo only when you build a proper Date table and mark it correctly — Odoo's
create_dateis a timestamp, not a date.- Multi-company consolidation needs a
Companydimension and explicit currency conversion measures, not a column.- Account hierarchy in Odoo (parent_id self-reference) requires
PATH()andPATHITEM()to flatten for Power BI.- Cohort retention measures rely on
EARLIER()orVAR ... CALCULATEpatterns to capture the customer's first purchase.- Inventory aging buckets are a
SWITCH(TRUE())overDATEDIFFfromstock_quant.in_date.- DAX measures should never reference Odoo IDs directly — always go through dimension tables for filter context.
- Calculation Groups (Power BI Desktop 2026-01) replace dozens of duplicate measures (PY, YoY, MTD, QTD).
- For 10M+ row Odoo deployments, switch to Import mode with Incremental Refresh, not DirectQuery.
Pattern 1: A Bullet-Proof Date Table
Odoo's account_move.date is a date, but create_date and write_date are timestamps with timezone. Mixing them in a date table causes silent filter mismatches. Build a clean date table:
Date =
ADDCOLUMNS (
CALENDAR ( DATE ( 2018, 1, 1 ), DATE ( 2030, 12, 31 ) ),
"Year", YEAR ( [Date] ),
"Quarter", "Q" & QUARTER ( [Date] ),
"Month", FORMAT ( [Date], "MMM" ),
"Month Number", MONTH ( [Date] ),
"YearMonth", FORMAT ( [Date], "YYYY-MM" ),
"FY", IF ( MONTH ( [Date] ) >= 7,
"FY" & ( YEAR ( [Date] ) + 1 ),
"FY" & YEAR ( [Date] ) ),
"Is Weekday", WEEKDAY ( [Date], 2 ) <= 5,
"Is Current Month", FORMAT ( [Date], "YYYY-MM" ) = FORMAT ( TODAY (), "YYYY-MM" )
)
Then in the model: right-click Date → "Mark as date table." Without this step, SAMEPERIODLASTYEAR and DATEADD will fail silently on misaligned filters.
Pattern 2: Year-to-Date Sales (Multi-Year Comparable)
This is the bread-and-butter measure. The Odoo subtlety: state must be 'sale' or 'done' — quotations and cancelled orders should not count.
Sales YTD :=
CALCULATE (
SUMX (
FILTER (
'sale_order',
'sale_order'[state] IN { "sale", "done" }
),
'sale_order'[amount_total]
),
DATESYTD ( 'Date'[Date] )
)
Sales YTD PY :=
CALCULATE ( [Sales YTD], SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
Sales YTD vs PY % :=
DIVIDE ( [Sales YTD] - [Sales YTD PY], [Sales YTD PY] )
If your fiscal year is non-calendar, replace DATESYTD with DATESYTD('Date'[Date], "06-30") for a July-June fiscal year.
Pattern 3: Multi-Company Consolidation
Odoo lets each res_company have its own currency. Power BI must consolidate to a single reporting currency. Build a currency_rate table (one row per currency-date pair, a join column to Date), then:
Sales (Reporting Currency) :=
SUMX (
'sale_order',
'sale_order'[amount_total]
* RELATED ( 'currency_rate'[rate_to_usd] )
)
Add a Company slicer driven by res_company.name. Now leadership can flip between consolidated USD and local currency per entity.
Pattern 4: Account Hierarchy Flattening
Odoo's account_account.parent_id is self-referential — chart of accounts is a tree. Power BI does not slice well on parent_id directly. Flatten with PATH:
Account Path = PATH ( 'account_account'[id], 'account_account'[parent_id] )
Level 1 = LOOKUPVALUE ( 'account_account'[name], 'account_account'[id], PATHITEM ( [Account Path], 1, INTEGER ) )
Level 2 = LOOKUPVALUE ( 'account_account'[name], 'account_account'[id], PATHITEM ( [Account Path], 2, INTEGER ) )
Level 3 = LOOKUPVALUE ( 'account_account'[name], 'account_account'[id], PATHITEM ( [Account Path], 3, INTEGER ) )
Account Depth = PATHLENGTH ( [Account Path] )
Now use Level 1, Level 2, Level 3 as a hierarchical row in a matrix visual — collapsible P&L done.
Pattern 5: Customer Cohort Retention
Cohort analysis is the highest-value pattern most Odoo teams skip because the SQL is painful. DAX makes it readable.
First Purchase Date :=
CALCULATE (
MIN ( 'sale_order'[date_order] ),
ALLEXCEPT ( 'sale_order', 'res_partner'[id] )
)
Cohort Month :=
FORMAT ( [First Purchase Date], "YYYY-MM" )
Months Since Cohort :=
DATEDIFF ( [First Purchase Date], MAX ( 'sale_order'[date_order] ), MONTH )
Active Customers :=
DISTINCTCOUNT ( 'sale_order'[partner_id] )
Cohort Retention % :=
DIVIDE (
[Active Customers],
CALCULATE (
DISTINCTCOUNT ( 'res_partner'[id] ),
ALLEXCEPT ( 'res_partner', 'sale_order'[Cohort Month] )
)
)
Pivot rows = Cohort Month, columns = Months Since Cohort, values = Cohort Retention %. Conditional format the matrix and you have a triangle that finance and growth teams will both want.
Pattern 6: Inventory Aging Buckets
Odoo's stock_quant.in_date is when inventory was received. Aging buckets help spot dead stock.
Aging Bucket :=
SWITCH (
TRUE (),
DATEDIFF ( 'stock_quant'[in_date], TODAY (), DAY ) <= 30, "0-30 days",
DATEDIFF ( 'stock_quant'[in_date], TODAY (), DAY ) <= 60, "31-60 days",
DATEDIFF ( 'stock_quant'[in_date], TODAY (), DAY ) <= 90, "61-90 days",
DATEDIFF ( 'stock_quant'[in_date], TODAY (), DAY ) <= 180, "91-180 days",
"180+ days"
)
Inventory Value :=
SUMX (
'stock_quant',
'stock_quant'[quantity] * RELATED ( 'product_template'[standard_price] )
)
Stack the inventory value by aging bucket. Anything in the 180+ bucket needs a write-down conversation.
Pattern 7: Day Sales Outstanding (DSO)
DSO is the single most asked-for finance measure. The Odoo logic: only account.move of type out_invoice and out_refund count, posted state, and net of credit notes.
AR Outstanding :=
CALCULATE (
SUM ( 'account_move_line'[amount_residual] ),
'account_move_line'[account_internal_type] = "receivable",
'account_move'[state] = "posted"
)
Trailing 90d Sales :=
CALCULATE (
[Sales],
DATESINPERIOD ( 'Date'[Date], TODAY (), -90, DAY )
)
DSO :=
DIVIDE ( [AR Outstanding], [Trailing 90d Sales] / 90 )
A DSO above your payment terms (e.g., 60 on net-30) is a collections issue. Trend it weekly.
Pattern 8: Inventory Turnover
COGS :=
CALCULATE (
SUM ( 'account_move_line'[debit] ),
'account_account'[account_type] = "expense_direct_cost"
)
Average Inventory :=
AVERAGEX (
VALUES ( 'Date'[YearMonth] ),
[Inventory Value]
)
Inventory Turnover :=
DIVIDE ( [COGS], [Average Inventory] )
Days Inventory Outstanding :=
DIVIDE ( 365, [Inventory Turnover] )
A turnover below 4 in retail or below 8 in fast-moving consumer goods means inventory is parked. Drill by category to find the culprit.
Pattern 9: Composite Key Joins (Multi-Company Lookups)
Odoo schemas use (company_id, code) as a logical key for accounts, products, and warehouses. Power BI does not support multi-column relationships well — workaround:
'account_account'[Composite Key] = 'account_account'[company_id] & "|" & 'account_account'[code]
Add the same calculated column to the fact table you are joining and use single-column relationships on the composite key. This is the cleanest workaround until Power BI ships native composite keys.
Pattern 10: Sales Funnel Conversion
Odoo CRM tracks stages. To compute funnel conversion:
Leads in Stage :=
CALCULATE (
DISTINCTCOUNT ( 'crm_lead'[id] ),
USERELATIONSHIP ( 'Date'[Date], 'crm_lead'[date_last_stage_update] )
)
Win Rate :=
DIVIDE (
CALCULATE ( [Leads in Stage], 'crm_stage'[is_won] = TRUE ),
[Leads in Stage]
)
Average Deal Size :=
AVERAGEX (
FILTER ( 'crm_lead', 'crm_stage'[is_won] = TRUE ),
'crm_lead'[expected_revenue]
)
USERELATIONSHIP is the trick — crm_lead has multiple date columns (created, last update, won) and you want the right one for the visual.
Pattern 11: Manufacturing Yield
For Odoo MRP users:
Planned Quantity :=
SUM ( 'mrp_production'[product_qty] )
Produced Quantity :=
SUM ( 'mrp_production'[qty_produced] )
Yield % :=
DIVIDE ( [Produced Quantity], [Planned Quantity] )
Scrap Rate :=
DIVIDE (
SUM ( 'stock_move'[product_qty] ),
[Produced Quantity]
) * IF ( CONTAINSSTRING ( SELECTEDVALUE ( 'stock_picking_type'[code] ), "scrap" ), 1, 0 )
Yield by product, by work center, by week — operations teams will live in this dashboard.
Pattern 12: Calculation Groups for Time Intelligence
This is the modeling shortcut that replaces 30 measures with 5. As of Power BI Desktop 2026-01, calculation groups are stable in the desktop UI.
Create a calculation group called "Time Intelligence" with these calculation items:
-- Current
Current = SELECTEDMEASURE ()
-- YTD
YTD = CALCULATE ( SELECTEDMEASURE (), DATESYTD ( 'Date'[Date] ) )
-- PY
PY = CALCULATE ( SELECTEDMEASURE (), SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
-- YoY
YoY = SELECTEDMEASURE () - CALCULATE ( SELECTEDMEASURE (), SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
-- YoY %
[YoY %] =
DIVIDE (
SELECTEDMEASURE () - CALCULATE ( SELECTEDMEASURE (), SAMEPERIODLASTYEAR ( 'Date'[Date] ) ),
CALCULATE ( SELECTEDMEASURE (), SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
)
Now any base measure (Sales, COGS, Margin, Inventory Value) automatically gets YTD, PY, YoY, YoY% variants. Drop the calculation group on a slicer or column header and let users flip between them.
Putting the Patterns Together
A complete Odoo + Power BI dataset uses all 12 patterns:
| Domain | Patterns |
|---|---|
| Foundation | 1 (Date), 3 (Multi-Company), 9 (Composite Keys) |
| Sales | 2 (YTD), 5 (Cohort), 10 (Funnel) |
| Finance | 4 (Account Hierarchy), 7 (DSO), 12 (Calc Groups) |
| Operations | 6 (Aging), 8 (Turnover), 11 (Yield) |
Build the foundation patterns first. Add domain patterns as stakeholders demand them. Refuse the request to add raw Odoo IDs to visuals — they will lead to confusing filter context bugs.
Performance Notes for Large Odoo Datasets
For Odoo deployments above 10M rows in account_move_line:
- Switch to Import mode + Incremental Refresh. DirectQuery against Odoo's PostgreSQL backend is too slow once you cross 10M rows. See our incremental refresh deep dive.
- Pre-aggregate in PostgreSQL. Build materialized views for monthly aggregates. Power BI consumes the views.
- Star schema, not snowflake. Odoo's normalized schema must be denormalized for VertiPaq to compress efficiently.
- Disable auto-date hierarchies. Power BI auto-generates a date table per date column — kill this in options or your model bloats.
Frequently Asked Questions
Should I use the Odoo XML-RPC connector or read PostgreSQL directly?
Read PostgreSQL directly via the native Power BI PostgreSQL connector. The XML-RPC connector is fine for small lookups but gets slow over 100K rows. For production dashboards, give Power BI a read-only PostgreSQL user and query the warehouse views.
Do these patterns work for Odoo Online (SaaS)?
Odoo Online does not give you direct PostgreSQL access. You can either use the XML-RPC connector (slow), export to a warehouse via Odoo's automated actions, or use a third-party connector. Most ECOSIRE clients on Odoo Online sync nightly to BigQuery or PostgreSQL via Airbyte and point Power BI at the warehouse.
How do I handle Odoo multi-currency at the line level?
Use currency_id on account_move_line and a currency_rate table. Odoo stores both amount_currency (transaction currency) and balance (company currency). For consolidation across companies, multiply balance by the company-to-reporting currency rate (Pattern 3). Do not use amount_currency for consolidation — you will mix currencies.
Are there ready-made templates?
Yes. ECOSIRE's Power BI dashboard service ships pre-built Odoo dashboards covering sales, inventory, manufacturing, and finance. We license the .pbit templates so your team can extend them. Browse our Power BI products catalog for the current library.
What about Microsoft Fabric? Does it change this?
Fabric absorbs Power BI Premium and adds OneLake, Data Factory, and Synapse. The DAX patterns above are unchanged — they live in the Power BI semantic model regardless of capacity SKU. Fabric mainly changes how data lands and is governed before it reaches the model.
These twelve patterns cover the 80% of Odoo + Power BI dashboard requests that come across our desk. If you need help implementing them on your specific Odoo version, or you want a managed Power BI deployment with refresh schedules, RLS, and embedded portals, our Power BI consultancy team builds these into production every month.
作者
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
相关文章
BMF Programmablaufplan Lohnsteuer 2026:实施德国官方工资税计算(XML、API、Odoo)
BMF Programmablaufplan Lohnsteuer 2026 开发人员指南:PAP 是什么、XML 伪代码格式、官方测试服务以及到 Odoo 工资单的映射。
2026 年 CRM 系统的成本是多少? 40 多个实施的实际定价
来自 40 多个实施的真实 CRM 定价:每个用户的许可成本、实施费用、隐藏成本以及 Odoo、HubSpot、Salesforce 等的 3 年 TCO。
eMAG Odoo 集成:将罗马尼亚最大的市场连接到您的 ERP(订单、库存、e-Factura)
将 eMAG Marketplace 连接到 Odoo ERP:报价和订单同步、AWB 运输、退货、库存和价格更新,以及卖家的罗马尼亚 e-Factura 合规性。
更多来自Data Analytics & BI
Microsoft Fabric 与 Power BI:有什么区别,2026 年您实际需要什么?
Microsoft Fabric 与 Power BI 向决策者解释:它们如何关联、F-SKU 发生了什么变化、Pro 许可何时足够以及 2026 年成本情景。
Power BI 顾问与内部团队:成本、速度以及何时雇用帮助 (2026)
您应该聘请 Power BI 顾问还是内部构建? 2026 年成本比较、速度和质量权衡、混合模型以及招聘公司时的危险信号。
Power BI Embedded:成本、容量调整以及何时优于构建您自己的仪表板
2026 年 ISV 和 SaaS 团队的 Power BI Embedded 成本细分:A-SKU 和 F-SKU 定价、按用户负载确定容量大小以及场景中的构建与购买数学。
2026 年 Power BI 实施成本是多少?实际项目预算解释
2026 年 Power BI 实施成本:按公司规模、顾问费率、许可项目、隐藏成本驱动因素和投资回收时间表划分的实际预算范围。
Power BI、Tableau 与 Looker (2026):实施团队的诚实比较
Power BI 与 Tableau 与 Looker 的比较,由实现 2026 年定价、建模层、治理、嵌入和总成本场景这三项的团队进行。
Power BI 行级安全性:动态与静态模式
Power BI RLS 深入探讨:静态角色与动态角色、USERPRINCIPALNAME 模式、安全表、管理器层次结构、RLS 测试和用于 SaaS 的嵌入式 RLS。