Wednesday, September 16, 2026
25 changes · saas-19.1
Resolved issues and error corrections
This fixes an issue in Discuss where quickly deleting the same attachment preview more than once could trigger an error, especially on slow connections. The system now tracks attachments already being removed, preventing duplicate requests and making attachment handling smoother for users.
Original PR description
**Steps to reproduce:** - Open Discuss app - Open any conversation - Setup the browser with throttled connection (e.g. Slow 4G) - Select multiple files to attach but don't send the message - While…
**Steps to reproduce:**
- Open Discuss app
- Open any conversation
- Setup the browser with throttled connection (e.g. Slow 4G)
- Select multiple files to attach but don't send the message
- While some are loading, double click the delete button of one of the loaded attachment preview
- Traceback will appear: `404 Not Found`
**Issue:**
On click the delete button triggers a deletion request on `/mail/attachment/delete`.
Repeatedly clicking the delete button before the first request was completed (and the `AttachmentList` get updated) but after the attachment was deleted, will cause a `404 Not Found` due to the `raise NotFound()` which was added by [1] in `mail_attachment_delete`.
```py
attachment = request.env["ir.attachment"].browse(int(attachment_id)).exists()
if not attachment or not attachment._has_attachments_ownership([access_token]):
request.env.user._bus_send("ir.attachment/delete", {"id": attachment_id})
raise NotFound()
```
**Fix:**
Prevent concurrent deletion requests for the same attachment by keeping track of attachments currently being deleted.
[1] https://github.com/odoo/odoo/commit/50f45dd436b97027cb1461410efeee7057dad4e8#
opw-6499125
Forward-Port-Of: odoo/odoo#285060The website editor color picker and shop comparison bar now handle longer translated labels more reliably. This prevents layout issues and ensures styling works consistently for users working in languages such as German.
Original PR description
Steps to reproduce: - Set the user language to German. - Open a color picker with the `Theme` tab in the website editor. - Check the color presets and the reset button. - Open the product comparison bar on the shop page. => Long labels do not fit and some styles are missing. Before this commit, long labels did not fit in the color preset picker. Some CSS selectors also relied on English `title` values, so their styles were not applied in other languages. After this commit, the preset picker adapts to long labels and the CSS selectors use dedicated classes that work in every language. task-6259086 Forward-Port-Of: odoo/odoo#286426
Fixes an issue where some form values in quotation header or footer PDFs disappeared when generating PDF quotes with newer PDF processing versions. Businesses using PDF Quote Builder can now rely on configured fields appearing correctly in customer-facing quotations.
Original PR description
Issue: --- When a quotation header/footer PDF has a hierarchical AcroForm field, the value set for that field is silently dropped from the generated PDF Quote when running on pypdf 5.4.0. Steps to…
Issue: --- When a quotation header/footer PDF has a hierarchical AcroForm field, the value set for that field is silently dropped from the generated PDF Quote when running on pypdf 5.4.0. Steps to reproduce: 1- Using a python 3.13 env, install requirements.txt (or otherwise run with pypdf==5.4.0 instead of PyPDF2). 2- Upload a header/footer PDF whose form field is a hierarchical field (`/T`/`/FT`/`/V` on the parent, not on the widget itself). The attachment from the ticket can be used as a sample to reproduce the bug. 3- Create a SO and select that document in the Quote Builder tab. 4- Print -> PDF Quote. 5- The value bound to that field does not appear in the printed PDF. Cause: --- After https://github.com/odoo/odoo/commit/4b02fbd717f62dd5345dad3ffb8d428c1c180007 `_add_pages_to_writer` renames the parent `/Field` object's `/T` when the widget itself has none. PyPDF2's `addPage` inserted the reader's page as is, keeping the widget's `/Parent`. pypdf 5.4.0 deep clones the page instead and ignores `/Parent` at every depth, so the widget loses the link to that field. It ends up with neither `/T` nor `/FT`, hence nothing matches the value mapping. Fix: --- Merge the field into its widget annotations instead: copy the prefixed `/T` and the inheritable keys onto each of them, then drop `/Parent`. The field's own `/T` is left untouched, so a field owning several widgets is filled on all of them. opw-6508728 Forward-Port-Of: odoo/odoo#287676 Forward-Port-Of: odoo/odoo#286186
This fix prevents website sitemap creation from running out of memory on sites with many pages. It does this by loading only the page data needed for sitemap generation, improving reliability without changing website behavior.
Original PR description
- Before this commit: All fields of ir.ui.view were prefetched, including arch_db and arch_prev. This caused an out-of-memory issue when dealing with many pages. - After this commit: Only required fields are fetched, avoiding unnecessary memory consumption from view architecture data. opw-6470593 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#284945
This fixes an issue where lot-tracked manufactured products could be placed in the main stock location instead of their assigned shelf after components were unreserved and re-reserved. Businesses using manufacturing and warehouse putaway rules will see finished goods routed to the correct storage location, reducing misplaced inventory.
Original PR description
Currently, when the user re-reserves a component and produces an MO for a lot-tracked product, putaway rules stop working. ## Steps to produce: - Install the Manufacturing application. - Go to…
Currently, when the user re-reserves a component and produces an MO for a lot-tracked product, putaway rules stop working. ## Steps to produce: - Install the Manufacturing application. - Go to Settings and enable Lots & Serial Numbers and Storage Locations. - Create a product named 'Laptop' and set it to be tracked by Lots. - Create a product named 'Graphics card' with some quantity on hand. - Create a Bill of Materials (BoM) for the Laptop with Graphics card as a component. - Create a location named 'Laptop Shelf' with 'WH/Stock' as its parent location. - Create a putaway rule: - When a product arrives in: WH/Stock - Product: Laptop - Store to: WH/Stock/Laptop Shelf - Create and confirm a Manufacturing Order (MO) for the Laptop. - Unreserve the components, open Details, and re-add the Graphics card. - Click Produce All and check the on-hand quantity list view of the Laptop. ## Issue: The Laptop is stored in WH/Stock instead of WH/Stock/Laptop Shelf. ## Root cause: When the user presses the unreserve button the finished move is passed to `_do_unreserve` https://github.com/odoo/odoo/blob/157874aad3aebef5bc9268de6e17530641107e31/addons/mrp/models/mrp_production.py#L2407 which unlinks all the move lines on finished product moves if they are not picked: https://github.com/odoo/odoo/blob/157874aad3aebef5bc9268de6e17530641107e31/addons/stock/models/stock_move.py#L1043 Now when the user presses the Produce All button, `button_mark_done` is called, which calls `_post_inventory` at: https://github.com/odoo/odoo/blob/a51ca77c825d2dba326dd540e2b4c04ef6c2385d/addons/mrp/models/mrp_production.py#L2236 `_post_inventory` assigns `lot_ids` to the finished product moves at: https://github.com/odoo/odoo/blob/157874aad3aebef5bc9268de6e17530641107e31/addons/mrp/models/mrp_production.py#L1928-L1930 This calls `_set_lot_ids` which creates a move line with quantity 1 at: https://github.com/odoo/odoo/blob/157874aad3aebef5bc9268de6e17530641107e31/addons/stock/models/stock_move.py#L679-L683 After that, `_post_inventory` sets the quantity for the finished product moves at: https://github.com/odoo/odoo/blob/157874aad3aebef5bc9268de6e17530641107e31/addons/mrp/models/mrp_production.py#L1935 Which calls `_set_quantity` and the delta quantity is now zero at: https://github.com/odoo/odoo/blob/157874aad3aebef5bc9268de6e17530641107e31/addons/stock/models/stock_move.py#L477-L483 Because `_set_lot_ids` has already created a move line that satisfies the move quantity, `_process_increase` is not called. Since `_process_increase` is not called, `_set_quantity_done` is never called at: https://github.com/odoo/odoo/blob/157874aad3aebef5bc9268de6e17530641107e31/addons/stock/models/stock_move.py#L463-L465 Since `_set_quantity_done` is not called, `_apply_putaway_strategy` is never called within that function at: https://github.com/odoo/odoo/blob/157874aad3aebef5bc9268de6e17530641107e31/addons/stock/models/stock_move.py#L2565 As a result, the laptop ends up in stock instead of on the shelf. This issue did not occur in lower versions because the feature to produce multiple lots was introduced in version 19.0 by the following commit: https://github.com/odoo/odoo/commit/4bb4e08066449177f89382718ceadd840ce90d0e Before this commit, only `_set_quantity` was called. Since no move line was created because lot_ids were not set in `_post_inventory`, `_process_increase` was called, which then called `_apply_putaway_strategy`, so the putaway rules were applied correctly. ## Solution: Apply putaway rules in the post-inventory function after the user manufactures a product, ensuring the items are placed in the correct locations and preventing products from being misplaced even when putaway rules are defined. opw-6517324 Forward-Port-Of: odoo/odoo#287390
This fixes an issue where users signing in while a live websocket connection was active could be sent back to the login page. The session is now saved during the connection handshake without overwriting the browser's current login cookie, making sign-in more reliable.
Original PR description
Before this commit, signing in from a page holding a websocket connection could land the user back on the login form. This happens because the websocket handshake marks the session dirty to get it written on disk, and _save_session sends a "session_id" cookie back for every dirty session. A handshake answered between the login response and the request that follows it therefore puts the pre-login session back in the browser. Note that the failure was seen on master, where a livechat tour signs in with the bus connected, but every version since 17.0 answers the handshake the same way. This commit saves the session from the handshake itself, so that the response carries no session cookie. https://runbot.odoo.com/odoo/error/947173 Forward-Port-Of: odoo/odoo#288151 Forward-Port-Of: odoo/odoo#287960
Point of Sale now keeps separate lines when selling multiple physical gift cards, eWallets, or discount-related items that require unique details. This prevents checkout failures and unsynced orders caused by duplicate gift card codes.
Original PR description
Steps to reproduce: - Create a gift card program (several programs sharing the same gift card product show the same issue) - In the PoS, sell a physical gift card: click the gift card product and set…
Steps to reproduce:
- Create a gift card program (several programs sharing the same gift card product show the same issue)
- In the PoS, sell a physical gift card: click the gift card product and set a code through "Sell physical gift card?"
- Click the gift card product again to sell a second physical card of the same value
- Validate the order
Issue:
The second unit is merged into the already coded orderline (one line, qty 2, one code), so the "Sell physical gift card?" link is no longer displayed and the second code cannot be entered. Validating the order then fails with "The operation cannot be completed: A coupon/loyalty card must have a unique code." and the order stays unsynced: the qty 2 line is split into two point entries both carrying the same gift_code, so two loyalty.card records are created with the same code.
Cause:
_setupGiftCardOptions() (and setupEWalletOptions()) pass merge=false so that a gift card line is never merged, and until 17.0 add_product() honored it ("options.merge !== false"). The 18.0 store refactoring dropped it: addLineToOrder() decides merging from a local variable that is only set to false when a price_unit is given in vals, and never reads opts.merge. The option became dead code, in point_of_sale's addLineToOrder as well as for the pos_discount caller.
Fix:
Honor opts.merge === false in addLineToOrder(). Callers that do not pass the option are unaffected, so the default merging behaviour is byte-for-byte the same; only the callers explicitly forbidding a merge (gift card, ewallet and discount lines) get their pre-18.0 behaviour back.
opw-6466324
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#287486
Forward-Port-Of: odoo/odoo#286237The website setup flow now handles temporary unavailability of Odoo's online website service instead of crashing. This prevents users from being blocked during the color palette step when creating a new website.
Original PR description
bug: configurator_missing_industry raised an uncaught RPC_ERROR when the IAP website API was unreachable, breaking the color palette step of the website configurator. steps: - Go in settings and set in the system parameters `website.website_api_endpoint` to anything that won't work - Create a new website - Traceback at the palette step fix: Wrap the IAP calls in a try/except for AccessError. task-6325919 Forward-Port-Of: odoo/odoo#280675
Testing a custom recorded tour from the tours list now properly loads the tour data before running it. This prevents the action from silently doing nothing, making it easier for users to validate recorded guidance and workflows.
Original PR description
Clicking "Testing" on a custom/recorded tour called startTour with mode "auto" and fromDB true, but getTour() only loaded from the database when mode was "manual". For a tour that only exists in the web_tour.tour record (not in the client-side registry), this left `tour` undefined and startTour() silently did nothing. Load from the database whenever fromDB is set, regardless of mode. Task-id: 6575435
Dutch VAT returns and ICP declarations could fail when companies used a Digipoort certificate with a password-protected private key. The update ensures the key is read in the expected format so submissions and status checks can proceed normally.
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#127528
The map view now uses the record limit configured on the action when no map-specific limit is set. This makes map behavior consistent with list and kanban views, helping users see the expected number of records.
Original PR description
Backport of odoo/enterprise#130336 The map view only considered the `limit` set in the arch, ignoring the one coming from the action (`ir.actions.act_window.limit`), unlike other views (list, kanban) which fall back to it. task-6531776 Forward-Port-Of: odoo/enterprise#131423
This fixes a subcontracting issue where manually changing component lines during production could leave stray inventory records behind. Removing those orphan records prevents later transfer validation errors and keeps inventory history cleaner.
Original PR description
#### Issue When manually adding component lines in the subcontracting Record Production flow, extra stock move lines can remain in the database without a linked stock move. Those lines have no…
#### Issue When manually adding component lines in the subcontracting Record Production flow, extra stock move lines can remain in the database without a linked stock move. Those lines have no move_id, so their related state is empty. They can later be selected by stock reservation code and cause validation errors when processing the transfer. #### Steps to reproduce 1. Create a subcontracted product. 2. Confirm a subcontracting receipt/purchase flow for that product. 3. Open the subcontracting Record Production popup. 4. Remove an existing component line. 5. Add a new component line for another storable product available at the subcontractor location. 6. Save/record the production. 7. Check stock.move.line records , Inventory > History > Group By Status. An extra stock move line is left with no move_id. #### Root cause The inverse of `mrp.production.move_line_raw_ids` already collects and deletes move lines detached from existing raw moves. However, when the user adds a new component product, the inverse creates a new additional raw move. Creating that raw move can also create reserved move lines. The inverse then replaces `move.move_line_ids` with the user-entered lines, but it did not collect the newly created reserved lines before replacing them. Those reserved lines were detached from the move and left in the database as orphan stock move lines. #### Fix Apply the same cleanup logic to newly created additional raw moves: collect their auto-created move lines before replacing `move_line_ids`, then unlink those detached lines at the end of the inverse. opw-6304649 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#281864 Forward-Port-Of: odoo/odoo#276235
This update restores the previous way Mexican electronic payment complements calculate and report amounts with the maximum allowed decimal precision. It aligns Odoo with the latest guidance from Quadrum following government consultation, reducing the risk of rejected or inconsistent payment documents.
Original PR description
Quadrum reverted their changes because > Derived from a consultation with the government we reverted to our previous behavior, we recommend using the maximum number of decimals allowed Reverts commit https://github.com/odoo-dev/enterprise/commit/f13d204dacf9eae98c78de26e9f2e54387a0eaee as well opw-6561617 Forward-Port-Of: odoo/enterprise#131640 Forward-Port-Of: odoo/enterprise#131581
Helpdesk teams can now link to more than one community forum without the website page crashing. This ensures customers using the Ask the Community option see the correct forum listing instead of an error, including when eLearning forum features are installed.
Original PR description
### Steps to Reproduce: 1) Install `website_helpdesk_slides_forum` module 2) On a Helpdesk team, link 2 (or more) forums under `Commnity Forums`. 3) Click on website smart button and then click `Ask…
### Steps to Reproduce: 1) Install `website_helpdesk_slides_forum` module 2) On a Helpdesk team, link 2 (or more) forums under `Commnity Forums`. 3) Click on website smart button and then click `Ask the community` button. ### Error: ``` odoo.addons.base.models.ir_qweb.QWebException: Error while render the template KeyError: '_forums' Template: website_helpdesk_slides_forum.helpdesk_forums ``` ### Root Cause: https://github.com/odoo/enterprise/blob/1bdd673c5fd19e335dc0948d2beed7a282e5e172/website_helpdesk_forum/controllers/website_forum.py#L18-L22 On clicking the "Ask the Community" button calls the `helpdesk_forums` controller. A single forum redirects straight to it. Several forums get rendered through `get_template_xml_id()`, with only `forums` in context. `get_template_xml_id()` is overridden in `WebsiteSlidesForumHelpdesk`, which points to : https://github.com/odoo/enterprise/blob/1bdd673c5fd19e335dc0948d2beed7a282e5e172/website_helpdesk_slides_forum/views/helpdesk_templates.xml#L3-L5 a primary copy of the inner partial `forum_all_all_entries`. That partial needs `_forums` (and, once extended by website_slides_forum, courses_discussions too), both only ever set by the page template's own t-call: [Reference](https://github.com/odoo/odoo/blob/23af2b443735c6d3a2f64e44f9ea5da45638b052/addons/website_forum/views/forum_forum_templates_forum_all.xml#L18-L19) Since `helpdesk_forums` is rendered directly instead of going through `website_forum.forum_all`, `_forums` is never set, hence `KeyError` occurs. The base `website_helpdesk_forum` module has the same root cause one level up, surfacing as a different error: https://github.com/odoo/enterprise/blob/1bdd673c5fd19e335dc0948d2beed7a282e5e172/website_helpdesk_forum/controllers/website_forum.py#L24-L25 `website_helpdesk_forum.forum_all` does not exist anywhere in the codebase. A team with multiple forums, without `website_helpdesk_slides_forum` installed, hits ``` ValueError: View 'website_helpdesk_forum.forum_all' in website 1 not found ``` Instead of a listing page. ### Fix: - Both `get_template_xml_id()` implementations now return the real, working `website_forum.forum_all` page template, which already sets `_forums/courses_discussions` through its own `t-call` and wraps the result in website.layout. - `helpdesk_forums()`'s render values are now built through an overridable `_get_helpdesk_forums_render_values()` hook instead of a hardcoded dict, so subclasses can extend the context. - `website_helpdesk_slides_forum` uses that hook to set `hide_forum_slides_link=True`, and adds one small non-primary view inheriting `website_slides_forum.forum_all_all_entries` that conditions the `/slides` promo link (not the "Course" badge, which doesn't navigate anywhere) on `not` `hide_forum_slides_link`. This targets the link element itself rather than a `t-call` site in `forum_all`, so it correctly applies whether `website_slides_forum` groups a given forum as "regular" or "course-linked". **opw-6332175** Forward-Port-Of: odoo/enterprise#131435 Forward-Port-Of: odoo/enterprise#124104
Fixed an invoicing issue where timesheets from one sales order could be excluded when invoicing multiple orders together. This ensures each invoice uses the correct date range, reducing missed billable work and improving invoice accuracy.
Original PR description
### Steps to reproduce: - Install `sale_subscription_timesheet` module - Create a subscription (Order A) with delivered-timesheet products and log timesheets within its current period (e.g., January)…
### Steps to reproduce: - Install `sale_subscription_timesheet` module - Create a subscription (Order A) with delivered-timesheet products and log timesheets within its current period (e.g., January) - Create a standard order (Order B) with delivered-timesheet products and log timesheets outside of Order A's period (e.g., March) - Select both orders and invoice them simultaneously in a single batch (uncheck 'Consolidated Billing') > Result: Order B's invoice is created but silently fails to link its March timesheets, because they fall outside Order A's January window. ### Cause of Issue: In `_link_timesheets_to_invoice`, the `start_date` and `end_date` parameters are overwritten inside the invoice line loop when `_get_range_dates` is called. Because these variables are never reset per iteration, the first order that yields a date range locks in that window for every subsequent invoice line in the batch. Timesheets outside this leaked window are silently ignored. Additionally, the `_get_range_dates` hook is invoked before verifying if the invoice line actually contains timesheet products, causing it to execute on empty recordsets (e.g. invoice line notes) ### Fix: Prevent the date range leak by preserving the original parameter state of the dates and safely ensure the extension hook is never invoked with an empty recordset. opw-6507110 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#287358 Forward-Port-Of: odoo/odoo#284781
Adds a test to ensure batch invoicing for multiple subscription orders uses each order's own billing period. This helps prevent invoices from accidentally including timesheet entries outside the correct date range.
Original PR description
A test case to guarantee that batch invoicing multiple orders respects their individual time frames. This test is made to assert the fix in this [PR](https://github.com/odoo/odoo/pull/284781) opw-6507110 Forward-Port-Of: odoo/enterprise#130928 Forward-Port-Of: odoo/enterprise#129404
Australian payroll users can now register super payments successfully from a pay run, regardless of how they opened the record. This prevents a context-related error that blocked payment registration when using the Pay Runs kanban view.
Original PR description
Current behavior: -- Registering a super payment from a pay run opened out of the Pay Runs kanban fails with "Wrong value for account.payment.state. The same super contribution registers without…
Current behavior: -- Registering a super payment from a pay run opened out of the Pay Runs kanban fails with "Wrong value for account.payment.state. The same super contribution registers without error when opened from Payroll > Reporting > Australia > Super Contributions. Expected behavior: -- Register Super Payment should work regardless of how the super contribution record was reached. Steps to reproduce: -- - Take an Australian pay run through to Paid - Open Payroll > Payslips > Pay Runs, kanban grouped by Status (default) - Open the pay run from the Paid column, then Super Submissions - Lock the super contribution and click Register Super Payment Cause of the issue: -- The Pay Runs kanban is grouped by state, so the web client adds default_state to the context of records opened from a column. That context follows the smart button into action_register_super_payment, where account.payment is created without an explicit state. The ORM applies default_state from the context which is a hr.payslip.run value that account.payment.state does not accept. The same leak reaches account.move through action_post, which creates the payment journal entry. Fix: -- apply with_context(clean_context(self.env.context)) to the recordset to remove the default_state key opw-6478084 Forward-Port-Of: odoo/enterprise#128724
Product carousels set to single-item scrolling now slide correctly on right-to-left websites such as Arabic. This prevents empty spaces and distorted images, improving the shopping experience for customers using RTL languages.
Original PR description
**Description of the issue/feature this PR addresses:** When a dynamic product carousel is set to single-image scrolling mode in an RTL language (e.g., Arabic), sliding between items behaves…
**Description of the issue/feature this PR addresses:**
When a dynamic product carousel is set to single-image scrolling mode in an RTL language (e.g., Arabic), sliding between items behaves inconsistently, resulting in empty spots or temporarily distorted images.
This occurs because the DOM manipulation creating the infinite scroll was coded for LTR physical movements. In LTR, the first DOM element is visually on the left. In RTL, the visual layout is mirrored, with the first DOM element visually on the right. When we then trigger a "left" slide in RTL, the LTR-based JavaScript doesn't appropriately move the last element to the first index prior to the animation, resulting in an empty gap, among other issues.
This commit resolves the issue by checking the document's text direction. By swapping the target direction ("left" or "right") in onSlideSingleScroll and onSlidSingleScroll when RTL is active, the underlying array operations now correctly align with the animation.
**Steps to reproduce:**
- Website > Edit > add Product Carousel > Customize > change Scrolling Mode to Single
- Website > Edit > Theme > Website > Language > change to Arabic
- Click the arrows in the product carousel > observe inconsistent scrolling with visual glitches
**Current behavior before PR:**
- Visual glitches when sliding carousels in single-image scrolling mode for RTL languages
**Desired behavior after PR is merged:**
- No visual glitches when sliding carousels in single-image scrolling mode for RTL languages
opw-6490277
Forward-Port-Of: odoo/odoo#286042Clicking at the end of text inside an editor button now keeps the cursor in the expected position instead of jumping outside the button. This makes editing button labels more reliable and prevents frustrating text editing behavior for website or content editors.
Original PR description
Problem: When trying to place the caret at the end of a button's content with the mouse, the caret always moves outside of the button instead of staying inside it. Cause: This happens because of the previous commit https://github.com/odoo-dev/odoo/commit/c8e93dbc806b5ea511cce695afbc9e81e30a1ca9 (which aimed to fix placing the caret after a link when at the end of a paragraph in the editable), which was not fixing the issue properly. Solution: Check if the click happens at the end of a link and the caret will move inside the link then manually place the selection after the link. Steps to reproduce: - Add a button with one character. - Try to put selection after that character. - Selection always jumps after the button. opw-6499237 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#287853 Forward-Port-Of: odoo/odoo#284197
This fix prevents sales quotations from failing when a user saves immediately after reordering order lines. It temporarily pauses saving while the reorder action completes, improving reliability for sales teams working with larger quotations or quick keyboard shortcuts.
Original PR description
When we save a sale order just after reordering its sale order lines, there's a chance that an error is thrown Steps to reproduce: 1. Install Sales 2. Go to Sales and open any quotation that has…
When we save a sale order just after reordering its sale order lines, there's a chance that an error is thrown Steps to reproduce: 1. Install Sales 2. Go to Sales and open any quotation that has multiple sale order lines 3. Reorder a sale order line and immediately save (there's more chance to reproduce if you have a lot of sol and if you use the shortcut `ALT + S`) 4. An error is thrown Issue: It is possible that the save takes place during the execution of super.sortDrop. This reassigns the id of all the records in this.props.list.records Therefore, calling `_handleQuantityAdjustment` with the recordMap computed before the save uses an id that has disappeard from this.props.list.records so we cannot find it at https://github.com/odoo/odoo/blob/4973903252865a6a7a2da235bc3a01675dbbff4d/addons/sale_management/static/src/fields/sale_order_line_field/sale_order_line_field.js#L263 which eventually throws an error Solution: Suspend any save mechanism when sortDrop starts and resume it when sortDrop has finished opw-6483206 Forward-Port-Of: odoo/odoo#286787
Automation rules could run twice when a new record received a default or computed field value during creation. This fix ensures the system recognizes records already handled, preventing duplicate emails, activities, or similar automated actions.
Original PR description
An automation rule triggered when a field reaches a specific value can run its actions twice when a record is created without that field provided explicitly. For example, a rule that sends one email…
An automation rule triggered when a field reaches a specific value can run its actions twice when a record is created without that field provided explicitly. For example, a rule that sends one email and schedules two activities can send two emails and schedule four activities for one record. ### Reproduction Steps Create an automation on `crm.lead` that triggers when the stage is set to "New", with one email action and one activity action. Create an opportunity through the website contact form, which leaves the stage unset. The lead is assigned to the "New" stage, but both actions run twice. ### Cause The lead stage is computed and read during creation. That computation can trigger the automation before the creation hook processes the same record, causing both paths to run the actions. The processing code tracks records already handled during nested computations. The feedback flag used for this tracking is attached to the records, but the code checked the automation context instead. When the computation was triggered during creation, the flag was therefore missed. ### Fix Read the feedback flag from the records so nested computations update the shared tracking state. The creation hook then skips records already processed by the computation. opw-6331134 Forward-Port-Of: odoo/odoo#288222
This fix prevents Odoo from showing client error popups when a user closes a form or wizard while a file upload is still finishing. It avoids confusing errors and prevents uploaded files from being left unattached when the original screen is already gone.
Original PR description
Description of the issue/feature this PR addresses: A file upload through `FileInput` (and therefore every `many2many_binary` field) still calls `onUpload` after the component has been destroyed.…
Description of the issue/feature this PR addresses:
A file upload through `FileInput` (and therefore every `many2many_binary` field) still calls `onUpload` after the component has been destroyed. When the form or dialog holding the field goes away while the upload request is in flight, the response crashes the web client with two "Odoo Client Error" popups and the uploaded attachment is orphaned.
Steps to reproduce on a stock database (19.0, and 17.0 carries the same code):
1. Open an Email Template form (Settings > Technical > Email Templates), page *Options*, field *Attachments*. Any `target="new"` wizard with a `many2many_binary` field shows the same thing, e.g. the Recruitment *Refuse* wizard.
2. Pick a file that takes a moment to upload (a few MB, or a slow connection).
3. Before the file tile appears, leave the form through the breadcrumb, or close the wizard with Escape, its close button, or its action button. Nothing in the field shows that an upload is still running, so users do this routinely.
Current behavior before PR:
When the upload response arrives, two errors are raised:
```
UncaughtPromiseError > Component is destroyed
at webRead <- _loadRecords <- _applyCommands <- addAndRemove
TypeError: Cannot set properties of null (setting 'value')
at FileInput.onFileInputChange
```
Cause: `FileInput` posts the file through the `http` service, which is not protected against a destroyed component (unlike `orm`), so the response still reaches `onFileInputChange` after the component, the field and the form controller are gone. It then calls `onUpload`, which for `Many2ManyBinaryField` links the attachment through the form controller's protected ORM (hence the rejection), and it clears a file input ref that no longer exists (hence the TypeError).
We hit this in production on three different wizards over a few months. The client error logs users saved all carry the same trace, and the audit log shows the dialog's action completing a fraction of a second before the upload landed.
Desired behavior after PR is merged:
After the upload resolves, `onFileInputChange` stops when the component is destroyed: nobody is left to receive the uploaded files, and this matches how the protected services already treat a destroyed component. No error is raised, and `onUpload` is not called.
A Hoot test in `file_input.test.js` mounts a `FileInput` behind a `t-if`, starts an upload, unmounts the input while the upload is pending, resolves the upload, and asserts that `onUpload` was not called. It fails before the fix with the two errors above and passes after.
`master` already guards the ref write through its signal-style refs, but still calls `onUpload` on the destroyed component, so the first error remains there.
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#286831This fixes a form display issue where Save and Discard buttons could remain visible even after there were no real changes left to save. Users who re-enter equivalent values, such as the same number with different formatting, will now see the form return correctly to its clean state.
Original PR description
**Description of the issue/feature this PR addresses:** Fixes #287978 In `useInputField` (`input_field_hook.js`), `onChange` and `commitChanges` only trigger the `FIELD_IS_DIRTY` bus event with…
**Description of the issue/feature this PR addresses:** Fixes #287978 In `useInputField` (`input_field_hook.js`), `onChange` and `commitChanges` only trigger the `FIELD_IS_DIRTY` bus event with `false` when the newly typed value actually differs from the value already stored on the record. When the typed text parses to the *same* value as what is already on the record (e.g. retyping `6,250` after the field already holds `6,25`), the code takes the `else` branch, which only resets the displayed text and never notifies the bus that the field is no longer dirty. `form_status_indicator.js` keeps whatever `fieldIsDirty` state it last received from that bus event, and shows the Save/Discard buttons whenever `root.dirty || fieldIsDirty`. Since nothing ever fires `FIELD_IS_DIRTY(false)` in that `else` branch, `fieldIsDirty` stays stuck at `true`. `Discard` correctly resets `root.dirty`, but has no way to reset `fieldIsDirty`, so the Save/Discard buttons keep showing even though the record is clean. **Current behavior before PR:** 1. Edit a field to a new value, click away (value committed, indicator shows Save/Discard as expected). 2. Edit the same field again, this time typing different text that parses to the *same* value (e.g. `1.20` when the field already holds `1.2`), click away. 3. Click **Discard**. The record is reverted, but the Save/Discard buttons remain visible. They stay stuck until the page is reloaded. **Desired behavior after PR is merged:** Retyping different text that parses to the same value clears the field's dirty state like any other case where the field ends up unchanged, so Discard (or any other action) correctly hides the Save/Discard buttons once there is nothing left to save. Forward-Port-Of: odoo/odoo#288030
Auto-completing a vendor bill no longer replaces a manually selected recipient bank account when the bill currency has not changed. This prevents accidental payment details changes for vendors with multiple bank accounts, reducing correction work and payment risk.
Original PR description
### Issue: When a bank account is manually set on a bill for a partner with multiple bank accounts, using the Auto-Complete feature could reset the manual selection even without a currency change ###…
### Issue: When a bank account is manually set on a bill for a partner with multiple bank accounts, using the Auto-Complete feature could reset the manual selection even without a currency change ### Cause: When `invoice_vendor_bill_id` is set, an onchange assigns `currency_id` unconditionally, even when it is the same value This triggers `_compute_partner_bank_id`, which always recomputes the best matching bank account from scratch without considering the currently set value If the currency did not change, this recompute is unnecessary and silently overrides the manual selection ### Steps to reproduce: - Install `account` - Create a Vendor with 2 bank accounts (keep default values to have equal priority on all accounts) - Create and post a Bill for this vendor with at least one line - Create a new Bill for the same vendor - Set the Recipient Bank to the second account in the list - In Auto-Complete, select the first Bill Before the fix, the Recipient Bank is reset to the first account opw-6210414 Forward-Port-Of: odoo/odoo#283479
Salespeople now receive upsell warning activities when several separate timesheet entries together exceed a prepaid service threshold. This helps teams spot upsell opportunities reliably, even when work is recorded in smaller increments rather than all at once.
Original PR description
Steps to reproduce: -------------------------------------------- 1. Install `sale_timesheet` module 2. Create a product with: * Type: Service * Invoicing Policy: Prepaid/Fixed Price * Create on…
Steps to reproduce:
--------------------------------------------
1. Install `sale_timesheet` module
2. Create a product with:
* Type: Service
* Invoicing Policy: Prepaid/Fixed Price
* Create on Order: Project & Task
* Upsell Threshold: Set some value (e,g. 600%)
3. Create and confirm the sale order with that product
4. Create and confirm an invoice for the sale order
5. From Recorder Hours Smart button:
* Add two timesheets for hours 3 & 4 (Combined h > Threshold Percentage/100)
Observation:
--------------------------------------------
* When adding a single 7h timesheet, an upsell warning activity is correctly created for the salesperson in the chatter.
* When adding multiple incremental timesheets whose combined duration exceeds the threshold, no upsell warning activity is created.
Issue:
--------------------------------------------
* The `_compute_field_value` method filters on `invoice_status != 'upselling'` before recomputing the new invoice status.
* After the first incremental timesheet is added, the sales order already has the upselling status from the previous computation.
* As a result, subsequent computations skip the upsell activity logic, even though `has_displayed_warning_upsell` is still `False` and no warning activity has yet been created.
Solution:
--------------------------------------------
* Removes the `so.invoice_status != 'upselling'` condition from the filter.
* The order-level `invoice_status` was never the right place to control upsell activity creation. An order can have `invoice_status == 'upselling'` for various reasons (delivered > invoiced), but that doesn't mean all lines have already triggered their upsell warnings.
opw-6117754
Forward-Port-Of: odoo/odoo#287642
Forward-Port-Of: odoo/odoo#266275