Monday, October 13, 2025
5 changes · 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/213154Resolved issues and error corrections
This fix prevents GST return setup from failing when a multi-company tax unit's main company has no purchase journal configured. The system now looks across all companies in the tax unit to find a valid purchase journal, improving reliability for Indian GST reporting.
Original PR description
Before this PR: - The system searched for a purchase journal only in `company_id`. - In a tax unit with multiple companies, if the main company had no purchase journal configured, record creation failed with a 'NOT NULL constraint violated' error. After this PR: - The journal search now checks all companies in `company_ids` (or falls back to `company_id`), - allowing the system to find a valid purchase journal across the tax unit. OPW: 5159518 Forward-Port-Of: odoo/enterprise#96838
This change prevents an uninstall process from failing when worksheet-related database fields have already been removed. It helps avoid incomplete module removals that could cause problems when reinstalling related apps later.
Original PR description
Recently pull request https://github.com/odoo/enterprise/pull/86084 introduced an ondelete method on `ir.model` that retrieves some worksheet templates to delete them. However, this method breaks…
Recently pull request https://github.com/odoo/enterprise/pull/86084 introduced an ondelete method on `ir.model` that retrieves some worksheet templates to delete them. However, this method breaks when uninstalling module `worksheet`:
```
ir.model.data._module_data_uninstall():
... records are deleted ...
ir.model.fields.unlink():
drop column of corresponding fields
delete ir.model.field records
ir.model.unlink():
drop table of corresponding models
ir.model._unlink_if_uninstalling():
self.env['worksheet.template'].search([('model_id', ...)]).unlink()
delete ir.model records
```
The call to `ir.model.unlink()` crashes when searching for worksheet templates, since column `model_id` has been dropped already. This makes the transaction fail, and it is rolled back to a savepoint just before the call to `ir.model.unlink()`. In other words, the uninstallation manages to drop most of the columns that must go, but fails to drop all the tables that must go. And the uninstallation proceeds anyway...
Now consider uninstalling module `resource`. That module defines model `resource.calendar` with required field `name`, and also defines a record in that model (a default calendar). When the module is uninstalled, module `worksheet` is also uninstalled (because it depends on `resource`), and so the situation above happens. Consequently, most of the columns of table `resource_calendar` are dropped, but the table is not. If we reinstall module `resource` after that, the ORM re-creates column `name` (which is `NULL` on the default calendar at least), but fails to add the `NOT NULL` constraint on that column.
The fix consists in avoiding the `search()` above in the ondelete method if the column `model_id` does not exist anymore.Spanish Veri*Factu invoices for customers outside Spain now select the correct export regime key instead of the general regime. This helps businesses submit more accurate e-invoicing data and avoid manual corrections for export invoices.
Original PR description
### Steps to reproduce: - Install l10n_es_edi_verifactu and switch too Spanish company - Create an invoice for a partner outside Spain (or with the tax "0% EX G") - Check the "Veri*Factu Regime Key" under the page "Veri*Factu" - It should be "Export" (02) but it's "General Regime Operation" (01) ### Cause: `_l10n_es_edi_verifactu_get_suggested_clave_regimen()` is called on the tax "0% EX G". The line ```taxes.filtered(lambda tax: (tax.l10n_es_type not in main_tax_types or tax._l10n_es_edi_verifactu_get_applicability() != forced_tax_applicability))``` doesn't do what the comment says: remove the main taxes with a different applicability. ### Solution: Change the `!=` to `==` so that the line does the same thing as the comment. opw-5071665 Forward-Port-Of: odoo/odoo#230823
Purchase requests created from approvals now use the currency configured for the selected vendor instead of defaulting to the company currency. This keeps RFQ pricing consistent with other purchasing flows and helps avoid currency mismatches.
Original PR description
### Issue: When creating an RFQ from an approval, the created purchase order does not use the currency set on the vendor of the product. Rather, it uses the currency of the company, with the value converted based on the vendor's currency to get the price. This is not consistent with other ways we create RFQs, which all respect the vendor currency. ### Solution: Pass the vendor's currency into the values sent when creating the purchase order. opw-4549937