Saturday, August 29, 2026
7 changes · saas-19.4
Enhancements to existing features
French public-sector customer invoices are now identified and routed through Chorus Pro instead of the standard Peppol flow when the official directory indicates the customer uses the government platform. This helps businesses comply with French B2G e-invoicing requirements while adding the needed invoice statuses for Chorus Pro processing and testing.
Original PR description
B2G invoices are not sent via Peppol but to Chorus Pro (via the approved platform). Chorus Pro is the platform with ID 9999 on the annuaire. Any invoice to partner associated with that platform on…
B2G invoices are not sent via Peppol but to Chorus Pro (via the approved platform). Chorus Pro is the platform with ID 9999 on the annuaire. Any invoice to partner associated with that platform on the annuaire is considered B2G (business to government). From a user point of view nothing much changes, except that they have to set some additional fields for which we rely on the existing module `l10n_fr_facturx_chorus_pro`. From a technical PoV we store the information whether a partner is behind Chorus Pro in the `peppol_supported_documents` field. (By putting the special document identifier for Chorus Pro invoices there.) We retrieve the information whether a partner is behind Chorus Pro / B2G from the annuaire lookup. The following new lifecycle statuses have been added. They are required for the functional tests for the Chorus Pro connection. - Sent (sent by the platform) - Suspended - Completed (to "resume" the "Suspended" state) See the related IAP PR: https://github.com/odoo/iap-apps/pull/1804 task-6278159 Forward-Port-Of: odoo/odoo#284677
Self-billing bill numbering is now kept unique for each partner, improving traceability and reducing the risk of mixed document sequences. Self-billing invoices can also be routed to dedicated sales journals, preventing regular sales journals from using self-billing numbering patterns during import.
Original PR description
This PR handles 2 cases : ===== PART 1 ===== Self-billing bill sequences should be unique per partner, as implemented in v19+. This PR backports that behavior to 17.0. ===== PART 2 ===== Previously, the `is_self_billing` option on `account.journal` was available only for purchase journals. This caused an issue when importing a self-billing invoice into a regular sales journal with quick edit mode (accounting firm) enabled. In such cases, the newly created invoices would use the self-billing sequence pattern, leading to traceability issues. This PR allows the creation of self-billing sales journals to prevent this issue. task-6103142 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#282642 Forward-Port-Of: odoo/odoo#259935
Resolved issues and error corrections
Fixed an issue where generating electronic product codes could fail when tracked and untracked inventory move lines were processed together. This prevents incorrect code assignment or unexpected errors, improving reliability for barcode and inventory operations.
Original PR description
Problem: `_compute_electronic_product_code` built `tracking_number_list` by filtering out move lines without a lot_id/lot_name, but kept iterating over the full, unfiltered `move_line_ids`. As soon…
Problem: `_compute_electronic_product_code` built `tracking_number_list` by filtering out move lines without a lot_id/lot_name, but kept iterating over the full, unfiltered `move_line_ids`. As soon as a tracked product had an untracked move line mixed in with tracked ones (e.g. a manufacturing byproduct move line with no lot), the two lists fell out of sync: at best tracking numbers got assigned to the wrong move line, at worst `tracking_number_list[i]` went out of range and raised an IndexError. Solution: Exclude untracked move lines from `move_line_ids` before building `tracking_number_list`, so both stay the same length and index- aligned. Untracked lines get their own explicit "no tracking number" error instead of breaking the alignment for the rest. Steps to reproduce: Open runbot V19 -> go to moves history (Inventory) -> add `electronic_product_code` to list view using studio -> remove filter/select all records -> https://anotepad.com/notes/jwxyskc2 Forward-Port-Of: odoo/enterprise#128521 Forward-Port-Of: odoo/enterprise#123859
Helpdesk website contact forms now keep their translations when generated for a team. This ensures visitors see the form in the website or visitor language, regardless of the language used by the employee who created the helpdesk team.
Original PR description
When a helpdesk team has its website form enabled, a dedicated qweb view is generated from the `ticket_submit_form` template. The arch was read in the language of the user creating or editing the…
When a helpdesk team has its website form enabled, a dedicated qweb view is generated from the `ticket_submit_form` template. The arch was read in the language of the user creating or editing the team, so a team set up by an English user language produced an English form even when the website served another language. Steps to reproduce ================== 1. Set a language other than English as the website default language. 2. While your user language is English, create a helpdesk team with the website form enabled. 3. Open the team form on the website. => The form is rendered in English instead of the website language. Root cause ========== `_ensure_submit_form_view` read the template arch without forcing a language, so it used the current user's language and stored only that value on the generated per-team view. Fix === Read the template arch in the default language of the team's website, so the generated form matches the website language regardless of the user's own language. opw-6303903 Forward-Port-Of: odoo/enterprise#129265 Forward-Port-Of: odoo/enterprise#121164
Website editors without Accounting or Expense access can now save SEO cover images without being blocked by unrelated expense attachments. The fix narrows the attachment lookup correctly, avoiding unnecessary access checks and errors on large attachment databases.
Original PR description
### Issue: A user with Website Editor rights but no Accounting or Expense access gets an `AccessError` when using Optimize SEO to add a cover image, if another user has an expense with an attachment…
### Issue:
A user with Website Editor rights but no Accounting or Expense access gets an `AccessError` when using Optimize SEO to add a cover image, if another user has an expense with an attachment
### Steps to reproduce:
- Install `ai` and `hr_expense`
- With Admin, upload an image as an expense
- Update Demo user rights (Accounting: No, Expenses: No, Website: Editor and Designer)
- Log in as Demo
- Go to Website > Site > This page > Optimize SEO
- In Cover Image, add any image, select it and save
Before the fix, an `AccessError` is raised
### Cause:
When Save is triggered, `ai.attachment.vacuum.mark_attachments_used()` is called with the outer domain:
`[('attachment_id', 'in', [3868])]`
The `ai.attachment.vacuum` security rule uses:
`domain_force = [('attachment_id.res_access_write', '=', True)]` https://github.com/odoo/enterprise/blob/474ee25f82a3980530aae4c3450c69d925b6076e/ai/security/security.xml#L1-L24
During optimization, the dotted path is decomposed into an `any` condition:
`('attachment_id', 'any', [('res_access_write', '=', True)])` https://github.com/odoo/odoo/blob/dcb9ad42ba302a5ed0ec334daf4c8a4cb5ec0931/odoo/orm/domains.py#L999-L1004
`_optimize_any_domain_at_level` tries to narrow the comodel scan by reusing the outer constraint on `attachment_id` via `search_domain` in context
It finds `c` where `c.field_expr == 'attachment_id'`, but `c.value` is `{3868}` — a plain set of ids, not a `Domain`
Since the code only recognized `Domain` values, it treated this as unknown and fell back to `comodel_domain = Domain.TRUE`
`_search_res_access` for `ir.attachment` then scanned every attachment using:
`['&', ('res_model', '!=', False),
'|', ('res_id', '!=', False), ('create_uid', '!=', 5)]`
This matches a lot of attachments because the initial `attachment_id` constraint is missing
This scan included an attachment linked to an expense the current user cannot read
`_inaccessible_comodel_records` in `hr_expense` then checked all expenses linked to the found attachments and raised the `AccessError`
This fallback also triggers a second failure when the database contains more than 10 000 attachments: `_search_res_access` caps its unbounded scan at `MAX_SEARCH_LIMIT` and raises `ValueError("Cannot search, too many attachments")`
Confirmed by seeding ~10 500 attachments and reproducing the error
### Notes:
A new test in `test_any_domain_search_context.py` verifies in isolation that unrelated attachments are never scanned when a plain `in` condition narrows the search
opw-6467087
Forward-Port-Of: odoo/odoo#284678This fixes an issue where saving Point of Sale settings with employee login and UrbanPiper enabled could remove employees manually added to advanced rights. Businesses can now save delivery and employee access settings without losing authorized staff access.
Original PR description
Steps to reproduce ------------------ 1. Go to the Point of Sale settings and select a shop. 2. Enable UrbanPiper and set a delivery provider. 3. Enable "Log in with Employees". 4. Add two employees…
Steps to reproduce ------------------ 1. Go to the Point of Sale settings and select a shop. 2. Enable UrbanPiper and set a delivery provider. 3. Enable "Log in with Employees". 4. Add two employees to the "Advanced rights" field. 5. Save. Observation -> Only the employee of the PoS manager is left, the two employees are gone. Why it's happening ------------------ When saving from the settings, `pos.config` is written with the context key `from_settings_view`, which tells `_preprocess_x2many_vals_from_settings_view` that the commands received for an `x2many` field are the complete new list, so every record not in these commands is unlinked. The `write` of `pos_hr` always puts `advanced_employee_ids` in the values, even when the caller does not write this field, to keep the employees of the PoS managers in the list (1766d6d40a45). Since b48ddcce7bf in enterprise, UrbanPiper writes on the config a second time during the same save, still with `from_settings_view`. That write does not touch the employees, so `pos_hr` fills `advanced_employee_ids` with the managers only (cf 1766d6d40a45), and the ones that were just saved are unlinked. The fix ------- When `pos_hr` adds the field by itself, keep the employees already set on the config instead of starting from an empty list. opw-6500207 Forward-Port-Of: odoo/odoo#284961
Saving Live Chat preferences multiple times no longer removes tags that were already selected. This prevents agents from accidentally losing expertise or language tags when updating their profile, helping keep routing and profile information accurate.
Original PR description
Before this commit, saving the Live Chat section of "My Preferences" twice in a row dropped the tag(s) picked on the first save: adding a second Live Chat Expertise or Spoken Language tag and saving…
Before this commit, saving the Live Chat section of "My Preferences" twice in a row dropped the tag(s) picked on the first save: adding a second Live Chat Expertise or Spoken Language tag and saving again left only that new tag, silently discarding the one saved earlier.
Steps to reproduce (runbot v19.3):
1. Install the Live Chat app (im_livechat).
2. Go to your user preferences ("My Preferences").
3. Under the "Live Chat" section, add a tag to "Live Chat Expertise" or "Spoken Languages" and save.
4. Edit the profile again, add a second tag to the same field, and save.
5. Observe that the first tag disappears, leaving only the newly added tag.
More generally, this affects any non-stored, user-writeable many2many field (compute + inverse) that also depends on context — as livechat_expertise_ids/livechat_lang_ids do, via `depends_context('uid')`, since their value is per-user. This happened because `Many2many.write_real()` read the field's "old" value through `.sudo()` to diff it against the new one. For such a field, that sudo read lands in a different cache bucket than the one being written. That read also happened, incidentally, every time the field's own compute method assigned itself, at which point the record was protected against recomputation; hitting that protection through the unrelated sudo bucket cached an empty value there instead of recomputing it. The next real write then read that stale, empty bucket as the "old" value and applied the new tag on top of nothing, so the previous tag(s) were lost.
This commit fixes the by making `write_real()` now only reads through `.sudo()` when the field is actually `store`d, i.e. backed by a real relation table where bypassing access rights to see the full relation is meaningful. A non-stored field no longer creates that second cache bucket, so old and new values are always read from the same place the write goes to.
opw-6450877
Forward-Port-Of: odoo/odoo#285224