Friday, September 25, 2026
10 changes · saas-19.2
Resolved issues and error corrections
This fix ensures contacts cannot be merged when that would incorrectly link multiple portal users to the same contact, even when the users are hidden by the selected company context. It protects customer and portal user records from inconsistent links during multi-company contact management.
Original PR description
Switching companies can let a contact merge bypass the check that prevents multiple users from ending up linked to the same contact. ### Steps to reproduce 1. As a user with Contact Creation rights…
Switching companies can let a contact merge bypass the check that prevents multiple users from ending up linked to the same contact. ### Steps to reproduce 1. As a user with Contact Creation rights and access to companies A and B, select company A. 2. Create two contacts with no company set and grant each portal access. 3. Select only company B. 4. Merge the contacts. The merge succeeds and links both portal users to the surviving contact. With company A selected, the same merge is correctly rejected. ### Cause The wizard checks that the contacts have at most one linked user in total, including archived users. However, it reads `user_ids` with the acting user's permissions. Company record rules hide both portal users when only B is selected, so the check finds none. The subsequent SQL update still moves both users' contact links to the surviving contact. ### Fix Use `sudo()` only for this check, since it must count every user whose link the merge would move. Retain `active_test=False` to include archived users. Add a regression test covering both company selections. opw-6586945 Forward-Port-Of: odoo/odoo#290537 Forward-Port-Of: odoo/odoo#290296
All-day calendar events created from approved time off now keep their correct multi-day span when their start time is adjusted, including during Google Calendar sync. This prevents approved leave from appearing shortened or inaccurate in employee calendars.
Original PR description
**Problem:** When modifying the start time of an all-day Calendar Event created from the Validation of a Time Off Request, the end time will be shortened dramatically, no longer spanning the correct…
**Problem:** When modifying the start time of an all-day Calendar Event created from the Validation of a Time Off Request, the end time will be shortened dramatically, no longer spanning the correct timespan. In the case of the ticket referred to below, this occurs during Google Calendar sync. **Cause:** When a Calendar Event is created from a Time Off Request being Validated, its duration is set according to the amount of days the Time Off Request spans multiplied by the employee's daily work hours as defined on their contract, even if the Calendar Event will be an all-day event. When the start time of a Calendar Event is modified, the end time is recomputed according to the duration added to the new start time. As such, if the duration of the Event does not match the duration of the dates covered, it will be shortened dramatically. **Purpose:** Modify the values defined when creating a Calendar Event as a result of a Validated Time Off Request so that, if the created Event will be an all day Event, the duration is computed according to `calendar.event._get_duration`. **Steps to Reproduce in Runbot:** 1. Create and Validate a Time Off Request of the Paid Time Off leave type spanning multiple days. 2. Modify the start time of the Calendar Event that is created as a result of the validation. (This step will likely require a nonstandard flow as the start time field is normally hidden for all-day Calendar Events) opw-6426586 Forward-Port-Of: odoo/odoo#289512 Forward-Port-Of: odoo/odoo#284552
This fixes a search issue where bills of materials linked to archived products could be missed, even when users explicitly searched archived records. Users can now reliably find archived BOMs by product name, improving accuracy in manufacturing data lookups.
Original PR description
When searching for an archived bom (filer archived activated), if the main product is also archived, the search on the produt will not find the bom. Steps to reproduce: ------------------- * Create…
When searching for an archived bom (filer archived activated), if the main product is also archived, the search on the produt will not find the bom.
Steps to reproduce:
-------------------
* Create product AA
* Create a bom for product AA
* Archive product AA
* Products > bom
* Search for "Archived" and Product: "AA" -> It will not find the bom
Observation:
-------------
When doing the search, it will start a web_search_read ['&', ('active', '=', False), ('product_tmpl_id', 'ilike', 'AA')].
While in _search it will decide if we filter active elements: https://github.com/odoo/odoo/blob/60b5b99d569dbe183ef9c980217ccfe9bf243756/odoo/orm/models.py#L5363-L5369 In the first level since "active = False" is in the domain (for the bom) it will not add the filter.
The _search will optimize the domain query level by level by calling optimize_full.
First the outher layer :
optimize_full (Domain '[]')> _optimize (DomainNary '&')> _optimize_step (DomainNary '&')
Then the children [1: (active bom), 2: (product template display name)]:
_optimize>optimize_step> ...
While optimizing the second child the domain condition is: [('product_tmpl_id', 'any', [('display_name', 'ilike', 'AA')])]
Since it's a any operation, in optimize_step, it will use the _optimize_any_domain_at_level:
https://github.com/odoo/odoo/blob/16aaaafd7d314c04f39b1f633af3b5e15b46d78b/odoo/orm/domains.py#L970-L972
After doing some checks it will continue to optimize per level with the related comodel:
https://github.com/odoo/odoo/blob/60b5b99d569dbe183ef9c980217ccfe9bf243756/odoo/orm/domains.py#L1389-L1393
Since it's an "any" operator it need to respect the following guidelines : https://github.com/odoo/odoo/blob/60b5b99d569dbe183ef9c980217ccfe9bf243756/odoo/orm/domains.py#L98-L101
this is no done in `_optimize_any_domain_at_level` since it resolves a relational field's 'any' domain by looking up a fresh comodel recordset without adjusting its context.
Which means that for the following level, on the product template, the active_name is not on the domain anymore and the context was not passed corretly, so _search will automaticly add the filter ("active = True").
opw-6514762
Forward-Port-Of: odoo/odoo#285942Employee Time Off approvers can now see the Time Off shortcut for the employees they manage, even if they do not have broader Time Off permissions. This ensures managers can review and manage requests they are responsible for without needing unnecessary extra access rights.
Original PR description
Issue: A user without any rights on Time Off should still be able to manage the requests of the users he's manager of. However, the computation for `show_leaves` only considers Time Off Officers/Admins and the employee themselves, while it should also consider the employee's Time Off approver. Fix: Include the employee's Time Off approver when computing `show_leaves`, allowing them to access the employee's Time Off smart button. Reproduction Steps: 1. Set a user (e.g. Marc Demo) to "No" for Time Off. Note: If reproducing in v19, Administrator rights on Employees are also needed to access the private employee form view where the issue occurs. 2. Set that user as another employee's Time Off approver. 3. Log in as that user and open the employee's form view. 4. Observe that the Time Off smart button is not visible. Related Tickets: opw-6445714 Forward-Port-Of: odoo/odoo#290047 Forward-Port-Of: odoo/odoo#281589
This fix makes an automated email template check wait until the selected contact model is fully ready before continuing. It prevents occasional false failures in testing caused by slow server responses, helping keep releases smoother without changing end-user behavior.
Original PR description
Before this commit, mail_template_dynamic_placeholder_tour could fail when the server answers the onchange of "Applies to" slowly: Tour mail_template_dynamic_placeholder_tour failed at step Check if…
Before this commit, mail_template_dynamic_placeholder_tour could fail when the server answers the onchange of "Applies to" slowly:
Tour mail_template_dynamic_placeholder_tour failed at step Check if
the dynamic placeholder popover is opened
(trigger: div.o_model_field_selector_popover)
This happens because the tour waits a fixed 200ms after picking "Contact" before typing "#" in the subject. The popover needs the model, and the record only holds the model once the onchange answers. On runbot the answer came after 230ms, so "#" showed the "select a model" notification instead of the popover.
This commit fixes the issue by waiting for the internal link button of the many2one, which only renders once the record holds the model.
Note that this step relies on "[FIX] web: cancel the pending search on autocomplete select": without it, a search still pending on the picked value marks the input as edited, which hides that button.
https://runbot.odoo.com/odoo/error/947209
Ref commit: https://github.com/odoo/odoo/pull/287888
Forward-Port-Of: odoo/odoo#290364
Forward-Port-Of: odoo/odoo#290185A test for onsite employee registration now opens the correct starting menu in the community edition. This prevents false test failures and helps keep the HR skills event workflow validation stable across editions.
Original PR description
**Trigger:** Running `test_onsite_employee_registration` in a community database starts in Discuss instead of enterprise's usual home menu which crashes the test on the first step. This commit adds the `showAppsMenuItems` util to the test. To ensure the test starts in the correct view. runbot-946289 Forward-Port-Of: odoo/odoo#289486
Point of Sale now avoids reusing a receipt number that is still attached to an unpaid or unsynced order. This prevents two paid orders from sharing the same receipt or invoice reference, reducing confusion and issues with fiscal integrations.
Original PR description
Steps to reproduce: - Add a product to a draft order, do not pay - Open the same PoS in a second tab of the same browser (the first tab is sent to the backend), or reload while a draft is being…
Steps to reproduce: - Add a product to a draft order, do not pay - Open the same PoS in a second tab of the same browser (the first tab is sent to the backend), or reload while a draft is being removed - Pay the restored draft, then make and pay a new order Issue: Both orders carry the same pos_reference (e.g. 264-3-000701). The number is printed on both receipts and sent to fiscal integrations that use it as the unique invoice number. Cause: The receipt number is issued client side by DeviceIdentifierSequence, a per-device counter kept in localStorage that all tabs of a browser share. When a draft is removed, its number is pushed on `unsynced_number_stack` to be reused by the next order. `removeOrder` pushes the number synchronously, then deletes the order from IndexedDB without waiting for the transaction. If the page unloads before it commits (the tab is closed by another tab opening the same PoS, a redirect to the backend, a reload), the next load restores the draft from IndexedDB with its number while the stack hands that same number to the next new order. Both are then created on the server with different uuids and the same pos_reference: the server only deduplicates by uuid. Fix: Before a new order takes a number, drop from the reuse stack the numbers still used by the unsynced orders this device holds: a draft restored from IndexedDB, or one whose copy another tab removed, still owns its number. The push stays synchronous, so a cancelled order is still replaced by one reusing its number. opw-6558992 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#287667
Ecuadorian electronic invoicing now avoids a crash when a company has a VAT number that is not a valid Ecuadorian RUC. This helps users confirm invoices reliably by skipping authorization number generation when the required Ecuadorian identification type is not present.
Original PR description
When the VAT of an Ecuadorian company's partner contains characters other than decimal digits, confirming an invoice raises a traceback. Steps to reproduce the error: - Install ``l10n_ec_edi`` module…
When the VAT of an Ecuadorian company's partner contains characters other than decimal digits, confirming an invoice raises a traceback. Steps to reproduce the error: - Install ``l10n_ec_edi`` module and switch to EC Company - Open the partner of EC Company > Set a VAT containing non-digit characters - Create a new invoice > fill the required fields > Confirm Traceback: ```py ValueError: invalid literal for int() with base 10: 'E' ``` During invoice confirmation, ``_l10n_ec_set_authorization_number()`` method is called to generate the authorization number at [1], It builds the ``key_value`` using the vat of company's partner at [2]. The generated ``key_value`` is then passed to ``_l10n_ec_get_check_digit`` method, which converts each character of ``key_value`` to an integer using ``int()`` at below line. https://github.com/odoo/enterprise/blob/799e9e94efcc338a8f3a6b7ab85cf77f9898f005/l10n_ec_edi/models/account_move.py#L830 When the VAT contains a character that cannot be converted to an integer, It will raises the above traceback. To build the ``key_value``, the company's partner vat should be RUC consisting of 13 digits. Ref: https://www.sri.gob.ec/o/sri-portlet-biblioteca-alfresco-internet/descargar/fb95cafc-a8ca-4a4c-afb6-12c4153165f0/FICHA%2520TECNICA%2520COMPROBANTES%2520ELECTR%25C3%2593NICOS%2520ESQUEMA%2520OFFLINE.pdf Solution: If the identification type of the company's partner is not RUC, return an empty string. [1]: https://github.com/odoo/enterprise/blob/799e9e94efcc338a8f3a6b7ab85cf77f9898f005/l10n_ec_edi/models/account_move.py#L806 [2]: https://github.com/odoo/enterprise/blob/799e9e94efcc338a8f3a6b7ab85cf77f9898f005/l10n_ec_edi/models/account_move.py#L825-L826 sentry-7642961403 Related Community PR: https://github.com/odoo/odoo/pull/280047 Forward-Port-Of: odoo/enterprise#132478 Forward-Port-Of: odoo/enterprise#126519
German quarterly tax report XML exports now include the correct period value. This helps companies submit complete tax files and avoids missing reporting information caused by the quarterly setting being misread.
Original PR description
The `Zeitraum` tag was not set when the tax return periodicity is `quarterly`. The `account_tax_periodicity` field defines `trimester` as the key for the `quarterly` label. In [account_generic_tax_report.py] though, the code compared the periodicity with the `quarterly` label instead of the `key` trimester. **Steps to reproduce**: - Install the `l10n_de_reports` module and switch to the DE Company. - Go to Accounting settings and set `Tax Return Periodicity` to `quarterly`. - Navigate to Accounting > Reporting > Tax Return. - Export the tax report as `XML`. - Open the generated XML file and observe the `Zeitraum` tag. [account_generic_tax_report.py]: https://github.com/odoo/enterprise/blob/338630decbec76580766dd3b38c3241ecfb4c77a/l10n_de_reports/models/account_generic_tax_report.py#L54 Ticket [link](https://www.odoo.com/odoo/project.task/6581536) opw-6581536 Forward-Port-Of: odoo/enterprise#132824 Forward-Port-Of: odoo/enterprise#132593
Scheduling a shift in Planning now correctly respects the user's active company access when checking for overlaps. This prevents crashes and avoids showing or using shift information from companies the user cannot currently access.
Original PR description
Steps to Reproduce --- 1. Log in as a user with access to multiple companies (e.g., Company A and Company B). 2. Ensure both companies are ticked in the top-right multi-company widget. 3. In the…
Steps to Reproduce --- 1. Log in as a user with access to multiple companies (e.g., Company A and Company B). 2. Ensure both companies are ticked in the top-right multi-company widget. 3. In the Planning app, create a shift for an employee from 09:00 to 17:00 in Company B. 4. Remove the access of Company B 5. In the Planning Gantt view, click the same empty cell to schedule the same employee at the same time for Company A. Issue --- The _compute_overlap_slot_count method uses raw SQL to find overlapping shifts. However, this raw SQL fetches overlapping slot IDs from shifts belonging to companies the user has currently deselected or does not have access to. Current behaviour --- The raw SQL populates the conflicting_slot_ids field with inaccessible record IDs (the shift from the deselected Company B). This triggers accessError. Expected behaviour --- The system should only calculate overlapping shifts for companies the user currently has active in their environment. It should not leak the existence of shifts in deselected/unauthorized companies, and it should open the new shift dialog without crashing. Fix --- Add company_id to both raw SQL queries (for existing records and new virtual records) inside _compute_overlap_slot_count. Pass company id to ensure the database only returns conflict IDs that are valid within the user's active multi-company context. task - 4554813 Forward-Port-Of: odoo/enterprise#132745 Forward-Port-Of: odoo/enterprise#109027