# Stock & MRP Performance for Large Databases

> Make stock reservation and MRP navigation fast again on aged, high-volume databases - no version upgrade and no data migration required.

- **What it is:** Make stock reservation and MRP navigation fast again on aged, high-volume databases - no version upgrade and no data migration required.
- **For:** Warehouse
- **Odoo versions:** 16.0, 17.0, 18.0, 19.0, 20.0 - Community and Enterprise
- **Runs on:** Odoo.sh or Odoo you host yourself - not Odoo Online, which accepts no third-party apps
- **Price:** 129 EUR, one-time, on the Odoo Apps Store
- **Technical name:** bambooforge_stock_move_perf
- **Manual:** https://docs.bambooforge.dev/bambooforge_stock_move_perf/
- **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_stock_move_perf.action_bf_move_graph_health
- **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_stock_move_perf
- **Manual last updated:** 2026-09-21

---
# Stock & MRP Performance for Large Databases

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:

1. **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.
2. **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.
3. **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

1. Copy `bambooforge_stock_move_perf` into your addons path.
2. Update the apps list and install **BambooForge Stock Performance**.
3. 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.

## How we make sure it works

Before every release, **12 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20.
Among them:

- Rollup move origs does not skip done.
- Orig dest orig search order matches core.
- Rollup move dests includes self and descendants.
- Rollup move origs includes self and ancestors.
- Available move lines in quantity.

The complete list is in the source you receive.

## Questions people ask

****Which Odoo versions does it run on?****

Odoo 16.0, 17.0, 18.0, 19.0, 20.0, on the series you buy it for; each series is its own listing.

****Community or Enterprise?****

Both. It needs no Enterprise app.

****What does it need?****

The standard Inventory, Manufacturing apps. No extra Python package.

****Where does it show up?****

Under the **Move Graph Health** menu.

****Does anything leave my server?****

No. It makes no call outside your Odoo server: no telemetry, no licence check, no third-party service.

****What happens if I uninstall it?****

Odoo drops the app's own tables and fields; the standard records it worked on (contacts, orders, products) stay as they are.

## Support

BambooForge Labs — [support@bambooforge.dev](mailto:support@bambooforge.dev)
