Aged databases make stock moves crawl
After years of serial tracking, backorders and multi-level manufacturing, Odoo’s stock_move_move_rel link table grows into a dense web — hundreds of millions of edges, single moves linked to thousands of others. The standard code walks that web through the ORM, so Mark as Done, Check Availability and opening a Manufacturing Order take tens of seconds or time out with a 504. Upgrading Odoo does not fix it — the schema and the chaining algorithm are unchanged from 16 to 19. The fix is algorithmic, and it can be applied in place.
Faster, and provably the same result
This is a performance change, not a behaviour change. The SQL helpers only gather a set of move ids; those ids are re-resolved through Odoo’s own search(), which re-applies every record rule (including multi-company) and returns them in the exact model order the standard code relies on. The grouping and quantity logic is copied verbatim from standard Odoo. The bundled automated tests assert this equivalence directly.
What gets faster
Mark as Done / Check Availability
The MTO availability traversal walked on every _action_assign is pushed into two bounded SQL queries. The heaviest call on a real database dropped from ~15 s to under 2 s.
Move-chain roll-up
Upstream/downstream chain walks become a set-based breadth-first search over the link table — one query per graph layer instead of one Many2many read per node.
Quant availability
On-hand availability is summed with one grouped query instead of loading every quant of a fragmented (product, location) — the check that fires constantly during reservation.
Verify it yourself
A reproducible benchmark ships with the module (tests/benchmark_move_graph.py): it builds a dense move graph and times the standard traversal against the bounded SQL, asserting identical results.
Record-rule safe
Move ids are re-resolved through search(), so multi-company and every other record rule apply exactly as before. No access bypass.
Move Graph Health diagnostic
A read-only report (Inventory » Reporting) X-rays your link table — size, density, worst-offending moves and how much only traces closed history. It never modifies data.
Drop-in and reversible
Pure method overrides — no schema change, no new stored column, no configuration. Install, and it works. Uninstall, and standard behaviour returns.
Measure before you act
The built-in Move Graph Health report quantifies exactly how dense your move graph is — safe to run on production.
Who it’s for
Manufacturers and heavy-inventory operations on Odoo 16 with a large stock-move history — especially serial/lot tracking with frequent backorders — whose validations and MO screens have become slow, and who cannot or will not upgrade or migrate hundreds of millions of link rows.
Questions, answered
No. Only how the move sets are gathered changes; the reservation and quantity logic is untouched and the automated tests assert identical results.
Yes. Ids are re-resolved through search(), so the multi-company record rule applies exactly as before.
No — that is the whole point. It is an in-place algorithmic fix for a problem upgrading does not solve.
Standard behaviour returns. There is no schema change to reverse.
Odoo 19.0 · License OPL-1 · No Enterprise modules required
Documentation-first — the built-in manual (this page &
doc/index.rst) covers install, verification and troubleshooting, so you can run it without a support subscription.
BambooForge Stock Performance
Make Mark as Done, Check Availability and opening a Manufacturing Order fast again on databases with a large stock-move history — without upgrading Odoo and without migrating your data. This module rewrites the hottest move-graph traversals as bounded SQL that returns the identical result the standard code returns, and ships a read-only diagnostic that shows you exactly how bad the problem is before and after.
This page is the complete manual. Follow it top to bottom and you can install, verify the speed-up and understand precisely what changed without contacting support.
The problem
Odoo links stock moves to each other through the stock_move_move_rel many-to-many table (move_orig_ids ↔ move_dest_ids). On a young database this table is small. On a database that has run for years — especially with serial/lot tracking, repeated backorders and multi-level manufacturing — it grows a combinatorial, bipartite structure: shared components link to many serial-tracked orders, each backorder keeps chains open, and procurement groups fan out. Link counts in the hundreds of millions and per-move fan-out in the thousands are real.
The standard code walks this graph through the ORM. Every hop (move_orig_ids.move_dest_ids.move_orig_ids) materialises hundreds of thousands of records and re-reads the company-rule-wrapped M2M. The result:
- Mark as Done on a manufacturing/MTO move calls _action_assign, which calls _get_available_move_lines_in / _out — a single such call has been measured at ~15 seconds on a real 210-million-link database.
- Opening a Manufacturing Order computes the source/child smart-button counts, which walk the whole procurement group — this can time out or never render the form.
- Under load these long calls pin worker processes and produce 504 Gateway Timeout.
Upgrading Odoo does not fix this: the stock_move_move_rel schema and the chaining algorithm are unchanged from 16 through 19, and migrating hundreds of millions of link rows is itself slow and risky. The fix is algorithmic, and it can be applied in place.
What this module does
It overrides five methods on stock.move and mrp.production so the graph traversal happens once, in the database, instead of node-by-node in Python:
- stock.move._get_available_move_lines_in / _out
- The MTO availability traversal walked on every _action_assign. The 2-hop and 3-hop move-id sets are produced by one bounded SQL statement each (DISTINCT collapses the explosion), then re-resolved through search().
- stock.move._rollup_move_origs / _rollup_move_dests
- The upstream/downstream chain walk, rewritten as a set-based breadth-first search over the link table (one query per graph layer) instead of per-record ORM recursion.
- mrp.production._get_children / _get_sources
- Manufacturing-order parent/child navigation and the source/child smart-button counts, pushed down to a single bounded query that returns the same MO ids.
It also narrows the dependencies of the mrp_production_child_count / mrp_production_source_count smart-button fields so that writing to a move no longer recomputes them across the entire procurement group.
- stock.quant._get_available_quantity
- On-hand availability is summed with one grouped SQL query over core's gather domain instead of loading every quant of a fragmented (product, location). Reservation itself is untouched.
Why the numbers stay identical
This is a performance change, not a behaviour change. Three properties keep the results bit-identical to standard Odoo:
- Same records. The SQL helpers only produce a set of move ids; those ids are re-resolved with search([('id', 'in', ids)]), which re-applies the stock.move record rules — including the multi-company rule. Nothing bypasses access control.
- Same order. search() returns rows in the model order (sequence, id), which is exactly the order the standard ORM chain yields, so the order-dependent itertools.groupby in the availability functions produces the identical grouping. No sorted() is introduced.
- Same grouping. The grouping and quantity-summation bodies are copied verbatim from standard Odoo — only the traversal that feeds them is replaced.
The bundled automated tests assert this equivalence directly: for a constructed move graph they check that each SQL helper returns the same id-set as the ORM chain it replaces, that search() re-resolution preserves order, and that a done receipt is summed to the correct quantity.
Installation
- Copy bambooforge_stock_move_perf into your addons path.
- Update the apps list and install BambooForge Stock Performance.
- No configuration and no data migration are required. The overrides are active immediately.
Dependencies: stock and mrp (both standard). No Enterprise modules are needed. There is no schema change and no new stored column, so the module is uninstall-clean.
The Move Graph Health diagnostic
Go to Inventory ▸ Reporting ▸ Move Graph Health. The report runs read-only SQL and shows:
- Size — the stock_move_move_rel table size and row count, and the number of stock moves.
- Density — average links per move and the worst single-move fan-out.
- Prune opportunity — how many links only connect moves that are already in a done or cancel state (i.e. trace closed history), as a share of all links. This is an estimate only; the diagnostic never modifies data.
- Worst offenders — the twenty highest fan-out moves, with their business reference, product and state, so you can see the real orders behind the numbers.
Use it to quantify the problem before installing elsewhere, and to show stakeholders the density of the graph. It is safe to run on production: every query is read-only and uses planner statistics for the big numbers so it does not full-scan a huge table.
Frequently asked questions
- Will my reservations or quantities change?
- No. See Why the numbers stay identical above — the change is limited to how the move sets are gathered; the reservation and quantity logic is untouched.
- Does it work on multi-company databases?
- Yes. Because the move ids are re-resolved through search(), the standard multi-company record rule is applied exactly as before.
- Do I need to upgrade Odoo or migrate data?
- No. That is the point of the module — it is an in-place algorithmic fix for a problem that upgrading does not solve.
- What happens on uninstall?
- The overrides are removed and standard behaviour returns. There is no schema change to reverse.
- My real bottleneck is disk size, not speed.
- This module addresses speed. The Move Graph Health report will also tell you how much of the link table only traces closed history — useful input for a separate slimming effort — but this module never deletes data.
Support
BambooForge Labs — bambooforge.labs@gmail.com
Odoo Proprietary License v1.0 This software and associated files (the "Software") may only be used (executed, modified, executed after modifications) if you have purchased a valid license from the authors, typically via Odoo Apps, or if you have received a written agreement from the authors of the Software (see the COPYRIGHT file). You may develop Odoo modules that use the Software as a library (typically by depending on it, importing it and using its resources), but without copying any source code or material from the Software. You may distribute those modules under the license of your choice, provided that this license is compatible with the terms of the Odoo Proprietary License (For example: LGPL, MIT, or proprietary licenses similar to this one). It is forbidden to publish, distribute, sublicense, or sell copies of the Software or modified copies of the Software. The above copyright notice and this permission notice must be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Please log in to comment on this module