Thursday, September 24, 2026
12 changes · saas-19.4
Enhancements to existing features
Inventory valuation calculations for a selected past date now process stock movements more efficiently. This reduces wait times and database workload for companies with large product and stock movement volumes, making reporting more responsive.
Original PR description
### Batch per all moves, not per product **Problem:** In https://github.com/odoo/odoo/pull/250526, moves are batched in order to prevent memory error in databases with many stock.move. However, the…
### Batch per all moves, not per product **Problem:** In https://github.com/odoo/odoo/pull/250526, moves are batched in order to prevent memory error in databases with many stock.move. However, the batching is done on the moves per product, meaning batches can be very small relative to the limit, and this causes unecessary queries compared to batching per all moves to process. **Solution:** Iterate through all moves rather than per product, and use/save the per product results directly in the relevant dicts. --- ### Batch and prefetch for initial product std_price **Problem:** When initializing products' standard price before replaying valuation, the first stock.move is read. This happens before any prefetching or batching occurs, so a query is made per move and contributes to performance issues. **Solution:** Batch and prefetch the products' first moves, then initialize the standard price. --- ### Use cached is_in and is_out values **Problem:** In `_get_valued_qty()`, `_is_in()` and `_is_out()` are called for each move, but these methods are already called for these moves and cached as `is_in` and `is_out`. **Solution:** Replace the method calls with the cached fields. If the move is not done, we fallback to the methods as the cached fields will be false for not done moves. --- **Perf Tables:** Record: product.template, each with AVCO automated valuation and a done in move Today: |Record count|Time before|Queries before|Time after|Queries after| |------------|-----------|--------------|----------|-------------| |1k |1.08s |264 |697ms |235 | |5k |2.74s |864 |2.23s |864 | |10k |4.83s |1375 |4.56s |1870 | At Date: |Record count|Time before|Queries before|Time after|Queries after| |------------|-----------|--------------|----------|-------------| |1k |3.10s |6109 |925ms |386 | |5k |14.68s |30299 |3.50s |1564 | |10k |26.98s |59173 |7.16s |3590 | opw-6134244 Forward-Port-Of: odoo/odoo#261624
Resolved issues and error corrections
Hungarian NAV invoice XML files will no longer include cash rounding as a separate invoice line. This keeps reported invoices aligned with Hungarian legal guidance and avoids treating settlement rounding as a taxable product or service line.
Original PR description
Global cash rounding can be applied to customer invoices. Before this commit, the rounding would be included in the XML file sent to NAV. It would be included as a new invoice line (same as the products lines) and the ATK tax is applied on it. As stated in the legal Hungarian Documentation, an invoice line should always relate to the supply of a good or the service provided. In this case, a cash rounding (which is not a financial advantage or disadvantage) will be considered by the law as a settlement difference, that is not part of the invoice. So, this commit removes cash rounding lines from the NAV XML. Moreover, it uses base_lines for the amounts computation instead of line_ids. task-6527383 Forward-Port-Of: odoo/odoo#290375 Forward-Port-Of: odoo/odoo#286258
This fixes currency formatting so amounts in currencies without decimal places, such as Japanese yen, keep all their digits. Business reports and project updates will now show correct values like 100 instead of 1, reducing the risk of misleading monetary summaries.
Original PR description
Description of the issue/feature this PR addresses: In Odoo 19.0, format_amount(..., trailing_zeroes=False) removes significant integer zeroes when the currency has no decimal places, such as JPY.…
Description of the issue/feature this PR addresses:
In Odoo 19.0, format_amount(..., trailing_zeroes=False) removes
significant integer zeroes when the currency has no decimal places,
such as JPY. Project update monetary summaries use this formatter
with trailing_zeroes disabled.
Steps to reproduce in an Odoo shell with the default JPY configuration:
```python
from odoo.tools.misc import format_amount
currency = env.ref('base.JPY')
format_amount(env, 100, currency, trailing_zeroes=False)
format_amount(env, 0, currency, trailing_zeroes=False)
```
Current behavior before PR:
The numeric part of 100 becomes 1, and the numeric part of 0
disappears. Grouped amounts can also lose digits and leave an
incomplete thousands group.
Desired behavior after PR is merged:
Preserve all integer digits for currencies without decimal places.
Only strip trailing zeroes when a fractional part is present.
Regression coverage includes zero, positive, negative and grouped
amounts, both currency symbol positions, and languages with distinct
or identical decimal and thousands separators.
Validation:
- Before the fix: the new regression test fails in 20 subcases.
- After the fix: all 9 tests in TestFormatAmountFunction and
TestFormatLangDate pass on an isolated PostgreSQL 16 database.
- git diff --check passes.
Test selection:
--test-tags=/base:TestFormatAmountFunction,/base:TestFormatLangDate
This PR also includes my signed Individual Contributor License
Agreement in doc/cla/individual/ysnkucuker.md.
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#290374Fixed an accounting issue where reconciliations with no exchange difference could use a foreign currency instead of the company currency. This helps prevent small leftover balances in company currency and keeps accounting records cleaner.
Original PR description
When doing reconciliation with no exchange difference, the currency used should always be the company currency. Using foreign currency in that case could leave residual amounts in company currency. task-6582723 Forward-Port-Of: odoo/odoo#289237
This fixes an issue in the HTML editor where the font size shown in the toolbar could stay outdated after users removed formatting from selected text. It also improves how default heading and font-size styles are recognized, helping avoid confusing toolbar states and unnecessary nested formatting.
Original PR description
### Steps To Reproduce: - Add this content in the HTML editor: `<h2 class=display-3-fs>heading 2</h2>` - Select the heading text. - Click Remove Format. - Check the font-size input in the toolbar.…
### Steps To Reproduce: - Add this content in the HTML editor: `<h2 class=display-3-fs>heading 2</h2>` - Select the heading text. - Click Remove Format. - Check the font-size input in the toolbar. ### Description of the issue/feature this PR addresses: - The font-size class was removed from the block element, but the font-size input still displayed the value associated with the removed class. - The toolbar did not show the correct font size for default block classes (like `o_default_font_size`). - Applying a new font size inside default block classes created nested spans instead of splitting them. - The "Remove format" button remained enabled even when selection had no custom formatting (only default block classes). ### Desired behavior after PR is merged: - The font-size input is updated after removing the font-size class and correctly displays the font size of the resulting block element. - Update the `getFontSizeDisplayValue` utility to find correct CSS variable dynamically to also find the font size of default block classes. - Add default font size classes (like `o_default_font_size` and headings) to `format_class_predicates` resource in `font_plugin.js`. This allows them to be split and replaced instead of creating nested spans. task-6321166 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#289509 Forward-Port-Of: odoo/odoo#272023
Fully booked time slots are no longer hidden from customers or staff during POS ordering. Customers can still see unavailable slots as disabled options, while staff see them highlighted, reducing confusion and making capacity limits clearer.
Original PR description
Before this commit: = * Slots that reached their maximum capacity for a given time frame were hidden from the `pos_self_order` & `point_of_sale` slot selection dialog. After this commit: = * Slots remain visible but are disabled when they reach their maximum capacity in `pos_self_order` & slot remains visible with a red background in the `point_of_sale` slot selection dialog. task-6340956 Forward-Port-Of: odoo/odoo#288338 Forward-Port-Of: odoo/odoo#286467
Fixed an issue where importing or creating multiple product variants at once could remove the product image from both the main product and its variants. This ensures product images are preserved during bulk imports, reducing manual cleanup and improving catalog accuracy.
Original PR description
Steps to reproduce: - Import a product CSV with one row per attribute value ("Product Values" column) and an image URL on every row, or create several variants of the same template in one `create()`…
Steps to reproduce:
- Import a product CSV with one row per attribute value ("Product Values" column) and an image URL on every row, or create several variants of the same template in one `create()` call with an image.
- Open the product: neither the template nor the variants have an image. Products without attribute values imported in the same file are fine.
The import creates all variants of a template in a single batch. The inverse of `image_1920` is applied record by record: the first variant finds no image on the template and writes its image there. Writing `image_1920` on the template invalidates the image cache of every product.product, including the sibling variants being created, whose `image_1920` is protected during the inverse and therefore reads back as empty. The next variant then takes the "clear the image from the template" branch and writes `False` on the template, undoing the first write. Nothing is stored.
Read the value to inverse for every record before writing anything, so the invalidation triggered by the template write cannot change what the following records write.
opw-6545651
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#290171
Forward-Port-Of: odoo/odoo#287419This fixes a checkout cart issue where shoppers could see an error if the related unpaid sales order was deleted in another tab before they removed an item from the cart. The cart now handles that missing order safely, reducing disruption during online shopping.
Original PR description
Steps to Reproduce: - Navigate to the shop, add a product, and go to the cart page (/shop/cart). - Open a new tab, open Sales, and locate the newly created unpaid Sales Order. - Delete this unpaid…
Steps to Reproduce: - Navigate to the shop, add a product, and go to the cart page (/shop/cart). - Open a new tab, open Sales, and locate the newly created unpaid Sales Order. - Delete this unpaid backend Sales Order record from the system completely. - Return to the original cart tab and click the item's Remove button. - Observe the unhandled "ValueError: Expected singleton: res.currency()" crash Error: `ValueError: Expected singleton: res.currency()` Cause: When updating a cart without a sale.order record (request.cart is empty), "_get_updated_cart_page_values()" computes "minor_amount" by calling "payment_utils.to_minor_currency_units(order_sudo.amount_total, order_sudo.currency_id)". Because "order_sudo" is empty,"order_sudo.currency_id" returns an empty "res.currency()" recordset,causing "currency.ensure_one()" to raise a singleton error. Fix: Safely compute "minor_amount" only when "order_sudo" is non-empty, defaulting to "0" otherwise. sentry-7631817249 Description of the issue/feature this PR addresses: Current behavior before PR: Desired behavior after PR is merged: --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Time off requests can no longer be saved or moved forward without a supporting document when the time off type requires one. This prevents incomplete leave requests from entering the approval process and helps ensure HR records stay compliant with company policy.
Original PR description
Problem: When a time off type is configured to require a supporting document, the system should prevent users from submitting a request without one. However, users could bypass this requirement by…
Problem: When a time off type is configured to require a supporting document, the system should prevent users from submitting a request without one. However, users could bypass this requirement by creating and saving a request without uploading a file. Because the validation was not strictly enforced during creation or subsequent write operations, requests could enter or remain in active states without the mandatory documentation. Solution: This commit ensures the system enforces mandatory attachments during the modification of time off requests that require a supporting document. The system now evaluates state transitions to block undocumented submissions. Steps to reproduce(runbot v19): 1. Go to Time Off > Configuration > Time Off Types, create a new leave type and enable the "Allow To Attach Supporting Document" setting. 2. Create a new time off request for this type, leave the attachment empty, and save. 3. Notice that the system allows the invalid request to be saved and persist in the database without a document. opw-6413621 Forward-Port-Of: odoo/odoo#289577 Forward-Port-Of: odoo/odoo#278991
This fix ensures journal item reconciliations involving accounts that do not support payment reconciliation use the company currency consistently. It prevents unnecessary exchange difference entries, helping keep accounting records accurate and avoiding confusing currency adjustments.
Original PR description
When reconciling journal items, if one of them is using an account with no payment reconciliation, then the currency of the reconciliation should be the company currency regardless of which currency was used on the items. Also, in that case, no exchange difference entry should be created. task-6582723 Forward-Port-Of: odoo/enterprise#132199
This fixes an issue where creating a rental planning shift and adding it to the last order could leave an unused slot behind if rescheduling failed due to a conflict. The slot creation and order update now happen together, so any error rolls back the full operation and keeps planning data clean.
Original PR description
Steps to reproduce: - On the Planning Gantt view setup resources with `role_sync_shift_rental=True`. - Drag to create a new shift. - Click "Add to Last Order" while the target Rental order requires the slot to be rescheduled to different dates, on a resource that has a slot overlapping these dates. - The reschedule creates a conflict with another slot on the same resource ,and the slot created from the drag remains. Before this commmit, The called `planning.slot` would get orphanated due to being created through a different save request by the gantt quick create before 'action_add_last_order` is called. After this commit, A single orm call is used for the `planning.slot` creation and `add_to_last_order`. If an error was to be thrown both will be unrolled with all respective computation, and no residual data remains. task-6389195 Forward-Port-Of: odoo/enterprise#132297 Forward-Port-Of: odoo/enterprise#131399
This fixes a problem that prevented Dutch VAT returns and ICP declarations from being submitted when the Digipoort certificate key was protected by a password. The system now reads the key in the expected format, restoring submissions for affected companies without changing behavior for certificates that already worked.
Original PR description
Steps to reproduce: 1. Set a Digipoort certificate whose private key has a password 2. Submit a VAT return or an ICP declaration 3. It fails with 'bad password read' Analysis: Before 19.0 the PEM key was in an unencrypted format. Since f88f8258ead, env['certificate.key'].pem_key is encrypted whenever the key has a password. Every consumer in this module loads it without a passphrase: the VAT wizard, the ICP wizard and the status polling cron. Same cause as (odoo/enterprise#96562), which fixed it for l10n_mx_edi. Solution: Decrypt the key when reading it, as was already the case before 19.0. opw-6421300 Forward-Port-Of: odoo/enterprise#131473 Forward-Port-Of: odoo/enterprise#127528