Sunday, August 30, 2026
10 changes · saas-19.3
Resolved issues and error corrections
This fix prevents quotation emails from failing for customers with large attachment databases. Odoo now limits attachment access checks to the documents relevant to the sale, avoiding broad searches that could hit system limits and block quote sending.
Original PR description
## Context Recently, we've had larger customers upgrading to 19.3. These customers began noticing that they were unable to send any quotations from their sale orders. It turned out that they were all…
## Context
Recently, we've had larger customers upgrading to 19.3. These customers began noticing that they were unable to send any quotations from their sale orders. It turned out that they were all experiencing the same issue that could be traced back to some of our ORM optimization code.
The problem had to do with an unbounded search on the `ir.attachment` model. This would happen when trying to load the relevant product documents to attach in a confirmation email, based on the products being sold. Where we should have been checking the user's "read" access on only the attachments being added to the email, we were checking "read" access for every single attachment in the database. This was hitting our search limit and preventing them from sending out quotes.
At the time of writing, `MAX_SEARCH_LIMIT` evaluates to `10,000`.
https://github.com/odoo/odoo/blob/f5ace41946dce669f72d1edeef3daebfdb4a4519/odoo/addons/base/models/ir_attachment.py#L660-L663
## Before this commit
When a search on a model reaches a related field through the `any` operator, and the model's own `search_domain` doesn't mention that field directly, the optimizer fell back to accepting all comodel records (`Domain.TRUE`). The comodel's own search then had no restriction to work with and scanned every row:
https://github.com/odoo/odoo/blob/f5ace41946dce669f72d1edeef3daebfdb4a4519/odoo/orm/domains.py#L1465-L1467
On `ir.attachment`, we'd hit that limit for larger customers easily, and then raise,
ValueError: Cannot search, too many attachments
## After this commit
Instead of discarding the outer `search_domain`, we build a lazy subquery of the records it actually reaches and pass that to the comodel as `context['search_domain']`. The comodel-side search can opt in to use it to narrow itself down; if it doesn't, nothing changes. The subquery is only ever executed if something reads it, so there's no cost when it isn't used. For `ir.attachment`, this restores the scoping and the search stays bounded.
The `domains.py` fix assumes that `context['search_domain']` describes the current model. But a few `_search_res_access`-style methods switch to a different comodel via `env[res_model_name]` without clearing that
key, so it leaks over from the original model. That was harmless with the old `Domain.TRUE` fallback, but the new lazy subquery actually builds SQL from it, so it crashes when the leaked domain references a field the comodel doesn't have.
Also, it is necessary that we include `bypass_access=True` on our subquery's `_search()` to avoid infinite recursion through our security domain optimization code. It's safe here because we're building it as a helper query based on records already deemed accessible to the user. So, both the outer model's and the comodel's security domains are
still going to be applied on their respective final queries.
## TODO in master
The real fix for the leak is to have `context['search_domain']` carry the model it was computed for, instead of a bare `Domain`, so a mismatch can be detected rather than acted on. That touches `models.py`'s `_search`, `ir_rule.py`'s domain computation, and every `_search_res_access`-style method in `base`/`mail` that switches to a different comodel via `env[res_model_name]` without clearing the key. That's a bigger change than we want in this fix, so I've left it for a follow-up on master.
opw-6486492This fix ensures Odoo checks the most up-to-date internal data before looking up XML identifiers. It prevents outdated settings, such as update protection flags, from being returned after changes are made.
Original PR description
Steps to Reproduce: - write on ir.model.data to modify noupdate. - _lookup_xmlids still returns the old noupdate value. Example: In [1]: imd = self.env['ir.model.data'] In [2]: xml_id =…
Steps to Reproduce:
- write on ir.model.data to modify noupdate.
- _lookup_xmlids still returns the old noupdate value.
Example:
In [1]: imd = self.env['ir.model.data']
In [2]: xml_id = self.env['ir.model.data'].search([], limit=1)
In [3]: imd._lookup_xmlids([xml_id.complete_name], self.env[xml_id.model])
Out[3]: [(18603, 'auth_signup', 'action_send_password_reset_instructions', 'ir.actions.server', 149, False, 149)]
In [4]: xml_id.write({'noupdate': not xml_id.noupdate})
Out[4]: True
In [5]: imd._lookup_xmlids([xml_id.complete_name], self.env[xml_id.model])
Out[5]: [(18603, 'auth_signup', 'action_send_password_reset_instructions', 'ir.actions.server', 149, False, 149)]
In [6]: imd.flush_model()
In [7]: imd._lookup_xmlids([xml_id.complete_name], self.env[xml_id.model])
Out[7]: [(18603, 'auth_signup', 'action_send_password_reset_instructions', 'ir.actions.server', 149, True, 149)]
Issue:
- _lookup_xmlids is returning values from ir.model.data executing an SQL query w/o flushing.
Fix:
- Add flushing in _lookup_xmlids.
Forward-Port-Of: odoo/odoo#262791Refreshing a social media feed no longer shows a generic error when the connected X/Twitter account token is invalid. Instead, the account can be disconnected as expected, reducing confusion for users managing social feeds.
Original PR description
Bug === If the token because invalid, then an UserError is raised instead of disconnecting the account. This is because we "blind raise" all errors we get from X, instead of filtering the error linked to the stream configuration. Task-6499283 Forward-Port-Of: odoo/enterprise#129560 Forward-Port-Of: odoo/enterprise#129110
This fix prevents database upgrades from changing an existing India cash rounding setup when it may already be used by an open point-of-sale session. It helps avoid upgrade interruptions for businesses using Indian localization and POS cash rounding.
Original PR description
An issue occurs during the database upgrade From version 19.2 to 19.3. During the upgrade, the system attempts to update the `name` and `rounding` configuration of the Cash Rounding account. If the…
An issue occurs during the database upgrade
From version 19.2 to 19.3.
During the upgrade, the system attempts to update
the `name` and `rounding` configuration of the Cash Rounding account.
If the Cash Rounding record is linked to
an open POS session, the update fails with
the following constraint error:
```
You are not allowed to change the rounding configuration while a POS session using it is already opened.
```
To avoid this traceback during the upgrade,
set the record's noupdate attribute to True so that it is not modified.
```
File "/home/odoo/src/odoo/saas-19.3/odoo/tools/convert.py", line 485, in _tag_record
record = model._load_records([data], self.mode == 'update')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/saas-19.3/odoo/orm/models.py", line 4566, in _load_records
data['record']._load_records_write(data['values'])
File "/tmp/tmpxr_9kp1h/migrations/base/0.0.0/pre-models-load_write.py", line 46, in _load_records_write
return super()._load_records_write(values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/saas-19.3/odoo/orm/models.py", line 4487, in _load_records_write
self.write(values)
File "/home/odoo/src/enterprise/saas-19.3/ai_fields/models/models.py", line 57, in write
res = super().write(vals)
^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/enterprise/saas-19.3/ai/models/models.py", line 339, in write
return super().write(vals)
^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/saas-19.3/odoo/orm/models.py", line 3860, in write
real_recs._validate_fields(vals, inverse_fields)
File "/home/odoo/src/odoo/saas-19.3/odoo/orm/models.py", line 1295, in _validate_fields
check(records)
File "/home/odoo/src/odoo/saas-19.3/addons/point_of_sale/models/pos_order.py", line 1575, in _check_session_state
raise ValidationError(
odoo.exceptions.ValidationError: You are not allowed to change the rounding configuration while a pos session using it is already opened.
```
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
Forward-Port-Of: odoo/odoo#281174Self-order and online payment notifications now avoid sending full order details through real-time websocket messages. This reduces unnecessary data sharing while preserving the notification flow for point-of-sale self-ordering.
Original PR description
Remove the order data from the websocket notification. Forward-Port-Of: odoo/odoo#285110 Forward-Port-Of: odoo/odoo#284631
This update brings the spreadsheet component to a newer version with fixes for editing sessions, keyboard focus visibility, and radar chart point colors. Users should see more reliable spreadsheet behavior and clearer chart presentation with minimal disruption.
Original PR description
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/1f4b1aea1b [REL] 19.3.18 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0)…
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/1f4b1aea1b [REL] 19.3.18 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/e130b07595 [FIX] radio_selection: add focus style [Task: 6384097](https://www.odoo.com/odoo/2328/tasks/6384097) https://github.com/odoo/o-spreadsheet/commit/a88b83ea25 [FIX] session: fix part of the command squisher [Task: 6462247](https://www.odoo.com/odoo/2328/tasks/6462247) https://github.com/odoo/o-spreadsheet/commit/ecb21709f7 [REV] Session: deactivate command squisher [](https://www.odoo.com/odoo/2328/tasks/) https://github.com/odoo/o-spreadsheet/commit/3f6b03df8e [FIX] chart: set point color for radar charts [Task: 6474893](https://www.odoo.com/odoo/2328/tasks/6474893) Co-authored-by: Florian Damhaut (flda) <flda@odoo.com> Co-authored-by: Anthony Hendrickx (anhe) <anhe@odoo.com> Co-authored-by: Alexis Lacroix (laa) <laa@odoo.com> Co-authored-by: Lucas Lefèvre (lul) <lul@odoo.com> Co-authored-by: Adrien Minne (adrm) <adrm@odoo.com> Co-authored-by: Ronak Mukeshbhai Bharadiya (rmbh) <rmbh@odoo.com> Co-authored-by: Dhrutik Patel (dhrp) <dhrp@odoo.com> Co-authored-by: Rémi Rahir (rar) <rar@odoo.com> Co-authored-by: Pierre Rousseau (pro) <pro@odoo.com> Co-authored-by: Vincent Schippefilt (vsc) <vsc@odoo.com> Co-authored-by: Marceline Thomas (matho) <matho@odoo.com>
This fixes two Spanish (Latin America) labels for customer identification types in Odoo 19. Users will now see the correct local terms, showing “NIF” instead of “IVA” for VAT and “Pasaporte” instead of the English “Passport.”
Original PR description
In Odoo 19, translations for l10n.latam.identification.type were moved from .po files to inline columns in the CSV data file. The es_419 correction previously applied via es_419.po (VAT → NIF) was…
In Odoo 19, translations for l10n.latam.identification.type were moved from .po files to inline columns in the CSV data file. The es_419 correction previously applied via es_419.po (VAT → NIF) was never ported to the new CSV mechanism, causing "IVA" to reappear. Passport was also missing its es_419 translation.
**Description of the issue/feature this PR addresses:**
In Odoo 17 and 18, the Spanish (es_419) translation of the VAT identification type was corrected from "IVA" to "NIF" (Número de Identificación Fiscal) via the es_419.po file in l10n_latam_base. In Odoo 19, the translation mechanism for this data changed: translations are now stored as name@es_419 columns directly in l10n_latam.identification.type.csv. The correction was never applied to this new file, so the wrong value ("IVA") reappeared. Additionally, the Passport type was missing its es_419 translation entirely.
**Current behavior before PR:**
- In any Odoo 19 instance with a Latin American localization, the identification type dropdown shows "IVA" for the VAT type (es_419 locale).
- The Passport type shows the untranslated English label "Passport" in es_419 locales.
**Desired behavior after PR is merged:**
- The VAT identification type displays "NIF" in es_419 locales.
- The Passport identification type displays "Pasaporte" in es_419 locales.
- Behavior in v17 and v18 is unaffected (different translation mechanism).
**Steps to test the functionality:**
1. On an Odoo 19 instance with l10n_latam_base installed and language es_419 active, go to Contacts → Configuration → Identification Types.
2. Verify that the type with external ID l10n_latam_base.it_vat shows "NIF" (not "IVA").
3. Verify that the type with external ID l10n_latam_base.it_pass shows "Pasaporte" (not "Passport").
4. Open a contact form, set country to Argentina, and confirm the identification type dropdown reflects the corrected labels.
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#272243Point of Sale loyalty rewards for “Buy 2 Take 1” promotions now correctly add the free item when a cashier enters the quantity directly, scans a quantity barcode, or adds products one by one. This prevents missed discounts for promotions based on product tags and helps ensure customers receive the expected offer at checkout.
Original PR description
Steps to reproduce: - create a "Buy 2 Take 1" program whose rule and reward both target a product tag containing several products - in the PoS, add one of the tagged products to the order - set its…
Steps to reproduce: - create a "Buy 2 Take 1" program whose rule and reward both target a product tag containing several products - in the PoS, add one of the tagged products to the order - set its quantity to 3 with the numpad Issue: The free product is not given. Clicking the product a third time instead of typing the quantity does give it, and so does a program whose reward is a single product. Cause: A reward whose products come from a tag is multi_product, so getClaimableRewards never computes its unclaimed quantity and the auto claim of updateRewards skips it: which product to give out is unknown. The only place claiming such a reward is addLineToCurrentOrder, which resolves it with the product that was just added. Setting a quantity does not go through it, hence the difference. Fix: Resolve the reward product the same way when auto claiming: the product of the line being worked on, or, failing that, the only reward product the order contains. The reward then follows the quantity of any of the tagged products, not only of the first one added, and the cashier is still asked when the order does not settle the choice. opw-6430385 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#284777
This fix prevents Odoo from mistakenly recreating an entire Outlook recurring event series when only the first event's start time changes. It keeps updates such as added attendees limited to the intended occurrence, avoiding incorrect calendar changes across the whole series.
Original PR description
The stored rrule of a recurrence embeds a DTSTART line based on the recurrence dtstart (the smallest start among its events). When the first occurrence is moved in Outlook, the Odoo dtstart no longer…
The stored rrule of a recurrence embeds a DTSTART line based on the recurrence dtstart (the smallest start among its events). When the first occurrence is moved in Outlook, the Odoo dtstart no longer matches that DTSTART, but the stored rrule is not recomputed at that point (it only depends on the pattern fields). On a later sync touching the seriesMaster (e.g. after adding an attendee to the moved occurrence), the recurrence values are written again and the rrule is reserialized with a DTSTART based on the moved occurrence. _write_from_microsoft() took this new rrule string as a pattern change and reapplied the recurrence: all occurrences were deleted and recreated as copies of the moved one, spreading its specific data (e.g. the newly added attendee) to the whole series. Ignore the DTSTART line when comparing the rrule before and after the write: a DTSTART-only difference does not reflect any pattern change in Outlook. An actual pattern change (FREQ, UNTIL, INTERVAL, ...) still reapplies the recurrence as before. Steps to reproduce: 1. In Outlook, create a recurring event 2. In Odoo, run the calendar sync 3. In Outlook, move the first occurrence of the recurrence (shift its start and stop time by 30 minutes) 4. In Odoo, run the sync 5. In Outlook, add an attendee to that same first occurrence 6. In Odoo, run the sync All the occurrences are deleted and recreated as copies of the first one, and the attendee ends up on every occurrence instead of one. opw-5129848 Forward-Port-Of: odoo/odoo#284747 Forward-Port-Of: odoo/odoo#269549
This fixes an issue where an Outlook calendar event could appear twice in Odoo after a user changed a single synced event into a recurring series in Outlook. Odoo now removes the old single event when creating the new recurrence, keeping calendars accurate and avoiding confusion.
Original PR description
When a single event already synced with Outlook is turned into a recurring event directly in Outlook, Microsoft reuses the event in place: the seriesMaster keeps the iCalUId of the former single event, while each occurrence gets a fresh id/iCalUId. On resync, a seriesMaster is only matched against calendar.recurrence, never against calendar.event. As no recurrence exists yet, it is handled as a brand new recurrence whose occurrences are created from scratch. The original single event, which shares the master's iCalUId and the first occurrence's timeslot, is then left untouched. Drop that pre-existing single event when building a new recurrence from an inbound seriesMaster, since it is now represented by the recurrence itself. opw-5129848 Forward-Port-Of: odoo/odoo#285196 Forward-Port-Of: odoo/odoo#271787