Daily updates from Odoo
Monday, October 13, 2025
1 change · 18.0
Enhancements to existing features
The Inventory replenishment view now calculates quantities in batches instead of one item at a time. This reduces repeated database work and makes the replenishment action open much faster for users managing many products.
Original PR description
Go to menu -> Inventory -> Operations -> Procurement -> Replenishment It is slow opening this action Notice this menu is similar to call the following action: ```python…
Go to menu -> Inventory -> Operations -> Procurement -> Replenishment
It is slow opening this action
Notice this menu is similar to call the following action:
```python
self.env.ref("stock.action_replenishment").run()
```
I got profilers and I have noticed the slow part is the following kind of queries:
<details>
<summary>Query</summary>
```sql
select
"purchase_order_line"."order_id",
"purchase_order_line"."product_id",
"purchase_order_line"."product_uom",
"purchase_order_line"."orderpoint_id",
"purchase_order_line"."location_final_id",
sum(
"purchase_order_line"."product_qty"
)
from
"purchase_order_line"
where
(
(
(
"purchase_order_line"."state" in (...)
)
and (
"purchase_order_line"."product_id" in (...)
)
)
and (
(
(
(
"purchase_order_line"."id" not in (
select
"stock_move"."purchase_line_id"
from
"stock_move"
where
"stock_move"."purchase_line_id" is not null
)
)
and (
"purchase_order_line"."location_final_id" in (
select
"stock_location"."id"
from
"stock_location"
where
(
"stock_location"."parent_path" like ?
)
)
)
)
or (
not exists (
select
?
from
"stock_move_created_purchase_line_rel" AS "purchase_order_line__move_dest_ids"
where
"purchase_order_line__move_dest_ids"."created_purchase_line_id" = "purchase_order_line"."id"
)
and (
"purchase_order_line"."orderpoint_id" in (
select
"stock_warehouse_orderpoint"."id"
from
"stock_warehouse_orderpoint"
where
(
"stock_warehouse_orderpoint"."location_id" in (...)
)
)
)
)
)
or (
"purchase_order_line"."order_id" in (
select
"purchase_order"."id"
from
"purchase_order"
where
(
"purchase_order"."picking_type_id" in (
select
"stock_picking_type"."id"
from
"stock_picking_type"
where
(
"stock_picking_type"."default_location_dest_id" in (...)
)
)
)
)
)
)
)
group by
"purchase_order_line"."order_id",
"purchase_order_line"."product_id",
"purchase_order_line"."product_uom",
"purchase_order_line"."orderpoint_id",
"purchase_order_line"."location_final_id"
order by
"purchase_order_line"."order_id" asc,
"purchase_order_line"."product_id" asc,
"purchase_order_line"."product_uom" asc,
"purchase_order_line"."orderpoint_id" asc,
"purchase_order_line"."location_final_id" asc
```
</details>
It is called 1.5k times ~35ms average duration
It sums ~1 minute
The code generating this query is the following "_compute_qty_to_order_computed" method:
https://github.com/odoo/odoo/blob/4baf55a5d108063b0b60beddf332b6ae367f1871/addons/stock/models/stock_orderpoint.py#L353
I have noticed the method `_quantity_in_progress` is called record by record without cache or prefetch to re-use the same query with multiple records
It is important to group-by location_id since that the query is filtering by "stock_location.parent_path" and it could combine results wrong
The profiler results are
Total time: 123.646 s
File: /home/odoo/instance/odoo/addons/stock/models/stock_orderpoint.py
Function: _compute_qty_to_order_computed at line 357
Line # Hits Time Per Hit % Time Line Contents
==============================================================
357 @api.depends('qty_multiple', 'qty_forecast', 'product_min_qty', 'product_max_qty', 'visibility_days')
358 @profile
359 def _compute_qty_to_order_computed(self):
360 1429 10722.3 7.5 0.0 for orderpoint in self:
361 1426 56357.7 39.5 0.0 if not orderpoint.product_id or not orderpoint.location_id:
362 orderpoint.qty_to_order_computed = False
363 continue
364 1426 123579332.8 86661.5 99.9 orderpoint.qty_to_order_computed = orderpoint._get_qty_to_order(qty_in_progress_by_orderpoint=orderpoint._quantity_in_progress())
Changing the method to use multi advantages the profiler result is
```txt
Total time: 31.7207 s
File: odoo/addons/stock/models/stock_orderpoint.py
Function: _compute_qty_to_order_computed at line 347
Line # Hits Time Per Hit % Time Line Contents
==============================================================
347 @api.depends('qty_multiple', 'qty_forecast
', 'product_min_qty', 'product_max_qty', 'visibility_days')
348 @profile
349 def _compute_qty_to_order_computed(self):
350 6 22373.3 3728.9 0.1 records = groupby(
351 3 68436.0 22812.0 0.2 sorted(self.filtered(lambda o: o.p
roduct_id and o.location_id), key=lambda o: o.location_id),
352 3 6.5 2.2 0.0 lambda o: o.location_id)
353 3 4.0 1.3 0.0 qty_in_progress_by_orderpoint = {}
354 32 99.2 3.1 0.0 for _location, orderpoints in records:
355 1455 2447053.1 1681.8 7.7 qty_in_progress_by_orderpoint.upda
te(self.browse([op.id for op in orderpoints])._quantity_in_progress())
356 1429 6515.8 4.6 0.0 for orderpoint in self:
357 1426 29176220.5 20460.2 92.0 orderpoint.qty_to_order_computed =
orderpoint._get_qty_to_order(qty_in_progress_by_orderpoint=qty_in_progress_by_orderpoint)
```
This way is ~3.5x faster
The query generated changed from:
"purchase_order_line"."product_id" in {only one id}
to:
"purchase_order_line"."product_id" in {many ids}
Reducing the calls from ~1.5k to only 0.1k
and the total duration reduced from ~1 minute to only ~3 seconds
Also, the validation
if not orderpoint.product_id or not orderpoint.location_id:
It is already considered from the method `_get_qty_to_order`
https://github.com/odoo/odoo/blob/5071c52bdae637dfc1292602a5dda2bf650e5abd/addons/stock/models/stock_orderpoint.py#L357-L358
So, removing this duplicated validation
Screenshots of the results:
- 
Code before of this commit:
- <img width="1507" alt="Screenshot 2025-05-16 at 1 17 36 p m" src="https://github.com/user-attachments/assets/368270d9-534b-475f-af27-a81eb02e6ecb" />
Code after of this commit:
- <img width="1507" alt="Screenshot 2025-05-16 at 1 21 16 p m" src="https://github.com/user-attachments/assets/11498d2a-fe53-49b5-a82e-bfc504dbfe05" />
UPDATED: for record I have ran the following methods
```python
# one by one
orderpoints = self.env["stock.warehouse.orderpoint"].search([("product_id", "!=", False), ("location_id", "!=", False)], order='id')
for orderpoint in orderpoints:
qty_in_progress_by_orderpoint = orderpoint._quantity_in_progress()
for (id, qty) in sorted(qty_in_progress_by_orderpoint.items()):
if not qty:
continue
print(f"{id}:{qty},")
```
and
```python
# multi in one shot
orderpoints = self.env["stock.warehouse.orderpoint"].search([("product_id", "!=", False), ("location_id", "!=", False)], order='id')
qty_in_progress_by_orderpoint = orderpoints._quantity_in_progress()
for (id, qty) in sorted(qty_in_progress_by_orderpoint.items()):
if not qty:
continue
print(f"{id}:{qty},")
```
And the result output for both codes are the same
UPDATED: Odoo is working on https://github.com/odoo/odoo/pull/213154