Wednesday, September 23, 2026
20 changes · saas-19.4
Resolved issues and error corrections
Point of Sale now correctly loads product option values when a product is updated using an existing attribute after the browser has cached session data. This prevents errors when opening products with variants and avoids forcing users to clear cached data or recreate sessions.
Original PR description
Steps to reproduce: - Open a PoS session so the browser caches the data (IndexedDB) - Add an attribute line on a product available in PoS, using an attribute that already existed and was not modified…
Steps to reproduce: - Open a PoS session so the browser caches the data (IndexedDB) - Add an attribute line on a product available in PoS, using an attribute that already existed and was not modified since - Reopen the PoS and click the product Issue: TypeError: undefined is not an object (evaluating 'values[0].is_custom') in openConfigurator. The attribute line is loaded but none of its values are; the variants have no attribute values on the frontend. Closing the session or reloading the browser does not help since the cache is kept. Cause: On an incremental load (pos_last_server_date in context), every model only returns the records written after the last sync. The domain of product.template.attribute.value restricts attribute_id to the ids in data['product.attribute'], which in that case only holds the attributes modified since the last sync. Values created on an older attribute are therefore excluded, and the frontend drops the ids it cannot resolve. Fix: Filter attribute_id with the domain of product.attribute itself instead of the ids of the records returned in the current load. A full load returns the same records as before. opw-6568808 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#288065
Users should no longer receive both a browser alert and an in-app alert for the same mention when Odoo is open but not active. This reduces notification noise and helps users trust that each alert represents a separate event.
Original PR description
Steps to reproduce: - Grant browser notification permission (subscribes to web push). - Set user A's `notification_type` to `inbox` in preferences and reload. - Keep A's tab open but unfocused. - As user B, post an @-mention on a non-channel chatter record targeting A. A receives two alerts: the JS one from the `mail.message/inbox` bus event and the native notification from web push. `OutOfFocusService.notify()` gated JS skip on `message.thread.model`, but the server never sends `mail.thread`, so chatter fell through. Replace it with a `message_type` + `!isSelfAuthored` filter mirroring the server side `_notify_get_recipients_for_extra_notifications`. Also swap the `isInbox` SW handshake workaround added by [1] when the user is busy. [1]: https://github.com/odoo/odoo/pull/232431 Forward-Port-Of: odoo/odoo#290052 Forward-Port-Of: odoo/odoo#282981
Products with multiple variants are no longer shown as sold out just because the first variant has no stock. The shop page now keeps Add to Cart available when at least one variant can still be purchased, preventing lost sales and customer confusion.
Original PR description
Steps to reproduce: =================== 1. Create a storable product with two variants and untick "Continue selling when out-of-stock". 2. Leave the first variant at 0 in stock, put some stock on the…
Steps to reproduce: =================== 1. Create a storable product with two variants and untick "Continue selling when out-of-stock". 2. Leave the first variant at 0 in stock, put some stock on the second one. 3. try the "Add to Cart" option of the shop page. => The product has no add to cart button, as if it were sold out, while its second variant can be bought. Reordering the variants so that the one in stock comes first brings the button back. Root cause: =========== `product.template._is_sold_out()` delegates to `product_variant_id`, which is `product_variant_ids[:1]`, so a whole template is declared sold out on the sole basis of its first variant. `_website_show_quick_add()` then hides the button for the template, whatever the stock of the other variants. The helper has looked at that single variant since it was written: - [1] added it to hide the button of sold out products. Fix: ==== Consider the template sold out only when all of its variants are. The check stops at the first variant in stock, so a product that can be bought still costs a single stock lookup. [1]: https://github.com/odoo/odoo/commit/4ae197cc770cec2058e1f113209c4f93a4a0f4b2 opw-6520052 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#289022 Forward-Port-Of: odoo/odoo#286781
Combo products can no longer be added to the cart unless every required choice has been selected. This prevents customers from checking out with incomplete combo orders, improving order accuracy and avoiding fulfillment issues.
Original PR description
Steps to reproduce: =================== 1. Publish a combo product whose choices each offer more than one item, so the combo configurator is shown. 2. On the shop, open the product and click "Add to…
Steps to reproduce: =================== 1. Publish a combo product whose choices each offer more than one item, so the combo configurator is shown. 2. On the shop, open the product and click "Add to Cart". 3. Select an item for one combo choice only. The dialog's "Add to cart" button stays disabled. 4. Remove the `disabled` attribute from that button with the browser developer tools and click it. => The combo is added to the cart with one of its choices unanswered, and the order can be paid in that state. Root cause: =========== The incomplete selection is only prevented on the client side, by disabling the button until every choice has been answered. Server side, `/website_sale/combo_configurator/update_cart` only rejects a completely empty selection, never that the selected items cover every choice. Fix: ==== Reject the request unless the selected combo items cover exactly the combo choices of the product. Comparing the set of choices rather than counting the items also rejects a selection answering the same choice twice while leaving another one unanswered. opw-6478837 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#289005 Forward-Port-Of: odoo/odoo#283465
Duplicating a tax now keeps its domestic status correctly instead of silently marking it as non-domestic. This helps prevent incorrect tax behavior and fiscal position handling when businesses copy existing tax records.
Original PR description
_compute_is_domestic checks whether the company's domestic_fiscal_position_id is part of the tax's fiscal_position_ids. Because this field is both stored and precompute=True, duplicating a tax recomputes it on a virtual new() record before insert. On that virtual record, fiscal_position_ids is resolved through NewId pseudo-ids while company_id (a plain Many2one) keeps its real id, so the containment check always failed, silently turning is_domestic into False on any duplicate. Use fiscal_position_ids._origin to compare against the real underlying records instead. opw-6575113 Forward-Port-Of: odoo/odoo#289896 Forward-Port-Of: odoo/odoo#289246
Unposted accounting entries dated in a locked period are now deleted instead of being reversed. This prevents cancellation flows, such as payroll cancellations, from creating accounting amounts that were never actually posted.
Original PR description
_unlink_or_reverse relies on _can_be_unlinked, which only looks at the hash and the lock dates. A draft entry dated in a locked period thus lands in the reversal branch: it is copied, the copy is posted after the lock date and the draft stays behind. The reversal books amounts that were never posted in the first place. Lock dates protect what has been booked. An entry that was never posted can be deleted whatever its date. Seen with hr_payroll_account when a payslip is cancelled after the accountant locked the period without posting the salary entry. Any caller of _unlink_or_reverse (assets, loans, deferred entries) behaves the same. Forward-Port-Of: odoo/odoo#289980
Shipment cancellations now send each delivery carrier only the shipment information that belongs to it. This prevents duplicate cancellation attempts and avoids incorrect or failed cancellations when multiple carriers are involved.
Original PR description
When cancelling shipments for multiple pickings with different delivery carriers, passing the entire recordset (self) instead of the individual picking to the carrier's cancel_shipment method causes each carrier to receive tracking references that do not belong to it. This results in API errors or silent wrong cancellations at the carrier level, and each shipment being cancelled N times instead of once, where N is the total number of pickings being processed. Forward-Port-Of: odoo/odoo#289809
Fixes an issue where creating a new website with eCommerce and design themes could fail at the final setup step due to missing theme content. The setup now checks all website themes for the needed snippets, making multi-website and eCommerce configuration more reliable.
Original PR description
# How to reproduce - Start a new db with the design-themes addons - Install the eCommerce module - Skip the first website configurator - Go to Website > Configuration > Settings > New website - Go…
# How to reproduce - Start a new db with the design-themes addons - Install the eCommerce module - Skip the first website configurator - Go to Website > Configuration > Settings > New website - Go through all the steps as usual # The issue At the last step, you will get, depending on the version either : - A traceback, pointing out the fact that a template is missing - A popup signaling that another module operation is being processed, even though there are none The configurator won't go through in both cases # Cause When calling `configurator_apply`, we will try to get the content of every snippet specific to theme installed with the website : https://github.com/odoo/odoo/blob/ef6765208d400aa1252cd4dff5bc074ea551bbc2/addons/website/models/website.py#L910 But, when trying to find the 'website_sale.configurator_homepage_s_dynamic_snippet_category_list' view, we find nothing and an error is thrown. This view should be created by the `_generate_primary_snippet_templates` function, which is called when loading any theme module or `website` or `website_sale`: https://github.com/odoo/odoo/blob/ef6765208d400aa1252cd4dff5bc074ea551bbc2/addons/website/models/ir_module_module.py#L594 The issue is that, to know which snippets to install, that function relies on `get_current_website()`: https://github.com/odoo/odoo/blob/ef6765208d400aa1252cd4dff5bc074ea551bbc2/addons/website/models/ir_module_module.py#L694 Which relies on multiple things to find the current website, notably the current request's session : https://github.com/odoo/odoo/blob/ef6765208d400aa1252cd4dff5bc074ea551bbc2/addons/website/models/website.py#L1374-L1381 Since [this commit], the request is not kept when creating a new registry, which is done when loading a module. `get_current_website()` fallbacks on the first website's theme, which is the default theme (since we skipped the first configurator). That theme does not have the `website_sale` snippet in its manifest, contrary to others. # Proposed solution Since a new registry is created, we cannot use the context, nor the current request. We also cannot add a parameter to the `_generate_primary_snippet_templates` function since we are in stable (It would not help much anyway because this function is called when loading a module, so giving the current website is not possible). This means that we have to stop relying on `get_current_website()` to differentiate between the two use case : - Installing a theme module - Installing a module with specific snippets We can determine if the module being installed is a theme by looking at its category. If it is not, we assume its the other use case as there should not be any other. In that case, instead of installing the snippets of the theme of the current website, we check the theme of all websites. Later in the code, we take only distincts snippets and check that a corresponding view do not already exist, so there should be no risk of duplicate : https://github.com/odoo/odoo/blob/1f2ea7be8377c906ded75da893d739109db93c4d/addons/website/models/ir_module_module.py#L692 https://github.com/odoo/odoo/blob/1f2ea7be8377c906ded75da893d739109db93c4d/addons/website/models/ir_module_module.py#L712 https://github.com/odoo/odoo/blob/1f2ea7be8377c906ded75da893d739109db93c4d/addons/website/models/ir_module_module.py#L608 Furthermore, I believe it makes more sense to check every website anyway. If the `website_sale` module is installed, and there already exists multiple website with different themes, these themes might require different `website_sale` specific snippets. opw-6517556 [this commit]: https://github.com/odoo/odoo/commit/be8c85b4ccb10d6f16ddea3ab493e4c9aac749e1 Forward-Port-Of: odoo/odoo#285935
This fixes an issue that could prevent users from signing in to Odoo through the Gmail plugin. The plugin now uses the current configuration method, avoiding an error during authentication and restoring a smoother login experience.
Original PR description
In version 19.1+, the ir.config_parameter model no longer has the function get_param. Instead you use get_int, get_str, etc. This was causing a traceback in auth_access_token when a user tries to authenticate with Odoo via the Odoo gmail plugin. Forward-Port-Of: odoo/odoo#289957
Live Chat now checks URL matching rules when they are created or updated, so invalid patterns are rejected before they can break the website experience. This helps administrators catch configuration mistakes early and prevents visitors from encountering chat initialization errors.
Original PR description
Currently, an error occurs when a user opens the website and Live Chat is initialized with an invalid URL regex. Steps to Reproduce: - Install `website_livechat` without demo data. - Open `Live Chat`…
Currently, an error occurs when a user opens the website and Live Chat is initialized with an invalid URL regex. Steps to Reproduce: - Install `website_livechat` without demo data. - Open `Live Chat` and, in the `YourWebsite.com` channel, click the `three-dot` menu and select `Configure Channel`. - Go to the `Rules` tab. - Add a rule, select `Welcome Bot` in `Chatbot`, and set an invalid regex such as `*` in `URL Regex` and save. - Click `Join Channel`. - Open the `website`. `re.PatternError: nothing to repeat at position 0 when serializing dict item 'result' ` - when `URL Regex is '['` `re.error: unterminated character set at position 0 when serializing dict item 'result'` When a user opens the website, Live Chat is initialized by checking operator availability and finding the matching country/URL rule [1]. The rule matching checks whether `regex_url` matches the current page URL. It uses `re.search()` [2], which compiles the regex pattern before searching. If `regex_url` contains an invalid pattern such as `*`, `re.search()` raises `nothing to repeat at position 0` because `*` must follow a preceding regex element. Similarly, `[` starts a character set (`[...]`) but has no closing `]`, causing `unterminated character set at position 0`. This commit adds a constraint to validate `regex_url` and reject invalid regex patterns when creating or updating Live Chat rules. [1]- https://github.com/odoo/odoo/blob/f962f1eccecfdbc95375f795174c8f687459b521/addons/im_livechat/controllers/main.py#L87 [2]- https://github.com/odoo/odoo/blob/f962f1eccecfdbc95375f795174c8f687459b521/addons/im_livechat/models/im_livechat_channel.py#L387 sentry-7679767396 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#283545
This fixes an error that could occur when opening image attachments linked to products, improving reliability in the Odoo web interface. It also improves internal test handling for attachments that reference other attachments, helping prevent similar issues in future releases.
Original PR description
### Steps to Reproduce: 1. Go to Sales > Products and select a product 2. Upload an image for the product 3. Go to Settings > Technical > Attachments 4. Click on one of the image attachments 5.…
### Steps to Reproduce: 1. Go to Sales > Products and select a product 2. Upload an image for the product 3. Go to Settings > Technical > Attachments 4. Click on one of the image attachments 5. Observe the Client Error: "Uncaught Promise > Invalid props for component 'Many2One': 'domain' is not a function" ### Issue: In `attachment_patch.js`, the domain for the `Many2OneReferenceField` is overridden when an attachment is linked to another attachment. The patch calculates the domain and incorrectly assigns a static Array (using `.toList()`) to `props.domain`. In Odoo 19, the `Many2One` component enforces strict prop validation (caught when Owl's debug mode is enabled during testing), causing a crash because it expects the domain prop to be a function. ### Solution: We can just wrap the resulting domain Array in an arrow function so that it evaluates dynamically when the `Many2One` component mounts. Also added a unit test to explicitly cover this patched scenario. The test manually applies a local patch to `Many2OneReferenceField.prototype` to accurately replicate the environment and verify the fix. ### For saas-19.4+: ### Issue: In the MockServer, when a `many2one_reference` field points to its own model (for example, an `ir.attachment` linked to another `ir.attachment`), the record is incorrectly erased and replaced by only its ID. This causes testing environments to lose record data when evaluating self-referencing relations, preventing accurate unit testing. ### Solution: We can update the mock model mapping to properly retain the full record data when the comodel is the same as the main model. opw-6563954 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#287951
Fixed an issue where partially processing a subcontracted purchase receipt in the Barcode app could block validation with an error. Businesses can now complete these partial receipts and backorders reliably, avoiding delays in receiving subcontracted products.
Original PR description
eproduction steps: - MRP subcontracting + purchase - Subcontracted product with BOM - Create PO with a qty > 1 for this product - Go into barcode - Scan > 0 and < all of the expected quantity - Back…
eproduction steps: - MRP subcontracting + purchase - Subcontracted product with BOM - Create PO with a qty > 1 for this product - Go into barcode - Scan > 0 and < all of the expected quantity - Back out of barcode - Click validate -> Error **OR** Go back into barcode, attempt to validate -> Error Added the StockMove and StockPicking classes to stock_barcode_mrp_subcontracting to extend functions `_subcontracted_produce`, `_clean_merged`, and `split_uncompleted_moves` to use a new context flag, `keep_subcontract_production`. When this flag is set, the move Barcode creates from the split keeps the MO of the move it was split from rather than splitting the MO in two, gives that link up before it is merged away so its cancellation does not cancel the MO, and the MO quantity is resynced with the merged move afterwards. Error thrown here: https://github.com/odoo/odoo/blob/f84eeb3ed1421e0cc07c906ff6444734dc01d35f/addons/mrp_subcontracting/models/stock_move.py#L248 opw-6428928 Forward-Port-Of: odoo/enterprise#131459
The scheduled payroll data update has been optimized to avoid memory errors during processing. This helps payroll operations complete smoothly and reduces the risk of interrupted background updates.
Original PR description
From memory error to smooth execution Forward-Port-Of: odoo/enterprise#129959
Resetting an expense report to draft now also clears any Studio approval already granted for posting journal entries. This prevents old approvals from being reused after changes, ensuring the approval process is repeated when needed.
Original PR description
Currently, when resetting to draft an expense sheet, approval steps added with studio are not reset, silently granting the change Steps to reproduce: - In Studio, add an approval rule on the "Post Journal Entries" button on expense sheet - Create an expense report, submit it and approve it - Click "Post Journal Entries" and approve approve it - Open the journal entry and reset it to draft - Back to the expense sheet, reset it to draft too Issue: The "Post Journal Entries" button is still approved, showing the previous approver with the same approval date. Analysis: Approval entries are dropped on state change by a base automation that Studio builds when the rule is created. However it is only done for sale order, account move and purchase order. opw-6530689 Forward-Port-Of: odoo/enterprise#130692
Invoiced point-of-sale orders are now correctly sent to preparation displays when payment is validated. This prevents kitchen or preparation teams from missing paid orders that include an invoice, helping avoid fulfillment delays.
Original PR description
Steps to reproduce ------------------ 1. Open a PoS that has a preparation display. 2. Create an order and go to the payment screen without sending it to the kitchen. 3. Enable Invoice, set a…
Steps to reproduce ------------------ 1. Open a PoS that has a preparation display. 2. Create an order and go to the payment screen without sending it to the kitchen. 3. Enable Invoice, set a customer and validate the payment. -> The order does not appear on the preparation display. Without the invoice it does appear. Why it's happening ------------------ `sync_from_ui` in `pos_enterprise` only creates the preparation lines when the order state is 'paid'. Validating an invoiced order pays it and creates the invoice in the same call, and `_generate_pos_order_invoice` sets the state to 'done', so when the state is checked it is not 'paid' anymore and the order is skipped. Until 19.3, these skipped orders were still sent by the sync coming with the `preparation` context, which was creating the lines without looking at the state. In 19.4, 8491f7a74363 changed this sync to only ask the displays to reload their orders, so the lines of the skipped orders are not created anymore. On master, invoicing does not set the state to 'done' since f1f5a0b8f8a3, so the order stays 'paid' and its lines are created. The fix ------- Also create the preparation lines when an order sent as 'paid' by the client ends up 'done', meaning it was paid and invoiced by the current call. opw-6513655
Belgian payroll now calculates guaranteed average pay more accurately when an employee's salary and work schedule change within the same month. This helps ensure payslips reflect the right balance of worked time and theoretical hours, reducing payroll discrepancies.
Original PR description
Handle salary and schedule changes occurring in the same month by splitting the month into salary periods, using the longest period as the anchor, valuing the shorter periods normally, and balancing the longest period with the remaining theoretical hours. Task Id: 6107129 Forward-Port-Of: odoo/enterprise#127104
Ecuadorian point-of-sale orders now keep the user's choice when the Invoice option is unchecked during payment. This prevents unwanted invoices from being created and sent to the tax authority after order validation or resynchronization.
Original PR description
Steps to reproduce: - Ecuadorian company, PoS with a preparation printer (or use the "Back" button on the feedback screen) - Open a session, add a product, go to the payment screen - Select a…
Steps to reproduce: - Ecuadorian company, PoS with a preparation printer (or use the "Back" button on the feedback screen) - Open a session, add a product, go to the payment screen - Select a customer, uncheck "Invoice", pay in cash and validate Issue: An invoice is created and sent to the SRI although "Invoice" was unchecked. Cause: l10n_ec_edi_pos patches PosOrder.setup() to force to_invoice = true. setup() runs not only on creation but every time the record is reloaded from the server, so the sync done at validation flips the flag back to true in the browser. Since the paid order can now be re-synced after validation (kitchen printer, "Back" on the feedback screen), the server processes it in process_saved_payments, writes to_invoice = True and generates the invoice. Fix: Only apply the Ecuadorian default when the loaded values carry no to_invoice, i.e. for a newly created order. Reloaded records keep the value the user chose. opw-6572987 Forward-Port-Of: odoo/enterprise#131612
Users working with branch companies can now find contacts owned by the parent company when setting a partner during bank reconciliation. This fixes a multi-company lookup issue and helps teams reconcile bank transactions without manual workarounds.
Original PR description
When setting a partner from the bank reconciliation control panel, the partner lookup domain only considered global contacts and contacts directly linked to the selected company IDs. This caused a multi-company issue for branch companies, where users could not find contacts owned by their parent company. The domain is now updated to include: Global contacts (company_id = false) Contacts whose company is a parent of the selected companies (company_id parent_of companyIds) Forward-Port-Of: odoo/enterprise#122935 Forward-Port-Of: odoo/enterprise#118719
This fix prevents Kenyan eTIMS invoice numbering from moving backwards after certain failed invoice submissions. It helps avoid duplicate invoice numbers and incorrect receipt details being copied between documents, improving reliability for compliance reporting.
Original PR description
Give an eTIMS invoice number back to the sequence only when the failing call is the one that took it, and only when it is still the last one handed out. When sending a customer invoice fails with anything other than a timeout, the number is given back so that it is not consumed for nothing. current issue: - send an invoice, let it time out, so it keeps number N - send other invoices, so the sequence moves past N - send the first one again and let it fail with a non-timeout error - the sequence drops by one and the next invoice sent reuses a number Both documents then sit under the same number. On its next attempt the one that was never accepted finds the other one's filing through selectInvoiceDetails and copies its receipt details. opw-6502563 Forward-Port-Of: odoo/enterprise#131790 Forward-Port-Of: odoo/enterprise#129994
Fixes several issues that could make accounting report snapshots use the wrong company context, miss branch companies, or delete snapshots unnecessarily. This improves the reliability of historical financial reporting, especially for multi-company setups and temporary lock date exceptions.
Original PR description
[FIX] account_reports: don't rely on self.env.company in _is_available_for This caused issues in snapshot generations, where the function is called without forcing the active company in the context.…
[FIX] account_reports: don't rely on self.env.company in _is_available_for This caused issues in snapshot generations, where the function is called without forcing the active company in the context. Relying on companies[0] should do the same in other contexts it is called in, and is more resilient. =============================================== [FIX] account_reports: snapshots: properly sandbox move lines per company when deducing the first date for snapshots =============================================== [FIX] account_reports: snapshots properly access subranches when checking snapshots to delete _accessible_branches only returns the companies that are in self.env.companies. If we want all the sub-branches (and the company itself), we should search with a child_of. This was found while debugging a test, because the accessible branches returned for a company where an empty recordset instead of the company itself, just because it wasn't in self.env.companies. =============================================== [FIX] account_reports: snapshots: don't delete snapshots uselessly when there is a temporary lock date exception Before this commit, when checking which snapshot needed to be deleted, we always removed the ones created after a temporary lock date exception, without considering the end time of this exception. Also, we were only deleting the snapshots with a higher or equal create_date, which was wrong: snapshot created before the exception should also be deleted. Forward-Port-Of: odoo/enterprise#132557