Friday, August 28, 2026
31 changes · saas-19.3
Enhancements to existing features
Accounting records now use a better database lookup path when finding the latest sequence entry for a journal and date. This can dramatically reduce waiting time in affected accounting screens, with one measured case improving from about 70 seconds to under a second.
Original PR description
This commit adds an index on `(journal_id, date)` to speed up `_get_last_sequence_domain`. The slow part is `.search(domain, order='date [desc|asc]', limit=1)` Because of the order by date,…
This commit adds an index on `(journal_id, date)` to speed up `_get_last_sequence_domain`. The slow part is `.search(domain, order='date [desc|asc]', limit=1)` Because of the order by date, postgresql scans the index on `date`, assuming a row will be quickly matched. If this assumption is wrong though, it'll scan a large part of the index or all of it, which is slow. This is the case with account move 17102258 on odoo.com at the time of writing this. - before - 1st query https://explain.dalibo.com/plan/6b8ff62b1572ah1a - 2nd query https://explain.dalibo.com/plan/41243cgccg5798gb - after - 1st query https://explain.dalibo.com/plan/4fd2c7ed28g7b138 - 2nd query https://explain.dalibo.com/plan/2heh6c6965c88h71 - ~~1st query https://explain.dalibo.com/plan/f874a31f4b07hdeb~~ - ~~2nd query https://explain.dalibo.com/plan/e8h09725gfh4c2e9~~ full `web_read` - before ~1min 10s - after ~650ms task-6481393 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#283159
Companies in the same database that share a SIREN can now reuse a successful identity verification from another company with that same SIREN. This reduces repeated manual KYC work for organizations managing many branches or entities under the same identifier.
Original PR description
We have some clients that have several hundreds of companies/branches on the same db, with the same SIREN (incubateur or the like). They will need to do the kyc (that will be identical, as it's the same SIREN) for all the companies. It's especially cumbersome if it needs manual intervention So, if one company on that database, with the same SIREN, managed to register, then it means it has succeded the kyc. Meaning we can bypass the kyc for the other identifiers as well. task-6515315 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#285236
Self-billing bills now keep separate numbering per partner, improving traceability and reducing confusion in accounting records. The change also allows dedicated self-billing sales journals so imported self-billing invoices no longer affect regular sales journal sequences.
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#282062 Forward-Port-Of: odoo/odoo#259935
Resolved issues and error corrections
Sending or replying to regular messages on a tax return no longer incorrectly changes its status to Paid. Payment finalization now only occurs from the intended tax payment instructions flow, preventing accidental status updates and reducing accounting confusion.
Original PR description
Before this fix: Replying to or sending a message from the chatter of a tax return could incorrectly change its state to Paid. This happened because action_send_mail() automatically called _action_finalize_payment() for account.return records. After this fix: Payment finalization only happens when the composer is opened from the tax payment instructions flow. Normal chatter messages and replies will no longer change the tax return state to Paid. task-6469536 Forward-Port-Of: odoo/enterprise#129161
Updating Live Chat expertise or spoken language tags in user preferences now keeps the tags that were already selected. This prevents users from accidentally losing existing profile information when adding new tags.
Original PR description
Problem: When updating live chat expertise or language tags from the user preferences, saving the changes removes all previously selected tags and keeps only the newly added ones on versions 19.3 and…
Problem: When updating live chat expertise or language tags from the user preferences, saving the changes removes all previously selected tags and keeps only the newly added ones on versions 19.3 and above. When a user adds a new tag to their livechat_expertise_ids or the livechat_lang_ids field, the system incorrectly overwrites the existing tags with the newly selected ones, which results in complete replacement of the field's values rather than appending the new tag to the existing list. Solution: This commit ensures that newly added expertise and language tags are correctly appended to the user's profile, preserving any previously selected tags upon saving. 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 tags. opw-6450877
This fixes an issue where event registrations were not recreated when a cancelled sales order was reset to quotation and confirmed again. Businesses can now rely on event bookings being created correctly even when orders are cancelled and restarted or confirmed through the customer portal.
Original PR description
**Steps to reproduce:** - Create a SO and add the Event Registration - Standard product and specify an event - Confirm the SO then select "Create/Update registrations" - Cancel the SO, then "Set to…
**Steps to reproduce:** - Create a SO and add the Event Registration - Standard product and specify an event - Confirm the SO then select "Create/Update registrations" - Cancel the SO, then "Set to Quotation" - Select "Preview" and confirm the Sale Order again - There will not be any new registration created when there should be one. **Behavior:** Usually when a sale order is confirmed the `action_sale_order_event_registration` form will be opened which when filled correctly creates registrations. However in certain cases: confirming from the customer portal, or simply closing the form when it is opened, will not trigger `action_make_registration` which creates registrations if it is not already the case `action_confirm()` should be creating the registrations correctly on its own anyway by calling ´init_registrations()´ : https://github.com/odoo/odoo/blob/beed378cde592bc96c1e79a976ac775264b843ed/addons/event_sale/models/sale_order_line.py#L49-L66 This function tries to create each missing registrations by looking at the amount in the so_line and deducting the already created registrations, however since some of them can be cancelled, this computation is wrong. And leads to registration not being created when they should. opw-6444127 Forward-Port-Of: odoo/odoo#280426
Saving Live Chat preferences now keeps previously selected expertise and language tags instead of replacing them with only the newest tag. This prevents users from silently losing their profile settings when editing preferences multiple times.
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-6450877PDF quotes created with Quote Builder could fail when templates included dynamic fields. This fix preserves required PDF form settings so affected quotes can be generated reliably.
Original PR description
Issue: --- Due to this issue, generating PDF Quote using Quote Builder with dynamic fields leads to a traceback. This was partially fixed by: e16edc7b0d9f56cb7769068ecf7d64ff8b0f6359 Steps: --- 1-…
Issue: --- Due to this issue, generating PDF Quote using Quote Builder with dynamic fields leads to a traceback. This was partially fixed by: e16edc7b0d9f56cb7769068ecf7d64ff8b0f6359 Steps: --- 1- Using a python 3.13 env, install requirements.txt. (You could instead uninstall pypdf2 and install pypdf==5.4.0) 2- Enable Quote Builder. 3- Create a SO and in quote builder tab, select a document. This document should have dynamic fields. e.g. you could use`Office Furnitures Header` document. 4- Print -> PDF Quote. Cause: --- In previous fix, we fixed the traceback when no dynamic field is set. However, if you have a dynamic field, then inside `PdfWriter._update_field_annotation()`, the font is get from `DR` dict inside acroform: https://github.com/py-pdf/pypdf/blob/f20954f2241640feb484800e191373f8fbdfa44b/pypdf/_writer.py#L917-L929 Even if we are not setting a font, we need to have an empty `DR` dict inside acroform, in order to avoid calling `get` on a none object, which is leading to the traceback. Also in previous fix we were losing `NeedAppearances` inside `AcroForm` by creating new dict which wasn't right. opw-6392001 Forward-Port-Of: odoo/odoo#278881
Website editors without accounting or expense access can now save SEO cover images without hitting access errors caused by unrelated expense attachments. The fix keeps attachment checks focused on the selected image, avoiding broad scans that could fail or slow down on large 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-6467087Fixes an issue in Documents where clicking inside the “Search More...” selection window could unexpectedly close it while editing document details. Users can now search, sort, and choose related records such as Owner or Customer without losing their current document selection.
Original PR description
Steps to reproduce: 1. Install Documents 2. In the Documents list view, select a document to display the inspector. 3. Edit a field such as Owner or Customer which uses a Many2one widget. 4. In the…
Steps to reproduce: 1. Install Documents 2. In the Documents list view, select a document to display the inspector. 3. Edit a field such as Owner or Customer which uses a Many2one widget. 4. In the field dropdown, click "Search More..." to open a modal dialog. 5. Click inside the "Search More..." modal (e.g., to sort columns or resize headers). Issue: - The modal dialog immediately closes, and the contact cannot be selected. Root cause: - When an inspector field is edited, the record row is put into edit mode. While in edit mode, the documents list renderer listens for global clicks. Clicking inside the "Search More..." modal dialog targets elements that have `.o_list_renderer` (since the modal dialog renders a list view). Because the click target is within a list renderer but is not a document row, `DocumentsListRenderer.onGlobalClick` executes and clears the selection of the main list view. Clearing the selection unmounts the edited field in the inspector, thereby destroying the modal dialog stack. Solution: - Modify DocumentsListRenderer.onGlobalClick to scope click handling to the current Documents list renderer. Ignore clicks outside this.root.el, so interactions in nested UI such as Search More... do not clear the main selection and destroy the inspector field. opw-6253360 Forward-Port-Of: odoo/enterprise#128812 Forward-Port-Of: odoo/enterprise#119262
This fixes cases where the same user group could be known by more than one internal identifier, causing permission checks to return inconsistent results. Users should now receive the correct access rights regardless of which valid group identifier is used.
Original PR description
A group can be identified by multiple xmlids. We add support to provide a list of "refs" to the `SetDefintions` object.
Reproductible issue:
```
demo = self.env["res.users"].browse(5)
demo.has_group("accountant.group_account_user") # False
demo.has_group("account.group_account_user") # True
assert self.env.ref("accountant.group_account_user") == self.env.ref("account.group_account_user")
```
task-6471260
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#285153
Forward-Port-Of: odoo/odoo#284866This fixes an issue where checks created from templates no longer appeared on matching tax returns. Accounting users can now rely on configured checks to show up when generating and reviewing tax returns, helping preserve the intended review process.
Original PR description
commit introducing the issue: https://github.com/odoo/enterprise/commit/034e0157eed0f19471879ca133faa40b7f0aabf3 Since this commit, it's no longer possible to see a check defined from a check template in a tax return. Steps to reproduce: - Go to Accounting / Configuration / Checks - Create a new one, give it a name, a random cycle and assign it a to a tax return - Open the tax returns view, generate them, and open a tax return of the same type -> The check should be visible
This fix ensures Uruguayan electronic invoicing uses the exact exchange rate saved on the original invoice, rather than recalculating it from rates that may have changed later. This prevents mismatches on related documents such as credit notes and improves consistency in tax and accounting records.
Original PR description
18.0 introduced an `invoice_currency_rate` field, storing the currency rate used for each invoice. Currently `_l10n_uy_edi_get_used_rate` uses the `currency.convert()` method using the date and company of the invoice. This is vulnerable to inaccuracy if the currency rates are changed after the invoice posting. For example, if an invoiceis posted and the currency rates are updated, the invoice will use the old rate, while `_l10n_uy_edi_get_used_rate` will return the new one. In the case of the customer in the related ticket, this caused their credit note reference currency rate to mismatch with the one on the invoice. This PR changes the `currency.convert()` call to fetching and inverting the `invoice_currency_rate` field. This ensures the returned value will match the one used for the invoice. opw-6456867 Forward-Port-Of: odoo/enterprise#128190
This fix ensures Nilvera e-invoice PDF actions only appear for Turkish companies and can be used by regular Invoicing users without administrator access errors. It also makes manual PDF fetching more responsive when an invoice status was previously unknown, reducing delays and confusion for users.
Original PR description
## Description of the issue/feature this PR addresses: Two related Nilvera e-invoice issues affecting Turkish companies: - The "Fetch Nilvera PDF" button appeared for every company, not just Turkish…
## Description of the issue/feature this PR addresses:
Two related Nilvera e-invoice issues affecting Turkish companies:
- The "Fetch Nilvera PDF" button appeared for every company, not just Turkish ones.
- Sending/fetching e-invoices (or fetching the PDF) as a non-admin Invoicing user raised an
AccessError, because the company's Nilvera API key field is restricted to System/Settings users
and several call sites read it without `.sudo()`.
## Current behavior before PR:
- The PDF-fetch button shows on both the list view and form view of `account.move` regardless of
the company's country.
- An Invoicing-only user (no System/Settings access) gets an AccessError when sending/fetching
e-invoices or fetching the PDF, because `_get_nilvera_client` reads
`company.l10n_tr_nilvera_api_key` without `.sudo()`.
## Desired behavior after PR is merged:
- Both buttons are only visible for Turkish companies (the list-view button has no bound record, so
its gate is driven by a new `l10n_tr` session_info/JS context injection instead of a direct field
reference).
- Invoicing users can send/fetch e-invoices and fetch the PDF without hitting an AccessError.
## Things to add on forward-port
The `.sudo()` fix lives inside `_get_nilvera_client` itself (`l10n_tr_nilvera/lib/nilvera_client.py`),
so every caller that goes through it is already fixed automatically once this diff forward-ports.
Only call sites that read `company.l10n_tr_nilvera_api_key` **directly**, bypassing
`_get_nilvera_client`, still need their own `.sudo()`:
### 19.4
- [ ] Fix e-Dispatch/e-Receipt fetch gate-check access error (direct read of the API-key field,
sudo it the same way as `_l10n_tr_nilvera_company_get_documents` in this PR)
task-6328589
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#284334
Forward-Port-Of: odoo/odoo#274312Archiving or deleting one user no longer removes their shared contact from restricted discussion channels when another active user for that contact still qualifies. This prevents unnecessary loss of channel access for businesses using multiple user accounts tied to the same contact.
Original PR description
Before this commit, archiving or deleting a user removed its partner from every group restricted channel, even when another user of that partner was still active and in the group the channel requires. This happens because the members to unsubscribe are searched on partner_id alone, so the search cannot tell whether the partner keeps another user. This commit fixes the issue by unsubscribing a partner only when none of its remaining users has the group the channel requires. Forward-Port-Of: odoo/odoo#283933 Forward-Port-Of: odoo/odoo#283807
This fixes an issue where default contact and user avatars could be downloaded instead of shown in the browser on some systems. The avatar image format is now recognized consistently, so initials-based avatars display properly across Odoo screens such as contacts, chatter, and Discuss.
Original PR description
**Description of the issue/feature this PR addresses:** `avatar.mixin._avatar_generate_svg()` generates an auto-initial SVG avatar for any record without a real uploaded image (used by `res.partner`,…
**Description of the issue/feature this PR addresses:** `avatar.mixin._avatar_generate_svg()` generates an auto-initial SVG avatar for any record without a real uploaded image (used by `res.partner`, `res.users`, and anything else inheriting `avatar.mixin`). The generated SVG opens with a single-quoted XML declaration: `<?xml version='1.0' encoding='UTF-8' ?>`. When this content is served via `/web/image/...`, `guess_mimetype()` needs to determine its Content-Type since it's a computed value with no stored attachment metadata. On systems where the installed `libmagic` library classifies that specific single-quoted byte pattern as `text/xml` rather than `image/svg+xml`, the wrong Content-Type reaches the browser. **Current behavior before PR:** On affected `libmagic` versions/databases, an auto-generated avatar (a contact or user with no uploaded photo) gets served with `Content-Type: application/octet-stream` (or `text/xml`) instead of `image/svg+xml`. Browsers can't render that inline as an image, so instead of showing the colored-initial avatar, the browser downloads it as an unrecognized file. This affects any place these avatars are displayed: contact/user form and kanban views, chatter message authors, Discuss, etc. Confirmed reproducible with `libmagic` 538 (`python-magic`), where the single-quoted declaration is classified as `text/xml`, while the exact same content with double-quoted attributes is correctly classified as `image/svg+xml`. **Desired behavior after PR is merged:** `_avatar_generate_svg()` now generates its markup with double-quoted attributes throughout, which is correctly sniffed as `image/svg+xml` regardless of the installed `libmagic` version. Auto-generated avatars render inline in the browser as intended. No other code depends on the exact quoting of this generated SVG - `res_users.py` and `hr_employee.py` both only assign the returned bytes to an image field without inspecting their content. Avatar fields are computed and non-stored, so nothing needs to be migrated - every record gets the corrected markup on its very next read, with no backfill required. Updated the two existing `test_avatar_mixin.py` tests that asserted the exact (single-quoted) SVG string, and added `test_generated_partner_avatar_mimetype` to assert the generated avatar is actually sniffed as `image/svg+xml`, so this can't silently regress. Ran locally: all 6 tests in `TestAvatarMixin` pass. Forward-Port-Of: odoo/odoo#283741 Forward-Port-Of: odoo/odoo#283149
Closed or renewed subscriptions will no longer incorrectly appear in Orders to Invoice because prepaid recurring lines are now cleared when no future billing is due. Postpaid items remain invoiceable so businesses can still bill for products or services already delivered before closure.
Original PR description
When a closed subscription could still show up in the Orders to Invoice because its recurring lines kept their "to invoice" status. This was misleading since no further period should be billed. When a subscription is churned (or renewed), prepaid lines are now flagged as nothing to invoice. Postpaid lines are left as is so that already delivered products can still be billed after closing. task-6227787 Forward-Port-Of: odoo/enterprise#117797
When users choose "Do not ask me again" while printing through an IoT Box, future reports now go directly to the selected printer instead of being downloaded as PDFs. This prevents printing interruptions and keeps repeated report printing aligned with the user's saved preference.
Original PR description
Since odoo/enterprise#113128, when a user prints a report through the IoT Box and enables the "Do not ask me again" checkbox, the next print: - before: it downloads the pdf instead of printing it, - after: it correctly prints the document.
This fixes an issue where French employees could not have their working hours updated if they had approved time off on a non-working day, such as a Saturday. The leave dates now remain valid in that situation, so HR can update schedules without removing or changing the approved time off.
Original PR description
**Problem:** For a French company, an employee whose working schedule differs from the company's cannot have their Working Hours changed when they have a validated time off that falls on a…
**Problem:** For a French company, an employee whose working schedule differs from the company's cannot have their Working Hours changed when they have a validated time off that falls on a non-working day (e.g. a Saturday). Saving fails with "The operation cannot be completed: The start date must be before or equal to the end date." **Steps to reproduce:** 1. Install l10n_fr_hr_holidays and work in a French company. 2. Set the company Working Hours and a reference (Paid) Time Off type. 3. Give an employee a Monday-to-Friday schedule that differs from the company's. 4. Create a one day Paid Time Off for the employee on a Saturday. 5. Change the employee's Working Hours. **Current behavior:** Saving is rejected by the date_from <= date_to constraint; the Working Hours cannot be changed as long as the weekend time off exists. **Expected behavior:** The Working Hours can be changed and the time off keeps a valid date range. **Cause of the issue:** When the French computation applies, `_get_fr_date_from_to` moves `date_start` forward to the first working day and, in a separate loop, moves `date_target` forward while the next day is a non-working day. The two loops are asymmetric: for a leave lying entirely on non-working days (a single Saturday for a Monday-to-Friday employee) `date_start` is pushed to the following Monday while `date_target` only reaches the Sunday. The pair is then written to `date_from`/`date_to` as Monday > Sunday, violating the date_from <= date_to constraint. **Fix:** A leave that contains no working day has nothing to anchor the "lost days" extension on, so the adjustment must not apply. Detecting the crossed pointers and keeping the leave's original dates preserves a valid range while leaving every leave that contains at least one working day untouched. opw-6348425 Forward-Port-Of: odoo/odoo#284534 Forward-Port-Of: odoo/odoo#278833
Submitting a Dutch VAT correction report now updates the correction return shown on screen, instead of accidentally applying the submission to the original VAT return for the same period. This prevents businesses from filing or marking the wrong tax return as submitted.
Original PR description
Opening a tax report from a return and submitting it could act on a different return than the one on screen. _get_return_from_report_options searches by company, period and report with limit=1, but…
Opening a tax report from a return and submitting it could act on a different return than the one on screen. _get_return_from_report_options searches by company, period and report with limit=1, but that combination is not unique: l10n_nl declares two return types on l10n_nl.tax_report, nl_tax_return_type and nl_tax_correction_return_type, so a VAT return and its correction both match. Which one is returned is then decided by _order (is_completed, date_deadline, name, id). For a Dutch VAT correction it resolves to the original VAT return of the same quarter, so send_xbrl submits and flags that record instead of the correction. The options already carry the return type they were built for, in the return_periodicity filter, so restrict the search to it when it is set. l10n_nl_reports kept a return_id option for the same reason when computing the already declared amount of a suppletie; it can use _get_return_from_report_options now. opw-6421300 Forward-Port-Of: odoo/enterprise#129278 Forward-Port-Of: odoo/enterprise#127073
Saving Point of Sale settings with employee login and delivery integrations no longer removes employees granted advanced rights. This prevents accidental loss of staff access when managers update shop settings.
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
Fixed an accounting issue where reversing a cash basis tax entry after unreconciling a payment could place the reversal in the current period instead of the original tax period. This keeps tax reports balanced in the correct month and prevents misleading period differences.
Original PR description
When unreconciling a payment from an invoice with a cash basis tax, the tax cash basis (CABA) entry is reversed. The reversal is supposed to land in the same period as the origin entry so the tax…
When unreconciling a payment from an invoice with a cash basis tax, the tax cash basis (CABA) entry is reversed. The reversal is supposed to land in the same period as the origin entry so the tax report nets to zero for that period. Steps to reproduce: - Enable cash basis and create a cash basis tax (exigibility on payment) - Post an invoice dated in the past with that tax - Reconcile a bank statement line to the invoice - Resequence the cash basis entry so the month is dropped from the name (CABA/08/2026/0001 -> CABA/2026/0001) - Unreconcile the statement line Issue: The reversal CABA entry created on the last unreconcile is dated today instead of the origin entry's month. In the tax report the original tax amount stays in the statement's month while the reversal amount appears in the current month, so the two no longer cancel out. Analysis: While under a monthly journal sequence a past date returns the last day of that month, under a yearly sequence a past date within the current year returns the latter between the move date and today, moving the reversal out of the origin period. opw-6301553 Forward-Port-Of: odoo/odoo#284840 Forward-Port-Of: odoo/odoo#281250
This fixes an issue where manually adjusted prices on optional quotation lines could be overwritten when customers changed quantities in the portal preview. Sales teams can now rely on custom prices being preserved while still allowing normal pricelist behavior when no manual price was set.
Original PR description
When a user manually sets a price on an optional line (overriding the pricelist), and then changes the quantity in the portal preview, the manual price is lost and gets reset to the pricelist price.…
When a user manually sets a price on an optional line (overriding the pricelist), and then changes the quantity in the portal preview, the manual price is lost and gets reset to the pricelist price. Steps to reproduce: --- - Install Sales module and enable Pricelists. - Create a product with qty-based pricelist rules: - min qty: 1 → price: 100 - min qty: 10 → price: 80 - Create a quotation with an optional section containing this product. - Manually change the product price to 150 (overriding pricelist). - Mark the section as optional and preview the quotation. - Change the quantity to 10 in portal preview. Issue: --- - The manually set price (150) is incorrectly reset to the pricelist price (80). Root cause: --- - After [commit], if there is no config parameter set and if there is an active pricelist, we simply call `_reset_price_unit()` without checking whether the price was manually set or not. - Additionally, `_reset_price_unit()` calls `update()` which writes `price_unit` and `technical_price_unit` one by one as separate `write()` calls. The `write()` method has a guard([1]) that strips a lone `technical_price_unit` write unless `sale_write_from_compute` is set in context. Without this flag, `technical_price_unit` is silently discarded, causing it to drift from `price_unit`. On the next qty change, this mismatch is detected as a manual price, permanently blocking further pricelist updates. Solution: --- - Check whether the price was manually set before calling `_reset_price_unit()`, and pass `sale_write_from_compute=True` in context so both `price_unit` and `technical_price_unit` are written correctly. [commit]: https://github.com/odoo/odoo/commit/93b6bdd6a4909bc0b45b90ab6a2d0734a218292d [1]https://github.com/odoo/odoo/blob/fffd987cc98d1ea0cd04e24dda2ed8b64a219cdc/addons/sale/models/sale_order_line.py#L1391-L1399 opw-6426634 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#281107
This fix ensures manufactured products are counted correctly in stock forecasts when a warehouse uses a three-step manufacturing flow. It prevents replenishment decisions from being based on missing incoming manufactured quantities, improving planning accuracy.
Original PR description
### Steps to reproduce: - In the settings enable Multi-Steps Routes - Put your warehouse in manufacture in 3 steps - Create a storable product P - Create and confirm an MO for 1 unit of P - Go to…
### Steps to reproduce: - In the settings enable Multi-Steps Routes - Put your warehouse in manufacture in 3 steps - Create a storable product P - Create and confirm an MO for 1 unit of P - Go to Inventory > Operations > Procurement > Replenishment - Create a new one for P in WH/stock #### > The forecasted quantity in stock is still 0 but should be at 1 ### Cause of the issue: This is the exact use case already fixed in 85dd3369ed17b98b2ce485be04f140cf4cfa8aa3, which stamped the finished move with a `location_final_id` pointing at WH/Stock so that the move contributes to the forecast there even though its `location_dest_id` is the intermediate WH/Post-Production. That fix was reverted in practice by 42275f83dc5350822a625e19d65148e8b41ab1d4, which replaced the value with `mo.location_dest_id`: https://github.com/odoo/odoo/blob/6a56908e5febfdb4e6e0eacfb8655b092319819f/addons/mrp/models/stock_move.py#L466-L467 Its reasoning was that in a single-warehouse setup `location_dest_id` equals the warehouse stock location, so the behaviour would be unchanged. That holds in 1 and 2 steps, where the extra step is on the component side and only moves `default_location_src_id` to the pre-production location. It breaks in 3 steps, the only mode that also moves `default_location_dest_id`, to the post-production location: https://github.com/odoo/odoo/blob/6a56908e5febfdb4e6e0eacfb8655b092319819f/addons/mrp/models/stock_warehouse.py#L246-L247 and `_compute_locations` propagates it to the MO: https://github.com/odoo/odoo/blob/6a56908e5febfdb4e6e0eacfb8655b092319819f/addons/mrp/models/mrp_production.py#L334-L341 WH/Post-Production is a sibling of WH/Stock under the warehouse view location, not a child of it. Since `location_final_id` takes precedence over `location_dest_id` for the non-done part of the move chain: https://github.com/odoo/odoo/blob/6a56908e5febfdb4e6e0eacfb8655b092319819f/addons/stock/models/product.py#L331-L334 the finished move stopped being counted in the WH/Stock forecast. Why the test did not catch it: `test_3_steps_manufacturing_forecast` stayed green through the whole regression, because it scoped `virtual_available` with a `location_id` context key. `_get_domain_locations` only reads `location` and `warehouse_id`; `location_id` is silently ignored: https://github.com/odoo/odoo/blob/6a56908e5febfdb4e6e0eacfb8655b092319819f/addons/stock/models/product.py#L284-L287 The call therefore fell through to the branch scoping the forecast to every warehouse view location: https://github.com/odoo/odoo/blob/6a56908e5febfdb4e6e0eacfb8655b092319819f/addons/stock/models/product.py#L303-L309 and the warehouse view location is the common parent of both WH/Stock and WH/Post-Production. The assertion held regardless of where `location_final_id` pointed, so the test was a false positive from the start: it also passes with 85dd3369ed17b98b2ce485be04f140cf4cfa8aa3 fully reverted. Using the `location` key makes it fail without the fix and pass with it. ### Fix: Neither fix proposition was right on its own; each one was correct only in its own scenario. The`mo.warehouse_id.lot_stock_id` resolves the warehouse from the components, so it points at the wrong warehouse as soon as the finished product is produced for another one. `mo.location_dest_id` is the post-production location as soon as the warehouse manufactures in 3 steps, so it drops the quantity from the forecast of the manufacturing warehouse itself. What separates the two is not the warehouse but whether the destination is a transit step. In 3 steps the finished product only reaches the stock through the post-production push rule: https://github.com/odoo/odoo/blob/6a56908e5febfdb4e6e0eacfb8655b092319819f/addons/mrp/models/stock_warehouse.py#L57 so the final location is the stock of the warehouse owning that destination. Any other destination is already final and is kept as is, which leaves cross-warehouse MOs and destinations set to a sub-location of the stock untouched. The rule's destination is read rather than `warehouse.lot_stock_id` because a push move takes its destination from the rule and not from the operation type: https://github.com/odoo/odoo/blob/6a56908e5febfdb4e6e0eacfb8655b092319819f/addons/stock/models/stock_rule.py#L256-L260 so the forecast stays correct when the store step is reconfigured to land somewhere else than the warehouse stock. The rule is looked up on `pbm_route_id` by its `picking_type_id` instead of through `warehouse.sam_rule_id`, because that field is no longer set. It used to be an entry of `_generate_global_route_rules_values`, and it is that entry which made the generic warehouse machinery create the rule and store it back on the warehouse: https://github.com/odoo/odoo/blob/6a56908e5febfdb4e6e0eacfb8655b092319819f/addons/stock/models/stock_warehouse.py#L403-L410 11e69870db1c49d9a6af79ffd263e4e162b34b6b removed it when the post-production step stopped being a pull rule on the Manufacture route and became a push rule generated from `get_rules_dict`. Only the field declaration was left behind, and nothing writes it any more: https://github.com/odoo/odoo/blob/6a56908e5febfdb4e6e0eacfb8655b092319819f/addons/mrp/models/stock_warehouse.py#L21-L22 so reading it would silently give an empty recordset. The lookup is not delegated to `_get_push_rule` to avoid a search per finished move. opw-4882390 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#283952 Forward-Port-Of: odoo/odoo#283234
Fixed an issue that prevented business users from launching the AI chat while editing product descriptions on website shop pages. The system now correctly recognizes the product being edited, avoiding missing-record errors and keeping the website editing workflow smooth.
Original PR description
Steps to reproduce ================== 1. Install Website, eCommerce and AI 2. Open a product page on the website (/shop) and click "Edit" 3. Click inside the product description and type "/ai" =>…
Steps to reproduce
==================
1. Install Website, eCommerce and AI
2. Open a product page on the website (/shop) and click "Edit"
3. Click inside the product description and type "/ai"
=> Missing record
Record does not exist or has been deleted. (Record: product.template('8',), User: 2)
Root cause
==========
The website builder reads the record from the edited element, whose id comes from a `data-oe-id` attribute, so `getRecordInfo` returns it as a string. `ChatGPTPlugin` forwards it to `action_launch_ai_chat`, where `browse(res_id)` receives `'8'` and iterates the string instead of taking an id: `browse('8')` yields `product.template('8',)`.
`check_access('read')` then evaluates the record rules, which read a field on that non existing record and raise. The error only shows up for a regular user, as a superuser skips the rule check.
Ids of two digits or more fail differently, `browse('23')` gives `product.template('2', '3')` hence "Expected singleton".
This regressed in 19.3 in [800208fdc485], which replaced `search([('id', '=', int(record_id))])` by `browse(res_id)` and dropped the cast. Editing the same field from the product form is unaffected, the form view sends the id as an integer.
Fix
===
Cast the record id when entering `action_launch_ai_chat`, so the later `browse` calls and the session `res_id` all get an integer.
[800208fdc485]: https://github.com/odoo/enterprise/commit/800208fdc485b9e37f77648a01100b8eff118490
opw-6467840This fixes an issue where Sendcloud shipping labels were always returned as PDFs, even when another format such as ZPL was requested. Businesses using label printers or specific shipping workflows will now receive labels in the correct format, reducing manual work and printing errors.
Original PR description
We send the label type we want to get (zpl, pdf, ...) in the request headers. However, since odoo/enterprise#115999, we also send the partner ID in the headers. This was overriding the headers passed to the method, making Sendcloud always return a PDF label. Forward-Port-Of: odoo/enterprise#129542
This fixes an issue where failed automatic subscription payments could cause the recurring invoice process to crash instead of handling the failure cleanly. It also prevents invoices from being incorrectly unlinked after successful payments, improving reliability for subscription billing.
Original PR description
Step to reproduce: - create a faulty token that won't work and link it to a subscription - launch the recurring invoice cron - the following traceback occurs ``` last_tx_sudo = (self.transaction_ids…
Step to reproduce:
- create a faulty token that won't work and link it to a subscription
- launch the recurring invoice cron
- the following traceback occurs
```
last_tx_sudo = (self.transaction_ids - existing_transactions).sudo()
```
When the payment fails, the system rollback and we store the last_tx_sudo value in a dedicated variable. After rollback, the record does not exists anymore. Therefore, accessing the value fails.
```
File "/home/odoo/src/enterprise/saas-18.2/sale_subscription/models/sale_order.py", line 1703, in _handle_automatic_invoices
if not last_tx_sudo or last_tx_sudo.renewal_state in ['pending', 'authorized']:
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/saas-18.2/odoo/orm/fields.py", line 1439, in __get__
self.compute_value(record)
File "/home/odoo/src/odoo/saas-18.2/odoo/orm/fields.py", line 1603, in compute_value
records._compute_field_value(self)
File "/home/odoo/src/odoo/saas-18.2/odoo/orm/models.py", line 4575, in _compute_field_value
determine(field.compute, self)
File "/home/odoo/src/odoo/saas-18.2/odoo/orm/fields.py", line 69, in determine
return needle(*args)
^^^^^^^^^^^^^
File "/home/odoo/src/enterprise/saas-18.2/sale_subscription/models/payment_transaction.py", line 25, in _compute_renewal_state
if tx.state in ['draft', 'pending']:
^^^^^^^^
File "/home/odoo/src/odoo/saas-18.2/odoo/orm/fields.py", line 1406, in __get__
raise MissingError("\n".join([
odoo.exceptions.MissingError: Record does not exist or has been deleted.
```
Moreover, since https://github.com/odoo/enterprise/pull/45236/files#diff-c36fd7952cc2bef40716419a668de41963d49e1aa4177d9319d503fc260da588R1678-R1682
```
if not last_tx_sudo or not last_tx_sudo.renewal_state not in ['pending', 'authorized']:
```
has become
```
if not last_tx_sudo or last_tx_sudo.renewal_state in ['pending', 'authorized']:
```
But it feels strange to unlink the invoice when the payment succeed.
This PR fixes it.
Forward-Port-Of: odoo/enterprise#83913The accounting gap indicator now checks invoice numbering separately for each suffix, so invoices like `00010` and `00010A` no longer interfere with each other. This prevents incorrect red warning flags and helps users trust that numbering gaps are only shown when there is a real issue within the relevant sequence.
Original PR description
### Issue: When moves share the same `sequence_number` in a journal but have different suffixes (e.g. `INV/2026/00010` and `INV/2026/00010A`), the `made_sequence_gap` flag was incorrectly set ###…
### Issue: When moves share the same `sequence_number` in a journal but have different suffixes (e.g. `INV/2026/00010` and `INV/2026/00010A`), the `made_sequence_gap` flag was incorrectly set ### Cause: `_update_sequence_made_gap`, introduced in commit https://github.com/odoo/odoo/commit/17893089e8b21c0ecab5e61ed8e2c33f3731b3ac selects the previous and next moves ordered by `sequence_number` without filtering by suffix This causes two issues: - Moves from different suffix sequences are used as neighbors, leading to incorrect gap detection - Duplicate `sequence_number` values across suffixes are not accounted for, so only one move is considered per number ### Steps to reproduce: - Install `account` - Post 12 invoices to get a sequence up to `INV/2026/00012` - Reset `INV/2026/00012` to draft, rename it to `INV/2026/00010A` - Reset `INV/2026/00010A` to draft, rename it to `INV/2026/00009A` and confirm Before the fix: `INV/2026/00011` is red Expected: `INV/2026/00011` should not be red because `INV/2026/00010` exists - Delete `INV/2026/00009` Before the fix: `INV/2026/00010` is red Expected: `INV/2026/00010` should be red (gap in no-suffix sequence) - Reset `INV/2026/00009A` to draft and confirm it again Before the fix: `INV/2026/00010` is not red Expected: `INV/2026/00010` should still be red (different suffix) ### Notes: Suffix changes are treated as distinct sequences following the same gap rules as any other sequence This was agreed with R&D — the gap flag is meant to signal inconsistencies within a sequence, not across suffixes opw-6454823 Forward-Port-Of: odoo/odoo#282503
Chilean invoices created from Point of Sale orders are now automatically sent to the SII tax authority as expected. This prevents businesses from having to manually send these invoices after closing a register and helps keep tax reporting compliant.
Original PR description
Issue: Invoices from PoS orders are not automatically send to SII. Steps to reproduce: - Open PoS - create an order - add a company as customer - pay - close register - go to invoice Current behavior: Invoice is created but not sent Expected behavior: Invoice is created and send to SII. Cause: Before 19.2 invoices were sent using a cron. Starting from 19.2, invoices are sent using the Send button of the invoice form. In order to get a perf improvement, PDF generation was deactivated for l10n_cl PoS invoices at creation. However, the same method used to generate the PDF is used to send the invoice to SII. Therefore, invoices from PoS were not sent to SII. opw-6423528 Forward-Port-Of: odoo/enterprise#126623
Unreconciling one bank statement line from an invoice or bill now only removes that specific match instead of clearing all related reconciliations. This prevents accidental disruption of payments already matched to the same document and keeps accounting records more accurate.
Original PR description
**STEP TO REPRODUCE** 1. Create a bill or an invoice. 2. Create multiples bank statement. 3. Reconciles those bank statements to the invoice/bill. 4. Unreconciles one of those bank statement on the invoice/bill. 5. Notice the invoice/bill is completely unreconciled. Expected behavior: only the unreconciled line should be unreconciled. **CAUSE** When unreconciling a partial linked to a bank statement, we call `delete_reconciled_line()` on both `partial.credit_move_id` and `partial.debit_move_id`. One on those is the the payment_term line of the invoice/bill the bank statement line is reconciled with. This payment_term line is also linked to all partial reconcilliation line on the invoice/bill, so calling `delete_renconciled_line()` delete all the reconciled line of the invoice/bill. **FIX** We should call `delete_renconciled_line()` only on the bank statement move line, not on the payment term line. opw-6465096 Forward-Port-Of: odoo/enterprise#128126
Appointment invitation emails now generate public calendar links without triggering access errors. This ensures attendees can receive and open their calendar invitations as expected, reducing failed email rendering for appointment bookings.
Original PR description
Since calendar attendee access tokens are restricted to system users, appointment mail templates must sudo token reads when generating public calendar links. This follows the same pattern as the calendar mail templates and avoids an AccessError when rendering attendee invitation emails. ref: https://github.com/odoo/enterprise/commit/88a3cca752a5f726cd0260b485fc93f65a268cf8 Task-4711415 Forward-Port-Of: odoo/enterprise#129511