BI Studio started as a way out of writing report modules. On every Odoo project we ran, the client kept asking for reports, and each request was a little different from the last one. Sales by branch. Sales by branch and salesperson. The same thing again, but only for one product category. Each request ended one of two ways. Either somebody exported to Excel and built a pivot table that was out of date by Friday, or a developer wrote another small module, with its own deploy and its own test cycle. By the tenth "sales by something" module there is nothing left to learn from writing it, and the developer writing it is not building anything else.
Two tools had each solved half of this already. Crystal Reports showed what a printed business report needs: groups, subtotals, page headers and a layout an accountant recognises at a glance. Metabase showed that a business user can ask a database a question in a few clicks, without SQL. Both sit outside Odoo, with their own login, their own copy of who may see what, and a database connection somebody has to look after. BI Studio takes both ideas and puts them inside Odoo. The target was easy enough for the person asking the question and powerful enough that the implementer stops writing report modules.
This guide is for the people who install, extend, audit or resell it: Odoo developers, implementers and OEM partners. It covers how BI Studio works, and also why it works that way, because the reasons tell you where it fits a project and where it does not. The User Guide covers using it.
What is in the box
BI Studio is plain Odoo 18 Community code. It depends on base and web only, ships under OPL-1, and needs nothing outside a standard Odoo install. Excel output uses xlsxwriter, which Odoo already requires. Two MIT libraries are vendored under static/lib: GridStack 13.2.0 for the dashboard grid and Konva 10.3.2 for the report canvas.
Modules
bi_studio is the engine: datasets, questions, dashboards, reports, sharing, menus, packages. The other 14 modules are app packs. Each depends on bi_studio plus one Odoo app, installs automatically when that app is present, and carries data only.
Take one bar from the shipped POS Profit dashboard: gross profit for one outlet, last month. This is everything that happens between the dataset definition and that bar.
1. The dataset. The Point of Sale pack ships a dataset called POS Sales Lines. One row is one ticket line. Its base model is pos.order.line, alias t0. Three forward joins bring in the ticket (t1, pos.order), the product variant (t2) and the product template (t3). Fifteen columns sit on top, among them Outlet (t1.config_id), Sales Amount (t0.price_subtotal), Total Cost (t0.total_cost) and Gross Profit, a calculated column defined as Sales Amount minus Total Cost. A dataset filter keeps tickets that are paid, done or invoiced. All of this is ordinary records in bi.dataset, bi.dataset.join, bi.dataset.line and bi.dataset.filter.
2. Generate. The definition compiles into one SQL statement, built with psycopg2.sql so every identifier is quoted by the driver. Cut down to the columns this example uses, it reads:
SELECT
"t0"."id" AS "id",
"t0"."id" AS "x_bi_source_id",
"t1"."config_id" AS "x_bi_t1_config_id",
"t0"."price_subtotal" AS "x_bi_t0_price_subtotal",
"t0"."total_cost" AS "x_bi_t0_total_cost",
(COALESCE("t0"."price_subtotal", 0) - COALESCE("t0"."total_cost", 0))
AS "x_bi_t0_price_subtotal_minus_t0_total_cost",
"t0"."company_id" AS "x_bi_company_id"
FROM "pos_order_line" AS "t0"
JOIN "pos_order" AS "t1" ON "t0"."order_id" = "t1"."id"
LEFT JOIN "product_product" AS "t2" ON "t0"."product_id" = "t2"."id"
LEFT JOIN "product_template" AS "t3" ON "t2"."product_tmpl_id" = "t3"."id"
WHERE "t1"."state" IN ('paid', 'done', 'invoiced')
PostgreSQL gets a view called x_bi_pos_sales_lines. On top of it Generate registers a manual Odoo model, x_bi.pos_sales_lines, with one field per column, list, pivot, graph and search views, access rules and a company rule. No join here can multiply rows, so id is simply the ticket line's own id. A click on the bar later opens real ticket lines.
3. The question. The shipped question Sales and Gross Profit by Outlet is stored as JSON on bi.question:
When the dashboard asks for it, the server checks every column name against the dataset's own column list and every aggregate against a fixed set. It turns the Period filter into a domain on the Date column the dashboard mapped it to, and calls _read_group on x_bi.pos_sales_lines as the person looking at the screen. The browser never sends SQL.
4. The rules._read_group passes through _apply_ir_rules like any ORM read. The company rule keeps rows whose x_bi_company_id the viewer may work in. Row security then applies the viewer's own record rules on pos.order.line, compiled into a sub-select and matched on x_bi_source_id. In a standard install those rules only separate companies, so a POS manager sees every outlet of their companies. A custom rule that limits a supervisor to one outlet would limit this bar in exactly the same way.
5. The labels. The grouped values come back as pos.config ids. Their names are read with sudo, the way Odoo's own many2one fields read them, so the bar says the outlet's name even for a viewer who cannot open the outlet's settings. The pack also wrote a warning into the Gross Profit column's help text and the question's description: cost is only known once a session is closed, so a session still open counts its whole sales as profit. That sentence is part of the number as much as the SQL is.
Dashboards run every tile through this same path. Reports read rows with search and read, also as the current user, and build a band tree that the HTML preview, the PDF and the Excel writer all render from. One security path, three outputs.
Client side
Four OWL client actions do the interactive work:
Tag
Component
bi_studio_dashboard
Dashboard viewer and grid editor
bi_studio_question_builder
Question builder
bi_studio_report_params
Report run form, as a page or a dialog
bi_studio_report_canvas
Canvas report designer
Everything else is ordinary Odoo forms and lists. Assets go into web.assets_backend. There is no separate front end to deploy.
Overview
Design decisions
Most of BI Studio follows from seven choices made early on. Each one bought something and each one costs something. If you know both sides you can tell, before the first workshop, whether BI Studio fits a client.
Inside Odoo's database, not beside it
The textbook BI setup copies data out of the ERP into a warehouse and points a reporting tool at the copy. You get a fast query engine that cannot slow down the people posting invoices. You also get a sync job that fails quietly, a second set of user accounts, and a copy of the permissions that stops matching Odoo's the day somebody edits a record rule.
We went the other way. A dataset is a view, a materialized view or a table in the same PostgreSQL database Odoo uses, read through Odoo's ORM by Odoo's own users. There is nothing to sync and nobody to provision twice.
What it costs: every query runs on the production database. A live view reruns its query on each tile load. At the sizes we test that is cheap (69 to 176 ms to group 300,000 sales lines by salesperson), and materialized and table storage exist for the histories where it is not. A client whose server already struggles at month end will feel BI Studio, not the other way round.
A real Odoo model on every dataset
Generate could have queried its view with raw SQL and stopped there. Instead it registers x_bi.<slug> as a manual model, and in return gets most of Odoo for free: _read_group, list, pivot and graph views, Odoo's export, access lists, record rules, multi-company, translations, menu actions and drill-through by domain. Any Odoo developer can open the model in Settings → Technical → Models and read it.
What it costs: generating writes ir.model and ir.model.fields rows and reloads the registry. That is why one dataset takes seconds, why packs install their datasets in Draft instead of generating them at install, and why on a multi-worker server the other workers reload their registry after a generation. The x_bi. models belong to Generate. Edit the dataset, never the model.
Named columns instead of SQL in the browser
Authors pick columns by business label from a list a Designer prepared. The browser sends column names and aggregates, never SQL, and the server rejects any name that is not on the dataset. Nothing can be injected because nothing is interpreted. The label, role, trust level and help text travel with the column, so "In the document's currency" follows the number into every question, export and report built on it. And an Author cannot write a query that happens to look right while joining the wrong way.
What it costs: anything the column list cannot express needs a Designer. Calculated columns stop at plus, minus and times within one row. Division is left out on purpose. A margin percentage per line cannot be summed into a margin percentage per category, and a feature that invites the mistake is worse than no feature. Ratios of totals are not a measure in this release either (see Known limits).
Row ids that stay put
When no join multiplies rows, the generated row id is the base record's id. Odoo caches records by id. A view that numbers rows with row_number() hands out different ids after every insert, and then clicking a row opens somebody else's order. Older BI view modules for Odoo have exactly that bug.
What it costs: a reverse join can repeat a base record, so its rows switch to row_number() ids and drill-through to a single record turns off.
Row security read from the source
The obvious way to make a dataset respect Odoo's rules is to copy each base model's record rules onto the generated model. Copies drift. Instead, one hook on _apply_ir_rules asks Odoo at read time which rows of the base model this user may read, and keeps the dataset rows whose x_bi_source_id is among them. The rules keep one owner, Odoo, and a rule changed this morning applies to BI Studio this morning.
What it costs: 40 to 110 ms on 300,000 rows in our measurements. Pre-aggregated datasets cannot take part, because a pre-aggregated row has no single source record.
Bands for printed reports
Business documents are groups with totals: accounts with their journal items, customers with their open invoices, warehouses with their stock. The band model Crystal Reports made familiar says that directly, and accountants have been reading reports built this way for thirty years. Flow layout handles long listings. The canvas handles documents with a fixed layout. Editing the canvas uses Konva in the browser, but the PDF comes out of Odoo's own report engine, so it looks like every other PDF the client prints. Print conditions are a JSON tree read by a small interpreter rather than Python passed to eval, so an Author cannot run code on the server.
What it costs: a report is rows and groups. A cross-tab with one column per month is a pivot question, not a report.
Shipped content the client may change
Packs install their records as noupdate. Upgrades change a shipped record only while it is still exactly as shipped. A client who renames the Sales dashboard or re-shares a question keeps that change through every upgrade.
What it costs: a fix to shipped content does not reach a record the client already edited. The changelog says which records a release touches, so you can apply the same change by hand.
When to use something else
These are the projects where we would tell a client to look elsewhere, or to pair BI Studio with another tool:
The data is not in Odoo. Marketplace sales, a second ERP, web analytics, bank feeds that never become journal items. BI Studio reads one Odoo database and nothing else. Metabase, Superset or Power BI connect to many sources.
Several Odoo databases need one consolidated view. One BI Studio sees one database.
Heavy exploration on tens of millions of rows on a production server already near its limits. An external tool on a read replica keeps that load away from the people posting invoices. BI Studio cannot be pointed at a replica.
Amounts must be converted at historical rates inside the data. Not built. See Known limits.
Documents Odoo already prints, such as invoices and delivery slips. Odoo's own QWeb templates are the right tool there. BI Studio reports are for listings and statements built from data.
Overview
Install and upgrade
Install
Put the 15 module folders directly in a custom addons path. Check that no other copy of bi_studio sits elsewhere in addons_path, and that no older module from before the merge is still installed. Earlier releases shipped bi_studio_query and bi_studio_report as separate modules. Their code now lives inside bi_studio, and leaving either installed loads two copies of the same JavaScript, which fails with "Cannot add key ... in the actions registry".
Then install bi_studio. The packs follow by themselves for every app already installed.
Packs install their datasets in Draft. Generating creates database views, and doing that during module installation would turn a 20-second install into several minutes and fail the whole install if one view failed. So it is a separate step.
Open BI Studio → Build → Datasets, select the datasets and run Action → Generate. Each dataset runs in its own savepoint, so one failure does not undo the others. On the test machine, all 35 shipped datasets took about five minutes together. That is long for one browser request on a server with a 120-second limit, so on production generate them in batches of five to ten, or from the shell:
Shipped datasets, questions, dashboards and reports are noupdate records. A new pack version cannot overwrite them through its data files, so packs change shipped records in migration scripts, and only while a record is still exactly as shipped. Customer edits survive every upgrade.
Upgrading to 18.0.1.13.0 does four things worth knowing before you run it:
Every dataset gets Row Security: Follow source permissions. Users whom Odoo restricts (a salesperson on "own documents only") start seeing fewer rows in BI Studio. That is the intended fix. Switch individual datasets to Dataset share only where you want the old behaviour.
Shipped datasets, questions and dashboards are re-shared with the app's own user groups instead of BI Authors and all BI Readers. Records someone already re-shared are untouched.
The activation key code is removed, along with its system parameters, cron and menu.
Sharing on every generated dataset is re-applied once, so the generated model matches what the dataset form says.
Back up the database and filestore first. Rehearse on a copy.
Uninstall
Uninstalling bi_studio drops every generated view, materialized view and table, removes the generated models, and deletes the menu entries and actions that dashboards, reports and questions created anywhere in Odoo. Nothing is left in PostgreSQL or on the home screen. Uninstalling a pack removes its records and the views of its datasets.
Datasets
The dataset engine
A dataset in the builder: the joined tables on the left, the columns with their roles and aggregates on the right.
Base model, joins and grain
A dataset starts from one base model (t0). Joins add tables by alias (t1, t2...). A forward join follows a many2one out, so each row meets at most one record and the row count stays the same. A reverse join brings in the records pointing back, and can multiply rows. BI Studio marks those joins multiplies_rows, and that choice changes how rows are identified.
When no join multiplies rows, the row id is the base record's id. Odoo caches records by id, so a stable id matters: it is what keeps "click a row, open the right record" correct after the data changes. When a join does multiply rows, ids come from row_number() and drill-through to a single record is switched off.
Every non-aggregated row also carries x_bi_source_id, the id of the base record it came from. Drill-through and row security both depend on it.
Columns
Each column (bi.dataset.line) has a business label, a role and formatting.
Role
Used for
Dimension
Grouping and filtering
Measure
Numbers to aggregate, with a default aggregate
Date
Time grouping at day, week, month, quarter or year
Attribute
Context shown in rows but not grouped or totalled
A column can be calculated: this field plus, minus or times another column of the same row. Division is left out on purpose, because a ratio per row cannot be added up afterwards. Gross profit is Sales Amount minus Total Cost per row, then summed.
Trust level (Exact, Derived, Estimated, Fallback) and a free-text caveat travel with the column. Questions and reports print them next to numbers built from that column.
The Fields tab: every column with its source, label, role, aggregate and trust level.
Storage
Materialization
What Generate creates
Freshness
Live view
CREATE VIEW
Always current
Materialized view
CREATE MATERIALIZED VIEW plus indexes
As of the last refresh
Table (incremental)
A table, reloaded one date window at a time
As of the last window rebuild
Materialized and table datasets get a cron at the chosen interval (hourly, daily, weekly). A table dataset rebuilds only the window of its incremental date column, which keeps nightly refreshes short on large histories. A record whose date moves out of the window, or is deleted, can leave a stale row until a full rebuild. Schedule one regularly when old periods can still change.
A pre-aggregated dataset groups rows inside the SQL. It is smaller and faster, but its rows no longer map to records: no drill-through, no row security.
What Generate does, in order
Tear down whatever the dataset generated before, checking pg_class for the relation kind so only the right DROP runs.
Create the relation from the compiled SQL, plus a company index.
Create the manual model and its fields through ir.model and ir.model.fields as superuser.
Create access rules for the visibility groups and a global multi-company rule.
Create views and the action, translated into every installed language.
Create or update the refresh cron.
A menu entry placed on the dataset survives regeneration. It is detached first and pointed at the new action afterwards.
The SQL tab shows the statement for Developers.
The SQL tab shows the compiled statement. Developers only.
Datasets
Security model
Security was designed in from the first version and re-audited in November 2026. Everything below has regression tests.
Roles
Group
Implies
Can
Reader (group_bi_user)
Open what is shared with them
Author (group_bi_author)
Reader
Create questions, dashboards and reports, and edit their own
Dataset Designer (group_bi_designer)
Author
Build datasets, edit anything, place menus in any app
Developer (group_bi_developer)
Designer
See generated SQL
Who may open what
Questions, dashboards and reports have an owner and Shared With groups. Record rules let a Reader read a record they own or one shared with a group they belong to. Authors write only what they own. Designers write everything.
The parts of a record follow it. Dashboard tiles and filters, report bands, elements, groupings and parameters, and dataset columns, joins and filters each have rules pointing at their parent. Before 18.0.1.12.0 these child models had access lists but no rules, so any Author could rewrite another person's tiles through RPC. Thirty rules in security/bi_child_rules.xml close that.
Who may read a dataset
visibility decides it, and an empty group list never means everyone:
Private: Dataset Designers only. The default.
Specific groups: the listed groups, plus Designers.
All BI Studio users: every Reader, chosen on purpose.
Generate turns this into ir.model.access rows on the generated model, read-only. Changing visibility or groups re-applies them straight away.
The Access tab: visibility, groups, row security and company scoping.
Which rows they get
Two filters apply on every read of a generated model.
The company rule. A global ir.rule on x_bi_company_id, taken from the base model or from a forward join named in Company From. A reverse join cannot supply it, since one row could then carry several companies.
Row security. With Follow source permissions (the default), BI Studio adds the viewer's own record rules on the base model. models/bi_row_security.py extends base._apply_ir_rules: for a generated model whose dataset follows source permissions, it takes ir.rule._compute_domain(base_model, "read") for the current user, compiles it into a sub-select on the base table, and keeps only rows whose x_bi_source_id is in it. Every ORM read path goes through _apply_ir_rules, so search, read, read_group, exports and reports are all covered by one hook.
Only record rules are applied, not the base model's access list. Sharing the dataset is the decision to give a group its data. The rules then narrow it the way Odoo would. A Reader with no Sales rights gets every order line of their companies from a dataset shared with them. A salesperson on "own documents only" gets their own lines.
The lookup is cached per registry and cleared whenever a dataset is generated, torn down or switched. Measured on 302,530 sales order lines, grouped by salesperson:
Viewer
Dataset share only
Follow source permissions
Administrator (company rule only)
69 ms
176 ms
Salesperson, own documents
69 ms
109 ms (82,308 rows)
Pre-aggregated datasets have no source ids and always behave as Dataset share only. Publish those only to people who may see every row.
Labels of related records
Grouping by a many2one returns records of another model. Their names are read through sudo, exactly as Odoo's own Many2one.convert_to_read does. A viewer who may read a row but not the warehouse it names still sees the warehouse's name instead of a failed tile. Only names of records present in rows the viewer can read are ever sent.
Menus
bi.menu.publish creates and moves ir.ui.menu records. Inside BI Studio, an Author may place their own work. Under another app or on the home screen it takes a Designer or base.group_system. An empty group list falls back to BI Readers, never to everyone.
Input handling
The browser never sends SQL or Python. Questions send JSON that is checked against the dataset's columns. Aggregates are limited to sum, avg, min, max, count and count_distinct. Report and dashboard conditions are a JSON tree evaluated by a small interpreter, with no eval. Identifiers in generated SQL go through psycopg2.sql.Identifier. Link and image URLs on dashboards accept http, https, mailto and site-relative paths only.
Extending
Write an app pack
A pack is a data-only module. Copy bi_studio_purchase, the smallest, and rename it.
Wrap records in <data noupdate="1">. Customers will edit them, and an upgrade must not undo that.
Install datasets in Draft. Never generate from a data file or an install hook.
Share the dataset with exactly the groups its content is shared with. Use the app's own groups (fleet.fleet_group_user), not the BI Studio groups. The TestShippedSharing test in bi_studio checks every installed pack: a question or report shared with a group whose members cannot read its dataset fails the build, and so does a dashboard on which some group can read nothing.
Put the grain in the description. "One row = one vehicle log entry." Authors read it under the data source.
Write caveats on columns. Currency, units, what a status means. They print next to the numbers.
Set the category on each dataset (a bi.dataset.category record) so the pack's work groups properly in lists and in the dashboard switcher.
Changing a pack later
Shipped records are noupdate, so a new version changes them in migrations/<version>/post-migrate.py. Change a record only if it still looks exactly as shipped. bi_studio.upgrade.reshare_if_untouched does this for sharing:
Report menus in packs point at an ir.actions.client with tag bi_studio_report_params and params: {'report_id': ...}, so they open on the report's own run form.
Extending
Move work between databases
Build → Import / Export moves datasets, questions, dashboards and reports from one database to another as a JSON package.
The Import / Export window. Pick what to export, or upload a package and validate it before importing.
No database id crosses over. Every reference is a stable name: a model's technical name, a field's technical name, or a key local to the package. Images on dashboards and reports travel inside the package. The format carries format_version and the Odoo major version and refuses a package from an incompatible one.
Import has a dry run. Validate checks every model and field the package refers to against the target database and lists what is missing, without writing anything. Import then runs inside one savepoint: either everything lands or nothing does. When a dataset of the same name already exists, the conflict setting decides: create a new one, update the existing one, or import as a copy. Nothing is overwritten unless you choose Update existing.
Imported datasets arrive in Draft. Generate them on the target.
A single dataset can also be exported from its form with Export Definition.
Extending
Tests
The suite has 248 tests across the 15 modules. Run them on a fresh database with every module installed:
Run them on a fresh database rather than with -u on a copy of production. Upgrading a database full of real transactions with tests enabled can trip over unrelated Odoo data and bury the BI Studio results.
A generated model backed by a view gets _auto = False from Odoo itself. The test harness reports that as a stray class attribute, so tests/common.py registers it through declare_view_backed.
Records created as superuser stay in the ORM cache, and a later with_user() read can skip access checks on cached values. Tests about access call self.env.invalidate_all() first, the way a real request starts with an empty cache.
The log shows Model x_bi.orphan_test_ds has no table three times during test_lifecycle. That test removes a relation behind Odoo's back on purpose.
Commercial
Licensing and support
BI Studio is licensed under OPL-1 (Odoo Proprietary License v1.0). It is written from scratch and contains no OCA or AGPL code. The two vendored libraries are MIT.
There is no activation key. The module never contacts a licence server and never locks itself. Licensing is a contract between PORTOKO and the buyer.
OEM licence
An OEM licence is for an Odoo partner who wants to sell BI Studio to their own customers. The terms PORTOKO offers by default:
Term
Default
Grant
Non-exclusive, worldwide, perpetual for the versions delivered
Source
Full source of all 15 modules
Rebranding
Allowed: module names, icons, author and menus
Deployments
Unlimited customer databases operated or resold by the partner
Resale of source
Not allowed. Customers receive the modules as part of the partner's service, not as a product to resell
Updates
12 months of new releases for Odoo 18, renewable
Support
12 months of developer support for the partner's team, by email and video call
Other Odoo versions
Ports to Odoo 17 or 19 on request at no extra cost during the update period
Ports are built when a partner asks for one. The timing is agreed per request, and a port is delivered with the full test suite passing on that Odoo version.
End-customer licence
A company buying for its own use gets the same modules under OPL-1 for one production database plus its test and staging copies, with the same 12 months of updates.
What OPL-1 allows
OPL-1 lets the licensee use, modify and deploy the modules on their own Odoo instances. Redistributing the modules or publishing them requires PORTOKO's written permission, which the OEM agreement gives for the partner's own customers. The full licence text is at odoo.com/documentation/legal.
This page summarizes the commercial offer. The signed agreement is what binds both sides.
Commercial
Known limits
This page lists what this release does not do, and what each gap looks like in front of a client. Better you read it here than find it in a demo.
Numbers that can mislead
No currency conversion. Sales, purchase, POS and invoice datasets keep each document in its own currency. Take a company that keeps its books in rupiah and sells a few export orders in US dollars. A USD 1,000 order adds 1,000 to a column of rupiah totals. Inside a total of 2.4 billion that error is invisible, which is exactly why it is dangerous. The sales, purchase and invoice datasets carry a Currency column and a note saying so. Group or filter by it before adding amounts up. POS figures are in each outlet's currency and the POS datasets have no Currency column, so outlets in different currencies have to be read per outlet. Journal Items and the signed cash-flow amount on Payments are in company currency and sum safely across currencies, so use them when a single figure has to be right.
Header amounts on a line dataset. A dataset at line grain repeats anything taken from the document header on every line. Add the invoice's open amount to a line-level dataset and an invoice with four lines and 10 million still owed shows 40 million owed. The shipped Accounting pack puts open balances on Receivables and Payables, at one row per document, for that reason. Keep to that when you build your own.
Reverse joins repeat rows. A reverse join from the product template to its vendor prices gives one row per vendor. A product bought from three vendors then counts its sales three times. BI Studio marks such joins and turns off drill-through to single records, but it cannot know which measures you meant to add up. Put vendor-level data in its own dataset.
POS profit is too good while a session is open. Odoo fills a POS line's cost when the session closes. Until then, Gross Profit equals Sales Amount. The shipped columns and questions say so in their notes. Read POS profit for yesterday and earlier, or for closed sessions only.
No ratio of totals. Margin percentage per category, average price per unit and similar ratios are not a measure yet. Show the two totals side by side, or export and divide in Excel.
Pre-aggregated datasets ignore source record rules. They have no source ids to filter on. Share them only with people who may see every row.
Freshness and load
Snapshots are snapshots. Say POS Sales Lines is switched to a materialized view refreshed once a day. At three in the afternoon a branch supervisor sees nothing of today, and a refund keyed in this morning is missing from the figures. Keep datasets people read for "today" live. Materialize the long histories that are mostly read by month. The dataset form shows the last refresh time.
Incremental tables can hold stale rows when a record is deleted or its date moves out of the rebuild window. Schedule a full rebuild.
Live views run on production. A wall display with twelve tiles and a 60-second refresh runs twelve queries a minute, all day. On a few hundred thousand rows that is noise. On tens of millions, materialize first or stretch the interval.
First generation is slow. About five minutes for all 35 shipped datasets on the test server, because each one reloads the registry. Generate in batches. See Install and upgrade.
Not built yet
No running totals in reports. The element type exists in older data. New ones are refused with a message pointing to subtotals.
"Hide repeats" is not printed. The option is stored on report elements but no renderer honours it yet, so the form no longer shows it.
Only Include on reports is JSON. The report-level filter takes a JSON list of conditions. Run-time filters belong in parameters, which have a proper form.
Questions filter with typed values. The question builder's Only Include takes typed values, not a record picker, for relational columns.
One database, Odoo only. No external sources and no consolidation across databases. See When to use something else.
Odoo 18 only. Ports to 17 and 19 are made on request. See Licensing.
Commercial
Changelog
Versions are those of the bi_studio module. Packs carry their own version numbers, listed where they changed.
18.0.1.13.0 (5 November 2026)
Row security. Datasets now follow the record rules of their base model for each viewer, through one hook on _apply_ir_rules. A salesperson limited to their own orders sees only their own lines in every question, dashboard and report. New field Row Security on the dataset, defaulting to Follow source permissions. Dataset share only keeps the previous behaviour.
Sharing applies at once. Changing a dataset's visibility or groups now updates the generated model's access straight away. Before, it waited for the next regeneration, so removing a group left it able to read.
Shipped content shared with the right people. Shipped datasets were shared with BI Authors while their dashboards went to every BI Reader, who then saw "not shared with you" on each tile. Every pack now shares its datasets, questions and dashboards with the app's own user groups, and each dataset with exactly the groups of what is built on it. A test checks this for every installed pack. Packs updated: Accounting 18.0.1.6.0, CRM 18.0.1.3.0, Manufacturing 18.0.1.3.0, Point of Sale 18.0.1.4.0, Purchase 18.0.1.3.0, Sales 18.0.1.3.0, Sales Margin 18.0.1.4.0, Inventory 18.0.1.5.0.
Names on charts. A tile grouped by a related record the viewer cannot open now shows its name, read the way Odoo's own many2one fields read it, instead of failing the tile.
Sharing warning. Dashboard and question forms say when the groups they are shared with cannot read part of the data behind them.
Report menus open the new run form. The shipped General Ledger, Aged Receivables, Aged Payables, Stock Count Sheet and Sales Margin reports opened Odoo's generic wizard from their menus. They now open their own run form.
Columns by name. Report groupings and band elements pick their column from a list of the dataset's labels instead of showing technical names such as x_bi_t0_account_id.
Honest options. New running-total elements are refused with a pointer to subtotals, since no renderer prints them. "Hide repeats" is no longer offered on the form for the same reason.
No activation key. The licence check, its cron, menu, wizard and stored parameters are removed. Licensing is by contract.
Fixes. Three duplicate field labels removed from the upgrade log.
18.0.1.12.0 (4 November 2026)
A full QA round: technical, end-user and hostile input.
Security. Thirty record rules on the parts of dashboards, reports and datasets. Before them, any Author could rewrite or delete another user's tiles, bands and parameters over RPC, and Readers could read the parts of private records.
Add to Menu. Dashboards, reports, questions and datasets can be placed inside BI Studio, under any other app's menu, or as their own app on the home screen. Entries can be moved and removed later and survive dataset regeneration.
Report run forms. Each report gets its own run form: date ranges with quick picks, pick lists filled from the data or typed by hand, number ranges, Yes/No, sections, half-width fields, hints, required fields, chosen outputs and a custom title and button. Shipped account and inventory reports switch their name filters to pick lists.
Dashboards keep working with partial access. A tile whose data the viewer cannot read shows a message, and the rest of the dashboard loads.
Clean uninstall. Generated views, tables, models, menus and actions are removed with the module.
Fixes. Zero was treated as an empty value in report parameters. The last day of a datetime range was cut off. Raw aggregates from the browser were accepted. Link widgets accepted any URL scheme. Dataset names that could not make a table name were accepted until generation failed. Generation logged two errors per regeneration on a clean drop.
18.0.1.11.0
Datasets say who may read them, and an empty group list no longer means every reader. Question sort keys are validated. Reports get several detail sort keys and an owner, and become private to the owner unless shared.
18.0.1.10.0
Dashboards get their own top-level menu. Existing dashboard shortcuts move there.
Earlier releases (0.9.x)
The engine, security model and designers came together over the 0.9 series: explicit visibility and multi-company rules, drill-through to source records, structured visibility conditions without eval, portable packages with dry-run validation, clone and template support across all four object types, nine and then twelve chart types, the GridStack dashboard grid, the Konva canvas designer, Excel and CSV export, and rich dashboard widgets with pinned periods and leap-year-safe comparisons.