Thursday, September 24, 2026
21 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
This change speeds up internal testing for Argentina localization reports by creating sample invoices more directly. It reduces test setup time and database queries while keeping report results unchanged, helping maintain delivery quality with less build time.
Original PR description
The use_current_date=False branch of _create_test_invoices_like_demo built 18 invoices through Form, which reruns the whole move onchange for every field of every line. It was two thirds of the l10n_ar_reports class setup. Create them with _create_invoice from the fields the Form set, like the other branch does. The accounting date is now the invoice date on every invoice, the report output is unchanged. With the l10n_ar_reports counterpart: | l10n_ar_reports, runbot 18.0 L10n | before (3 builds) | after | |------------------------------------|-------------------|-------| | TestArReports setUpClass | 78-103s | 31s | | module test time | 80-106s | 33s | | queries | 45.4k | 28.4k | Forward-Port-Of: odoo/odoo#290123 Forward-Port-Of: odoo/odoo#290111
The Argentina localization report tests now create sample vendor bills more efficiently, reducing automated test setup time while keeping report results unchanged. This helps speed up validation for future updates without changing business functionality.
Original PR description
Same as the l10n_ar fixture: the 10 demo vendor bills were built through Form, rerunning the whole move onchange for every field of every line. Create them with _create_invoice from the fields the Form set. The accounting date is now the invoice date on every bill, the report output is unchanged. With the l10n_ar counterpart: | l10n_ar_reports, runbot 18.0 L10n | before (3 builds) | after | |------------------------------------|-------------------|-------| | TestArReports setUpClass | 78-103s | 31s | | module test time | 80-106s | 33s | | queries | 45.4k | 28.4k | Forward-Port-Of: odoo/enterprise#132841 Forward-Port-Of: odoo/enterprise#132697
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
Fixed an issue where a selection dropdown could reopen and remain visible after a user picked a suggested value. This improves form reliability and prevents automated workflows from failing while waiting for the dropdown to close.
Original PR description
Before this commit, the autocomplete dropdown of a many2one could open again right after a suggestion was picked, and stay open. The step of mail_template_dynamic_placeholder_tour waiting for it to…
Before this commit, the autocomplete dropdown of a many2one could open again right after a suggestion was picked, and stay open. The step of mail_template_dynamic_placeholder_tour waiting for it to close then times out:
FAILED: [8/36] Tour mail_template_dynamic_placeholder_tour
Step Wait for the drop down to disappear
(trigger: div[name="model_id"] .o-autocomplete:not(:has(.ui-autocomplete)))
TIMEOUT step failed to complete within 10000 ms.
This happens because the search filling the dropdown runs 250ms after the last keystroke, so a suggestion of the previous search can be picked while the next search is still scheduled. Picking a suggestion only closes the dropdown. The scheduled search then runs, opens the dropdown on the selected value, and nothing closes it after that.
This commit fixes the issue by discarding the scheduled search when the dropdown closes.
https://runbot.odoo.com/odoo/error/233574
https://runbot.odoo.com/odoo/error/237744
https://runbot.odoo.com/odoo/error/944454
https://runbot.odoo.com/odoo/error/945517
https://runbot.odoo.com/odoo/error/947141
Forward-Port-Of: odoo/odoo#290317
Forward-Port-Of: odoo/odoo#287888Point of Sale orders now keep gift card, eWallet, and coupon rewards applied after a page refresh or when reopening an order. This prevents cashiers from accidentally charging customers again for amounts already covered by a code-activated reward.
Original PR description
Steps to reproduce: - Create a gift card program and a gift card with some balance - Open a PoS session, add a product to a new order and enter the gift card code: a reward line is added and the…
Steps to reproduce: - Create a gift card program and a gift card with some balance - Open a PoS session, add a product to a new order and enter the gift card code: a reward line is added and the total decreases - Refresh the page, or leave the order and take it back from the Orders screen Issue: The reward line disappears and the total goes back to the full price, while the server still holds the pos.order.line with `is_reward_line`, `coupon_id` and `points_cost`. Nothing warns the cashier, so the customer can be charged for a service already paid with the card. Cause: `_code_activated_coupon_ids` is a local field of pos.order: it is neither stored in IndexedDB nor sent to the server, so it is empty once the order is rebuilt on the client. `_updateRewardLines` deletes every reward line and only re-applies the ones whose coupon is found in `_code_activated_coupon_ids` or in `uiState.couponPointChanges`. `updatePrograms` only rebuilds the latter for programs detectable from the order content, so a gift card, eWallet or coupon card activated by code is never recognised again and its reward is dropped by the next `updateRewards` call (TicketScreen.setOrder, barcode scan, partner change, line added...). In 17.0 `codeActivatedCoupons` was exported and restored with the order; the field became local with the 18.0 models rewrite. Fix: Add `_restoreCodeActivatedCoupons` on pos.order and call it from `checkMissingCoupons`, which runs once per rebuilt order (`setup` flags it with `invalidCoupons`) before the programs and the reward lines are refreshed. It re-links every persisted coupon referenced by a reward line that is in neither structure. Nominative cards are excluded so that removing the partner still drops their reward. opw-6568849 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#290257 Forward-Port-Of: odoo/odoo#288102
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
Belgian partner records now keep their BCE/KBO business identifier in sync when the VAT number is changed. This prevents outdated registration details from remaining on customer or vendor records and helps related e-invoicing information stay accurate.
Original PR description
Steps to reproduce: * Install **Accounting** module. * Create a Belgian partner. * Assign a VAT number (e.g. BE0477472701). * The BCE/KBO field in Multi ID is correctly populated. * Modify the VAT…
Steps to reproduce:
* Install **Accounting** module.
* Create a Belgian partner.
* Assign a VAT number (e.g. BE0477472701).
* The BCE/KBO field in Multi ID is correctly populated.
* Modify the VAT number.
Observed behavior:
* The BCE/KBO (BE_EN) in Multi ID keeps the old value.
Cause:
* `_deduce_additional_identifiers_from_vat` skipped any identifier key that already existed in `additional_identifiers`: new_identifiers = {k: v for k, v in deduced.items() if k not in identifiers}
* On the first VAT save, BE_EN is written correctly.
* On subsequent VAT changes, BE_EN already exists, so the condition short-circuits and the old value is preserved.
Fix:
* Replace the key-presence guard with a value-equality check so that deduced identifiers are always overwritten when their value would change: updated = {k: v for k, v in deduced.items() if identifiers.get(k) != v}
* Since BE_EN is fully derived from the VAT (it is the VAT with the country prefix stripped), it must always mirror the current VAT.
* The Peppol endpoint is already declared as depending on `additional_identifiers`, so it recomputes automatically once BE_EN is corrected — no further change needed there.
opw-6545770
Forward-Port-Of: odoo/odoo#288057Fully 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
This fixes an internal data-model issue where certain related, non-saved many-to-many fields could be given an unintended database relationship. The change helps prevent data from one field being incorrectly interpreted as belonging to another, improving reliability for customized Odoo setups.
Original PR description
When we setup a many2many field which is not stored, it must not have a relation. This is already done for nonrelated fields, but we must do it also for related fields. Otherwise, we may have…
When we setup a many2many field which is not stored, it must not have a relation. This is already done for nonrelated fields, but we must do it also for related fields. Otherwise, we may have side-effects like finding sibling fields in 20.0: in the code below, reading categories would fill roles.
```py
model_id = self.env["ir.model"]._get("test_new_api.foo")
fields = [{
"name": "x_partner_id",
"ttype": "many2one",
"relation": "res.partner",
}, {
"name": "x_role_ids",
"ttype": "many2many",
"relation": "res.partner.category",
}, {
"name": "x_partner_category_ids",
"ttype": "many2many",
"relation": "res.partner.category",
"related": "x_partner_id.category_id",
"store": False,
"readonly": True
}]
for f in fields:
f["model_id"] = model_id.id
self.env["ir.model.fields"].create(fields)
env['test_new_api.foo']._fields['x_partner_category_ids'].relation # should be None
```
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#290286
Forward-Port-Of: odoo/odoo#290067Fixed 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
Message previews in Mail now display the intended text for Odoo action links instead of showing placeholder # symbols. This makes notifications such as pinned messages and join updates clearer for users.
Original PR description
Before this commit, JS-handled links (e.g. pinned messages notification, joined notification) would be formatted as "#" in `htmlToHtmlInline()` (introduced in [1]). This causes message preview for pinned message notification to show as "# #". This commit fixes the issue by specifically handling JS-handled links (recognized by odoo-specific data attributes) and rendering their tet content. [1]: https://github.com/odoo/odoo/pull/238080 task-6571060 Forward-Port-Of: odoo/odoo#290292
This fix prevents one unauthorized editor subscription from stopping the whole real-time update connection. Users can continue receiving other unrelated live updates, while restricted channels are simply skipped.
Original PR description
`_build_bus_channel_list` should never raise. Otherwise, the entire request is aborted, meaning the client fails to subscribe to *all* channels, including completely unrelated channels. Instead, if a client do not have permission to subscribe to a channel, the channel should just be skipped. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#287142
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
Opening a signing template that has already been sent and contains no fields no longer triggers an access error. The template view now avoids adding a placeholder signer when the template is locked by existing signature requests, so users can reopen these templates normally.
Original PR description
Version: 19.0 Steps to reproduce: - Create a sign template with no sign fields - Send a sign request using that template - Open that template from Templates Issue: After a sign request is sent, record rules make the template read only for sign items (create/write only when there are no sign requests). On open, if the template has no signers, the UI auto creates a dummy signer. For an empty sent template, that create is denied by those rules and raises an Access Error. Fix: Guard the auto create signer flow with a sign requests check so that a sent template does not try to create a new signer on open. Task: 6591146 Forward-Port-Of: odoo/enterprise#132851 Forward-Port-Of: odoo/enterprise#132516
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