Daily updates from Odoo
Monday, July 20, 2026
189 changes
24 changes
Resolved issues and error corrections
No description available.
Steps to reproduce: - Go to Website - Upload an image in the company logo (navbar) - Open the website editor and click on the logo - Open the image info panel Current behavior: When clicking on the website logo in the editor, the size shown is always a constant number ~5.9kB, regardless of the actual size of the uploaded logo. The correct size is visible in the browser DOM. This gives users the wrong impression that their image is being heavily compressed or losing quality when it isn'
Original PR description
Steps to reproduce: - Go to Website - Upload an image in the company logo (navbar) - Open the website editor and click on the logo - Open the image info panel Current behavior: When clicking on the…
Steps to reproduce: - Go to Website - Upload an image in the company logo (navbar) - Open the website editor and click on the logo - Open the image info panel Current behavior: When clicking on the website logo in the editor, the size shown is always a constant number ~5.9kB, regardless of the actual size of the uploaded logo. The correct size is visible in the browser DOM. This gives users the wrong impression that their image is being heavily compressed or losing quality when it isn't. Reason: When clicking the logo, the editor tries to find the original, unprocessed version of the image so it can support cropping and other edits. It does this by asking the server to match the image's URL to a stored attachment. The website logo is served through a dynamic link (`/web/image/website/<id>/logo/<name>`) that isn't tied to a regular attachment record the way normal content images are, since it isn't uploaded through the usual media picker. Because of this, the server can't find a matching original, and the editor is left without a valid image source to work with. As a fallback, the editor tries to load a placeholder path instead of a real image. This request fails and silently resolves to Odoo's generic "image not found" placeholder. All further processing (and the size calculation) then happens on this small placeholder image instead of the actual logo, which is why the size shown never changes. Fix: When `get_image_info` does not return a usable `original`, `loadImageInfo` now falls back to using the image's own current src as `originalSrc`, instead of leaving it unset. This ensures `loadImage` always receives a valid, resolvable URL, so image processing (and the size shown) reflects the actual logo. opw-6260496 Forward-Port-Of: odoo/odoo#277125 Forward-Port-Of: odoo/odoo#273542
### [FIX] web: fix mocked dates in tests Dates are mocked in all unit tests by default; either to a default static value, or to one defined in the test/suite. To do so, the `window.Date` constructor is overridden to have the arguments auto-filled with the current mock date paramters. Before this commit, an additional offset was always added to the given arguments to reflect: - the time elapsed from the beginning of the test; - any virtual offset added by a helper such as 'advanceTime
Original PR description
### [FIX] web: fix mocked dates in tests Dates are mocked in all unit tests by default; either to a default static value, or to one defined in the test/suite. To do so, the `window.Date` constructor…
### [FIX] web: fix mocked dates in tests Dates are mocked in all unit tests by default; either to a default static value, or to one defined in the test/suite. To do so, the `window.Date` constructor is overridden to have the arguments auto-filled with the current mock date paramters. Before this commit, an additional offset was always added to the given arguments to reflect: - the time elapsed from the beginning of the test; - any virtual offset added by a helper such as 'advanceTime'. The issue is that the spec of the Date constructor wants that the returned date object has to match all given arguments, and any omitted argument will be defaulted to the current date/time. This was not the case in tests, as the offset was always added, regardless of the given arguments. With this commit: only the arguments that are NOT given and that have been defaulted to current (mocked) date/time will be offset by the adequate value. Furthermore: as these mocked parameters are meant to reflect UTC values, the offset now also considers the *actual* browser offset to generate a local date from the mock date parameters. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#277041 Forward-Port-Of: odoo/odoo#275391
Rather than assign `log_target` and blow up if the third branch is taken, use a `defaultdict` and just increment the count in each branch on hit. The continue in the third branch is not strictly necessary, but that way it protects us if anyone decides to add post-processing which also doesn't account for the third branch. Forward-Port-Of: odoo/odoo#276757 Forward-Port-Of: odoo/odoo#276481
Original PR description
Rather than assign `log_target` and blow up if the third branch is taken, use a `defaultdict` and just increment the count in each branch on hit. The continue in the third branch is not strictly necessary, but that way it protects us if anyone decides to add post-processing which also doesn't account for the third branch. Forward-Port-Of: odoo/odoo#276757 Forward-Port-Of: odoo/odoo#276481
In the Outlook calendar sync, it's possible to have the same attendee twice. That's because the normalized email wasn't used to check for preexisting attendee. To reproduce, sync event with the organizer, also an attendee, using high case in the email such as: ORGANIZER: Mike@organizer.com ATTENDEE: Mike@organizer.com ATTENDEE: John@attendee.com opw-6186606 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#272258 For
Original PR description
In the Outlook calendar sync, it's possible to have the same attendee twice. That's because the normalized email wasn't used to check for preexisting attendee. To reproduce, sync event with the organizer, also an attendee, using high case in the email such as: ORGANIZER: Mike@organizer.com ATTENDEE: Mike@organizer.com ATTENDEE: John@attendee.com opw-6186606 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#272258 Forward-Port-Of: odoo/odoo#268201
Steps to reproduce: ------------------------------------ 1. Install barcode_gs1_nomenclature Module. 2. Go to Inventory > Configuration > Barcode Nomenclature. 3. Open any barcode nomenclature. Observation: ------------------------------------ The "Is GS1 Nomenclature" field is visible in the list header. Issue: ------------------------------------ The field uses the invisible attribute, which hides the field values but does not hide the corresponding list header, resulting in an e
Original PR description
Steps to reproduce: ------------------------------------ 1. Install barcode_gs1_nomenclature Module. 2. Go to Inventory > Configuration > Barcode Nomenclature. 3. Open any barcode nomenclature. Observation: ------------------------------------ The "Is GS1 Nomenclature" field is visible in the list header. Issue: ------------------------------------ The field uses the invisible attribute, which hides the field values but does not hide the corresponding list header, resulting in an empty header column. Solution: ------------------------------------ Replace the 'invisible' attribute with 'column_invisible' so that both the field values and the corresponding list header are hidden. opw-6390731 Forward-Port-Of: odoo/odoo#276532
Since #198829, a `t-att-class` has been added to the `loadOlder` template that relies on `mountedAndLoaded` of thread state to control the visibility of the `Load More` button. But it doesn't read the value from the state properly. As a result, the button is always transparent. This change fixes this by reading the value from the state. Forward-Port-Of: odoo/odoo#276443 Forward-Port-Of: odoo/odoo#275253
Original PR description
Since #198829, a `t-att-class` has been added to the `loadOlder` template that relies on `mountedAndLoaded` of thread state to control the visibility of the `Load More` button. But it doesn't read the value from the state properly. As a result, the button is always transparent. This change fixes this by reading the value from the state. Forward-Port-Of: odoo/odoo#276443 Forward-Port-Of: odoo/odoo#275253
The `account_journal_type.bank` KPI (surfaced by the Databases app as `account_journal_type_bank`, tooltip "Draft entries in journal Bank") is meant to report journal entries in bank journals that still need attention: drafts, posted-but-unchecked entries, and posted bank entries that are not yet reconciled. The "not yet reconciled" case was implemented as: ```LEFT JOIN account_bank_statement_line st_line ON move.statement_line_id = st_line.id AND (st_line.id IS NULL O
Original PR description
The `account_journal_type.bank` KPI (surfaced by the Databases app as `account_journal_type_bank`, tooltip "Draft entries in journal Bank") is meant to report journal entries in bank journals that…
The `account_journal_type.bank` KPI (surfaced by the Databases app as `account_journal_type_bank`, tooltip "Draft entries in journal Bank") is meant to report journal entries in bank journals that still need attention: drafts, posted-but-unchecked entries, and posted bank entries that are not yet reconciled.
The "not yet reconciled" case was implemented as:
```LEFT JOIN account_bank_statement_line st_line
ON move.statement_line_id = st_line.id
AND (st_line.id IS NULL OR NOT st_line.is_reconciled)```
`st_line.id IS NULL` does not only match unreconciled bank transactions, it also matches any posted move booked directly in a bank journal that never originated from an imported bank statement line (manual entries, bank fees, opening balances, ...). Those moves have no `statement_line_id` by design and can never be reconciled, so they were counted as "pending" forever, permanently inflating the KPI for any database with such entries.
Only require an actual bank statement line before flagging it as unreconciled, matching the logic already used for the "to reconcile" count on the journal dashboard (account_journal_dashboard.py).
opw-6199785
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#275597In [1], the need to build a registry for outgoing messages was removed, but `acquire_cursor` wasn't adapted. As a result, we might end up building more registries than needed, blocking the gevent server CPU. This commit fixes this issue. [1]: https://github.com/odoo/odoo/pull/235746 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
Original PR description
In [1], the need to build a registry for outgoing messages was removed, but `acquire_cursor` wasn't adapted. As a result, we might end up building more registries than needed, blocking the gevent server CPU. This commit fixes this issue. [1]: https://github.com/odoo/odoo/pull/235746 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#276441 Forward-Port-Of: odoo/odoo#276166
\* : html_editor Commit [1]: Steps to reproduce: replacing image stuck issue when deleted 1. Go to Website > Edit. 2. Add any picture snippet (e.g., Text-Image). 3. Click the 'Replace' button and upload an image. 4. Open the media dialog again and delete the uploaded image. 5. Click the 'Discard' button. 6. Try to save the changes. Issue: - The website gets stuck in the same position and does not allow saving. - In the Python terminal, a missing error warning appears because the
Original PR description
\* : html_editor Commit [1]: Steps to reproduce: replacing image stuck issue when deleted 1. Go to Website > Edit. 2. Add any picture snippet (e.g., Text-Image). 3. Click the 'Replace' button and…
\* : html_editor Commit [1]: Steps to reproduce: replacing image stuck issue when deleted 1. Go to Website > Edit. 2. Add any picture snippet (e.g., Text-Image). 3. Click the 'Replace' button and upload an image. 4. Open the media dialog again and delete the uploaded image. 5. Click the 'Discard' button. 6. Try to save the changes. Issue: - The website gets stuck in the same position and does not allow saving. - In the Python terminal, a missing error warning appears because the image is deleted from both `ir.ui.view` and `ir.attachment`. Expected behaviour: - Saving should be allowed with a default image, that is similar to other images. This commit catch the warning response and replaces the deleted image, allowing the website to save changes without getting stuck. Commit [2]: resolve traceback when leaving edit mode via browser Steps to reproduce: 1. Go to Website > Edit. 2. Open the snippet modal and select any snippet. 3. Press the 'Back' button in your browser. 4. A dialog will appear asking to discard changes; click 'OK'. 5. A traceback error occurs, and an empty space appears in the editor. Issue: - Previously, a commit addressed a similar scenario, but that time the browser had an event listener bind on hashchange. - Now, that `hashchange` event of browser has been replaced with `popstate`, which triggers before the 'window' event listener. - As a result, the editor is left in an unstable state, causing a traceback error. Solution: - This commit ensures the 'window' event executes before the browser event. - It verifies if the editor is open and forces a `skipLoad`, preventing the `route_change` call in the browser. task-4570164 Forward-Port-Of: odoo/odoo#277089 Forward-Port-Of: odoo/odoo#199193
See commit messages for details. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Original PR description
See commit messages for details. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Steps to produce: --- - Install the `Sales` module. - Go to Invoicing > Configuration > Accounting > Taxes. - Open the 0% Exports tax and set a value in the `Label on Invoices.` Field under the Advanced Options tab. - Create a sale order with multiple sections, apply the 0% Exports tax to the order lines, and enable `Hide Composition` on one of the sections. - Preview the sale order. Issue: --- - The tax displayed in a collapsed section summary differs from the tax shown on regular or
Original PR description
Steps to produce: --- - Install the `Sales` module. - Go to Invoicing > Configuration > Accounting > Taxes. - Open the 0% Exports tax and set a value in the `Label on Invoices.` Field under the…
Steps to produce: --- - Install the `Sales` module. - Go to Invoicing > Configuration > Accounting > Taxes. - Open the 0% Exports tax and set a value in the `Label on Invoices.` Field under the Advanced Options tab. - Create a sale order with multiple sections, apply the 0% Exports tax to the order lines, and enable `Hide Composition` on one of the sections. - Preview the sale order. Issue: --- - The tax displayed in a collapsed section summary differs from the tax shown on regular order lines for the same tax. Root cause: --- - At [1], `_get_grouped_section_summary()` only returns `tax_labels`, which are then used when rendering the collapsed section summary. As a result, the invoice label is displayed instead of the actual tax name. Solution: --- - Add `tax_names` to the values returned by `_get_grouped_section_summary()` and use them when rendering the collapsed section summary, ensuring consistent tax information across the preview. - Also removed the unnecessary fallback in the return statement, as `res` is always populated. [1]https://github.com/odoo/odoo/blob/be0bf1365e054521ece224f5e3a4b7cbce402315/addons/sale/models/sale_order_line.py#L1572-L1604 Before: --- <img width="1068" height="156" alt="image" src="https://github.com/user-attachments/assets/8030c35e-5c73-4fed-9feb-c4729add7894" /> After: --- <img width="1057" height="143" alt="image" src="https://github.com/user-attachments/assets/336030ed-68b2-4d14-b26f-ff391257d6d4" /> opw-6294141 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#277414 Forward-Port-Of: odoo/odoo#270544
Before this commit vat_label on address always changed to `Tax Identification Number` when l10n_my_edi_pos installed regardless of which company set website/user. Cause: In this PR https://github.com/odoo/odoo/pull/247156 in xpath replaced vat label with `Tax Identification Number` so vat_label is no longer calculated based on company or selected country. Fix: - Only set that label for MY country. Forward-Port-Of: odoo/odoo#276789
Original PR description
Before this commit vat_label on address always changed to `Tax Identification Number` when l10n_my_edi_pos installed regardless of which company set website/user. Cause: In this PR https://github.com/odoo/odoo/pull/247156 in xpath replaced vat label with `Tax Identification Number` so vat_label is no longer calculated based on company or selected country. Fix: - Only set that label for MY country. Forward-Port-Of: odoo/odoo#276789
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create Invoices` > select `Regular invoice` > `Create Draft`. (leave the invoice in the draft state). - Navigate to Accounting > Review > Invoiced Not Delivered. - Select the newly created invoice and click `Create Accrual Entries`. **Error:** `ZeroDivisionError: division by zero` **Root Ca
Original PR description
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create…
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create Invoices` > select `Regular invoice` > `Create Draft`. (leave the invoice in the draft state). - Navigate to Accounting > Review > Invoiced Not Delivered. - Select the newly created invoice and click `Create Accrual Entries`. **Error:** `ZeroDivisionError: division by zero` **Root Cause:** At [1], `processed_qty` is initialized with `0`. If all the selected invoices are in the draft state, the code never enters the for loop, so `processed_qty` remains 0. Later, it attempts to divide the `amount` by `processed_qty`, causing an error. **Fix:** This commit prevents errors when creating accrual entries for draft invoices. [1]: https://github.com/odoo/odoo/blob/a7d285a833f5b1d47a11a1b8ec31e3730c1c854c/addons/account/wizard/accrued_orders.py#L258-L265 opw-6332652 Forward-Port-Of: odoo/odoo#273330
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce email from will default to the bounce alias of the main company of the DB. In a multi company setup, where each company might have their own mail alias domain, this can be confusing for the receiving party that didn't send the original email to the main company's alias domain. ## Propo
Original PR description
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce…
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce email from will default to the bounce alias of the main company of the DB. In a multi company setup, where each company might have their own mail alias domain, this can be confusing for the receiving party that didn't send the original email to the main company's alias domain. ## Proposed solution: We call `_routing_create_bounce_email` with the right company in the context, since the journal company is already detected. Additionally, in the context of the accounting code calling the bounce private method, we pass an explicit value for `reply_to` ## How to reproduce it: 1) Set up DB with two companies (companyA and companyB). Install account. 2) Set up two mail alias domains (companya.com and companyb.com) 3) Send an email to the invoice alias of companyB (e.g. invoices@companyb.com). -> bounce email will have FROM as "notifications@companya.com" (the notifications alias of companyA) Example headers before: ``` Reply-To: "YourCompany" <catchall@companya.com> To: customer@example.com Date: Thu, 02 Jul 2026 11:18:49 -0000 Return-Path: bounce@companya.com From: "MAILER-DAEMON" <bounce@companya.com> ``` After fix: ``` Reply-To: info@companyb.com To: customer@example.com Date: Thu, 02 Jul 2026 11:16:19 -0000 Return-Path: bounce@companyb.com From: "MAILER-DAEMON" <notifications@companyb.com> ``` opw-6342778 Forward-Port-Of: odoo/odoo#273671
Issue: There is a missing closing curly bracket on line 67 in odoo/addons/stock/static/src/stock_forecasted/forecasted_details.xml (View) This PR corrects this error opw-6367046 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276636 Forward-Port-Of: odoo/odoo#274575
Original PR description
Issue: There is a missing closing curly bracket on line 67 in odoo/addons/stock/static/src/stock_forecasted/forecasted_details.xml (View) This PR corrects this error opw-6367046 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276636 Forward-Port-Of: odoo/odoo#274575
On touch devices (smartphones, tablets), the in-browser notification sound plays through the media audio channel, which is not affected by the device's silent mode. This causes unexpected sound playback when the device is set to silent. Suppress _playSound() on devices with maxTouchPoints > 1 (touch/mobile). Push notifications handle alerts on mobile and properly respect the device's silent mode. The side effect is that also laptops with touch screen are affected and need to use web push n
Original PR description
On touch devices (smartphones, tablets), the in-browser notification sound plays through the media audio channel, which is not affected by the device's silent mode. This causes unexpected sound playback when the device is set to silent. Suppress _playSound() on devices with maxTouchPoints > 1 (touch/mobile). Push notifications handle alerts on mobile and properly respect the device's silent mode. The side effect is that also laptops with touch screen are affected and need to use web push notifications in order to get notification sounds played. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276675 Forward-Port-Of: odoo/odoo#275797
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/d32e61b118 [REL] 19.4.3 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/a2ed1a6c65 [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) https://github.com/odoo/o-spreadsheet/commit/5a0ce53863 [FIX] TableComputedStylePlugin: skip table style for invalid pivot [Task: 6306263](https://www.odoo.com/odoo/2328/tasks/6
Original PR description
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/d32e61b118 [REL] 19.4.3 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0)…
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/d32e61b118 [REL] 19.4.3 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/a2ed1a6c65 [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) https://github.com/odoo/o-spreadsheet/commit/5a0ce53863 [FIX] TableComputedStylePlugin: skip table style for invalid pivot [Task: 6306263](https://www.odoo.com/odoo/2328/tasks/6306263) https://github.com/odoo/o-spreadsheet/commit/2a17559c32 [FIX] squisher: adapted ranges that become cells [Task: 6390727](https://www.odoo.com/odoo/2328/tasks/6390727) https://github.com/odoo/o-spreadsheet/commit/ca1b96ded1 [FIX] documentation: Adapt doc/example to owl3 [Task: 6384935](https://www.odoo.com/odoo/2328/tasks/6384935) https://github.com/odoo/o-spreadsheet/commit/5f36bffc85 [FIX] components: replace `props/plugin` with `useProps/usePlugin` [Task: 6384935](https://www.odoo.com/odoo/2328/tasks/6384935) https://github.com/odoo/o-spreadsheet/commit/9ba5f1954c [IMP] package: update to owl alpha 43 [Task: 6384935](https://www.odoo.com/odoo/2328/tasks/6384935) https://github.com/odoo/o-spreadsheet/commit/17a7db83ab [FIX] default: clear empty keys in cell style [Task: 6366056](https://www.odoo.com/odoo/2328/tasks/6366056) https://github.com/odoo/o-spreadsheet/commit/c6c38b110c [FIX] default: fix 1st edit issue [Task: 6366056](https://www.odoo.com/odoo/2328/tasks/6366056) https://github.com/odoo/o-spreadsheet/commit/413fc40344 [IMP] default_clipboard: enlarge content in instead of multiple [Task: 6366056](https://www.odoo.com/odoo/2328/tasks/6366056) 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>
Issue: ---------------------------------------- Opening the accrual form view returns a traceback. Cause: ---------------------------------------- This forwardport 13322323c533cc3e4bfc7346e893dcfd2a467ccb did not adapt to the new OWL3 way of getting variables (44ad878ac2338d14ac1c8f919d3b5702a266cf2c). Solution: ---------------------------------------- Use `this` when reading template variables. opw-6367235
Original PR description
Issue: ---------------------------------------- Opening the accrual form view returns a traceback. Cause: ---------------------------------------- This forwardport 13322323c533cc3e4bfc7346e893dcfd2a467ccb did not adapt to the new OWL3 way of getting variables (44ad878ac2338d14ac1c8f919d3b5702a266cf2c). Solution: ---------------------------------------- Use `this` when reading template variables. opw-6367235
Klipy requires attribution with "Search KLIPY" as the default placeholder [1]. This commit complies these guidelines. [1]: https://docs.klipy.com/attribution Forward-Port-Of: odoo/odoo#276714 Forward-Port-Of: odoo/odoo#275677
Original PR description
Klipy requires attribution with "Search KLIPY" as the default placeholder [1]. This commit complies these guidelines. [1]: https://docs.klipy.com/attribution Forward-Port-Of: odoo/odoo#276714 Forward-Port-Of: odoo/odoo#275677
Previously, the computation of `withholding_total_amount_currency` only considered taxes directly applied on invoice lines. When a grouped tax was used and one or more of its child taxes were withholding taxes, the withholding amount was not computed because only the parent tax was checked. With this commit, child taxes of grouped taxes are also taken into account when computing the total withholding amount. task-6391535 upg:https://github.com/odoo/upgrade/pull/10798
Original PR description
Previously, the computation of `withholding_total_amount_currency` only considered taxes directly applied on invoice lines. When a grouped tax was used and one or more of its child taxes were withholding taxes, the withholding amount was not computed because only the parent tax was checked. With this commit, child taxes of grouped taxes are also taken into account when computing the total withholding amount. task-6391535 upg:https://github.com/odoo/upgrade/pull/10798
Before this commit, when logging in as Demo user with a direct chat, logging out + logging in with Mitchell Admin would crash with the following error: ``` TypeError: can't access property "imStatusUI", this.channel.correspondent is undefined ``` This happens because this attempts to restore the direct chat in chat window. Normally this shouldn't happen thanks to being unable to fetch inaccessible channels, however Mitchell Admin is the admin of DB thus can fetch the channel data. The cr
Original PR description
Before this commit, when logging in as Demo user with a direct chat, logging out + logging in with Mitchell Admin would crash with the following error: ``` TypeError: can't access property…
Before this commit, when logging in as Demo user with a direct chat, logging out + logging in with Mitchell Admin would crash with the following error: ``` TypeError: can't access property "imStatusUI", this.channel.correspondent is undefined ``` This happens because this attempts to restore the direct chat in chat window. Normally this shouldn't happen thanks to being unable to fetch inaccessible channels, however Mitchell Admin is the admin of DB thus can fetch the channel data. The crash comes from computation of `correspondent` that is `undefined` because it can't determine a correspondent from the Point of view of Mitchell Admin, and the template isn't defensive against missing a `correspondent` in a direct chat. One solution could have been to support opening of direct chat, but this implies fixing a lot of complex issues, such as: - Mitchell Admin can't use composer because this attempts to add a member but there's a contraint for direct chat to have at most 2 members - This direct chat looks like a group chat without a name, we can guess this is a direct chat only because of broken avatar and the start message in message list stating this is a direct chat - Several other actions show weird behavior, like "Invite People" ignoring people in the chat This commit fixes the issue by ignoring the restore of chat in chat hub for direct chats that current user is not a member. Forward-Port-Of: odoo/odoo#277199
### STEPS TO REPRODUCE: 1. Install AI, Livechat, and Website 2. Open Livechat and create a new channel 3. Ensure there is a welcome message 4. Add a rule with any AI Agent 5. Navigate to Settings > Website, enable Live Chat, and select the channel you just created 6. Go to the Website, click on the chat bubble, and verify that the first message shows "unnamed" as the sender of the welcome message ### CAUSE When a default message exists, the `author_id` is resolved by looking at the cur
Original PR description
### STEPS TO REPRODUCE: 1. Install AI, Livechat, and Website 2. Open Livechat and create a new channel 3. Ensure there is a welcome message 4. Add a rule with any AI Agent 5. Navigate to Settings > Website, enable Live Chat, and select the channel you just created 6. Go to the Website, click on the chat bubble, and verify that the first message shows "unnamed" as the sender of the welcome message ### CAUSE When a default message exists, the `author_id` is resolved by looking at the current channel's history. However, it only checks `livechat_agent_history_ids`. Since a bot can also send the welcome message instead of an agent, `livechat_bot_history_ids` should also be checked. Forward-Port-Of: odoo/odoo#277086 Forward-Port-Of: odoo/odoo#273204
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through upsert_en()) can have their English source text changed by a module upgrade while keeping the same key. For example product.template's selection value 'consu' went from meaning "Consumable" to meaning "Goods" between 17.0 and 18.0, without the stored value itself changing. In that situation, upsert
Original PR description
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through…
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through upsert_en()) can have their English source text changed by a module upgrade while keeping the same key. For example product.template's selection value 'consu' went from meaning "Consumable" to meaning "Goods" between 17.0 and 18.0, without the stored value itself changing. In that situation, upsert_en() always refreshes the 'en_US' entry from the current code, so English is never wrong. But every other language keeps whatever value it had, since upsert_en() blindly merges the new 'en_US' entry into the existing jsonb without checking whether the previous 'en_US' value actually changed. When the module's .po file is later reloaded, TranslationImporter.save() finds a non-empty value already there for each language and, by design, does not overwrite it unless explicitly asked to (`overwrite`) on a record that isn't `noupdate`. That protection is correct when the English text hasn't changed (it keeps manual translation customizations across upgrades), but here it ends up protecting a translation of a source string that no longer exists. As a result, a value whose meaning changes across versions keeps showing its old label in every language except English, forever, unless someone forces a translation overwrite. Fix this in upsert_en() itself: when the incoming 'en_US' value differs from the one currently stored, drop every other language already stored for that row instead of merging into it. Those translations were made for a source string that no longer exists, so there is nothing worth protecting. Once dropped, they are simply missing from the jsonb, so TranslationImporter.save() naturally lets the freshly (re)loaded translations in on its next run, without needing to know anything about source drift, and `overwrite`/`noupdate` keep their exact original meaning for every other case. Task-6333080 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#277491 Forward-Port-Of: odoo/odoo#275080
15 changes
Resolved issues and error corrections
Steps to reproduce: ------------------------------------ 1. Install barcode_gs1_nomenclature Module. 2. Go to Inventory > Configuration > Barcode Nomenclature. 3. Open any barcode nomenclature. Observation: ------------------------------------ The "Is GS1 Nomenclature" field is visible in the list header. Issue: ------------------------------------ The field uses the invisible attribute, which hides the field values but does not hide the corresponding list header, resulting in an e
Original PR description
Steps to reproduce: ------------------------------------ 1. Install barcode_gs1_nomenclature Module. 2. Go to Inventory > Configuration > Barcode Nomenclature. 3. Open any barcode nomenclature. Observation: ------------------------------------ The "Is GS1 Nomenclature" field is visible in the list header. Issue: ------------------------------------ The field uses the invisible attribute, which hides the field values but does not hide the corresponding list header, resulting in an empty header column. Solution: ------------------------------------ Replace the 'invisible' attribute with 'column_invisible' so that both the field values and the corresponding list header are hidden. opw-6390731 Forward-Port-Of: odoo/odoo#276532
Before this commit, when logging in as Demo user with a direct chat, logging out + logging in with Mitchell Admin would crash with the following error: ``` TypeError: can't access property "imStatusUI", this.channel.correspondent is undefined ``` This happens because this attempts to restore the direct chat in chat window. Normally this shouldn't happen thanks to being unable to fetch inaccessible channels, however Mitchell Admin is the admin of DB thus can fetch the channel data. The cr
Original PR description
Before this commit, when logging in as Demo user with a direct chat, logging out + logging in with Mitchell Admin would crash with the following error: ``` TypeError: can't access property…
Before this commit, when logging in as Demo user with a direct chat, logging out + logging in with Mitchell Admin would crash with the following error: ``` TypeError: can't access property "imStatusUI", this.channel.correspondent is undefined ``` This happens because this attempts to restore the direct chat in chat window. Normally this shouldn't happen thanks to being unable to fetch inaccessible channels, however Mitchell Admin is the admin of DB thus can fetch the channel data. The crash comes from computation of `correspondent` that is `undefined` because it can't determine a correspondent from the Point of view of Mitchell Admin, and the template isn't defensive against missing a `correspondent` in a direct chat. One solution could have been to support opening of direct chat, but this implies fixing a lot of complex issues, such as: - Mitchell Admin can't use composer because this attempts to add a member but there's a contraint for direct chat to have at most 2 members - This direct chat looks like a group chat without a name, we can guess this is a direct chat only because of broken avatar and the start message in message list stating this is a direct chat - Several other actions show weird behavior, like "Invite People" ignoring people in the chat This commit fixes the issue by ignoring the restore of chat in chat hub for direct chats that current user is not a member.
The `account_journal_type.bank` KPI (surfaced by the Databases app as `account_journal_type_bank`, tooltip "Draft entries in journal Bank") is meant to report journal entries in bank journals that still need attention: drafts, posted-but-unchecked entries, and posted bank entries that are not yet reconciled. The "not yet reconciled" case was implemented as: ```LEFT JOIN account_bank_statement_line st_line ON move.statement_line_id = st_line.id AND (st_line.id IS NULL O
Original PR description
The `account_journal_type.bank` KPI (surfaced by the Databases app as `account_journal_type_bank`, tooltip "Draft entries in journal Bank") is meant to report journal entries in bank journals that…
The `account_journal_type.bank` KPI (surfaced by the Databases app as `account_journal_type_bank`, tooltip "Draft entries in journal Bank") is meant to report journal entries in bank journals that still need attention: drafts, posted-but-unchecked entries, and posted bank entries that are not yet reconciled.
The "not yet reconciled" case was implemented as:
```LEFT JOIN account_bank_statement_line st_line
ON move.statement_line_id = st_line.id
AND (st_line.id IS NULL OR NOT st_line.is_reconciled)```
`st_line.id IS NULL` does not only match unreconciled bank transactions, it also matches any posted move booked directly in a bank journal that never originated from an imported bank statement line (manual entries, bank fees, opening balances, ...). Those moves have no `statement_line_id` by design and can never be reconciled, so they were counted as "pending" forever, permanently inflating the KPI for any database with such entries.
Only require an actual bank statement line before flagging it as unreconciled, matching the logic already used for the "to reconcile" count on the journal dashboard (account_journal_dashboard.py).
opw-6199785
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#275597Login uniqueness is enforced by a `UNIQUE (login)` constraint that Postgres compares byte for byte, so signing up with foo@example.com and then Foo@example.com produces two separate accounts pointing at the same real mailbox. https://github.com/odoo/odoo/blob/66a6c16551041543b5addfe846f15e769b4e9afe/odoo/addons/base/models/res_users.py#L274 Even if the DB constraint did catch an exact-case duplicate and `_signup_create_user` re-raised it as a `SignupError`, the controller's friendly "already re
Original PR description
Login uniqueness is enforced by a `UNIQUE (login)` constraint that Postgres compares byte for byte, so signing up with foo@example.com and then Foo@example.com produces two separate accounts pointing…
Login uniqueness is enforced by a `UNIQUE (login)` constraint that Postgres compares byte for byte, so signing up with foo@example.com and then Foo@example.com produces two separate accounts pointing at the same real mailbox. https://github.com/odoo/odoo/blob/66a6c16551041543b5addfe846f15e769b4e9afe/odoo/addons/base/models/res_users.py#L274 Even if the DB constraint did catch an exact-case duplicate and `_signup_create_user` re-raised it as a `SignupError`, the controller's friendly "already registered" branch only triggers when the duplicate lookup finds a row, and that lookup goes through `_get_login_domain` with an exact `=` operator. Case variants would fall into the generic "Could not create a new account" branch instead. https://github.com/odoo/odoo/blob/66a6c16551041543b5addfe846f15e769b4e9afe/addons/auth_signup/controllers/main.py#L68-L75 https://github.com/odoo/odoo/blob/66a6c16551041543b5addfe846f15e769b4e9afe/odoo/addons/base/models/res_users.py#L749-L750 `_signup_create_user` now refuses creation when a user with the same email already exists, applying to both b2c free signup and token-based invitations. It raises `UserError` directly so the controller's `except UserError` surfaces the message without a redundant lookup. The check uses `_get_email_domain`, whose base implementation is switched from `=` to `=ilike` over a value escaped via `tools.escape_psql` so `%` and `_` are matched literally rather than as wildcards. Its only existing caller is `reset_password`, which already wants case-insensitive matching. Steps to reproduce: 1. In Settings, set Customer Account to "Free sign up" and save. 2. Log out, then on the login page click "Don't have an account?". 3. Register with foo@example.com. 4. Log out again and click "Don't have an account?". 5. Register with Foo@example.com. => Two distinct user accounts are created for the same mailbox. opw-6199441 Forward-Port-Of: odoo/odoo#273071 Forward-Port-Of: odoo/odoo#263864
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause: ====== The donation snippet posts its configuration (prefilled amounts, display options, descriptions) in the request body. If the user switches the website language on the payment page, it triggers a page reload via a GET request which drops the original form body (this also applies to
Original PR description
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause:…
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause: ====== The donation snippet posts its configuration (prefilled amounts, display options, descriptions) in the request body. If the user switches the website language on the payment page, it triggers a page reload via a GET request which drops the original form body (this also applies to a simple page refresh). As a result, the kwargs were empty and the page fell back to the default free-amount input, losing the configured options. Solution: ========= Implement Post/Redirect/Get: on POST, redirect to /donation/pay with the options in the query string so any later GET (language switch, refresh, iframe src reload) re-renders a fully configured page without losing data. Alternative Solution: ===================== We could also store the options in the session, but the current solution is much simpler and avoids session pollution. opw-6282391 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#271460
The [`_compute_short_url_host`](https://github.com/odoo/odoo/blob/a81fa8699c89385d2cccb260ada07255c6ea1275/addons/website_links/models/link_tracker.py#L20-L24) override builds link.tracker short URLs from `website.get_current_website()` and the current company's website domain. It was introduced by https://github.com/odoo/odoo/commit/f13ecb15af7f0bdc67e37dfb41e8bac77f5a541a for a multi-company backend flow (users switching companies to post social marketing links), but it runs for every compute,
Original PR description
The [`_compute_short_url_host`](https://github.com/odoo/odoo/blob/a81fa8699c89385d2cccb260ada07255c6ea1275/addons/website_links/models/link_tracker.py#L20-L24) override builds link.tracker short URLs…
The [`_compute_short_url_host`](https://github.com/odoo/odoo/blob/a81fa8699c89385d2cccb260ada07255c6ea1275/addons/website_links/models/link_tracker.py#L20-L24) override builds link.tracker short URLs from `website.get_current_website()` and the current company's website domain. It was introduced by https://github.com/odoo/odoo/commit/f13ecb15af7f0bdc67e37dfb41e8bac77f5a541a for a multi-company backend flow (users switching companies to post social marketing links), but it runs for every compute, including CRON contexts with no HTTP request such as the mass-mailing queue. Without a request, `get_current_website()` picks an arbitrary website (the first in the database) and `self.env.company` resolves to the user's main company, so the short URL uses that company's website domain instead of `web.base.url`. Fall back to `super()._compute_short_url_host()` (which uses `web.base.url`) when no website is resolvable from the request, session, or context. Backend flows with a real request still hit the company-aware branch. Steps to reproduce: 1. Install Email Marketing and Website. 2. Settings > Companies: create a second company B. Settings > Websites: ensure website A points to company A with domain A, and create website B for company B with domain B. 3. Settings > Technical > Parameters > System Parameters: set `web.base.url` to a third domain C, and add `web.base.url.freeze` = `True`. 4. On company A, Email Marketing: create a mailing with body `<a href="http://example.com">test</a>` and a recipient list, then click Send. 5. Settings > Technical > Automation > Scheduled Actions > "Mass Mailing: Process queue" > Run Manually. 6. Email Marketing > Configuration > Link Tracker: open the tracker generated for the mailing. => The Tracked URL uses domain A. => The Tracked URL uses domain C. Ticket [link](https://www.odoo.com/odoo/project.task/6038590) opw-6038590 Forward-Port-Of: odoo/odoo#259200
### STEPS TO REPRODUCE: 1. Install AI, Livechat, and Website 2. Open Livechat and create a new channel 3. Ensure there is a welcome message 4. Add a rule with any AI Agent 5. Navigate to Settings > Website, enable Live Chat, and select the channel you just created 6. Go to the Website, click on the chat bubble, and verify that the first message shows "unnamed" as the sender of the welcome message ### CAUSE When a default message exists, the `author_id` is resolved by looking at the cur
Original PR description
### STEPS TO REPRODUCE: 1. Install AI, Livechat, and Website 2. Open Livechat and create a new channel 3. Ensure there is a welcome message 4. Add a rule with any AI Agent 5. Navigate to Settings > Website, enable Live Chat, and select the channel you just created 6. Go to the Website, click on the chat bubble, and verify that the first message shows "unnamed" as the sender of the welcome message ### CAUSE When a default message exists, the `author_id` is resolved by looking at the current channel's history. However, it only checks `livechat_agent_history_ids`. Since a bot can also send the welcome message instead of an agent, `livechat_bot_history_ids` should also be checked. Forward-Port-Of: odoo/odoo#273204
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create Invoices` > select `Regular invoice` > `Create Draft`. (leave the invoice in the draft state). - Navigate to Accounting > Review > Invoiced Not Delivered. - Select the newly created invoice and click `Create Accrual Entries`. **Error:** `ZeroDivisionError: division by zero` **Root Ca
Original PR description
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create…
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create Invoices` > select `Regular invoice` > `Create Draft`. (leave the invoice in the draft state). - Navigate to Accounting > Review > Invoiced Not Delivered. - Select the newly created invoice and click `Create Accrual Entries`. **Error:** `ZeroDivisionError: division by zero` **Root Cause:** At [1], `processed_qty` is initialized with `0`. If all the selected invoices are in the draft state, the code never enters the for loop, so `processed_qty` remains 0. Later, it attempts to divide the `amount` by `processed_qty`, causing an error. **Fix:** This commit prevents errors when creating accrual entries for draft invoices. [1]: https://github.com/odoo/odoo/blob/a7d285a833f5b1d47a11a1b8ec31e3730c1c854c/addons/account/wizard/accrued_orders.py#L258-L265 opw-6332652 Forward-Port-Of: odoo/odoo#273330
This fixes two bugs in the web push subscription flow: - register_devices() compared partner records with 'is not' instead of '!='. Records loaded via sudo() live in a different environment than self.env.user, so 'is not' was always True and the ownership guard never behaved as intended. Use '!=', which compares record identity by model and id as Odoo's ORM intends. - webclient.js sent the previous subscription endpoint under the snake_case key 'previous_endpoint', while the server reads i
Original PR description
This fixes two bugs in the web push subscription flow:
- register_devices() compared partner records with 'is not' instead of '!='. Records loaded via sudo() live in a different environment than self.env.user, so 'is not' was always True and the ownership guard never behaved as intended. Use '!=', which compares record identity by model and id as Odoo's ORM intends.
- webclient.js sent the previous subscription endpoint under the snake_case key 'previous_endpoint', while the server reads it as 'previousEndpoint' (kw.get('previousEndpoint', endpoint)). The mismatch meant the lookup always fell back to the new endpoint, so a refreshed subscription created a duplicate device instead of updating the existing one. Send the camelCase key to match the server.
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#276082Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through upsert_en()) can have their English source text changed by a module upgrade while keeping the same key. For example product.template's selection value 'consu' went from meaning "Consumable" to meaning "Goods" between 17.0 and 18.0, without the stored value itself changing. In that situation, upsert
Original PR description
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through…
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through upsert_en()) can have their English source text changed by a module upgrade while keeping the same key. For example product.template's selection value 'consu' went from meaning "Consumable" to meaning "Goods" between 17.0 and 18.0, without the stored value itself changing. In that situation, upsert_en() always refreshes the 'en_US' entry from the current code, so English is never wrong. But every other language keeps whatever value it had, since upsert_en() blindly merges the new 'en_US' entry into the existing jsonb without checking whether the previous 'en_US' value actually changed. When the module's .po file is later reloaded, TranslationImporter.save() finds a non-empty value already there for each language and, by design, does not overwrite it unless explicitly asked to (`overwrite`) on a record that isn't `noupdate`. That protection is correct when the English text hasn't changed (it keeps manual translation customizations across upgrades), but here it ends up protecting a translation of a source string that no longer exists. As a result, a value whose meaning changes across versions keeps showing its old label in every language except English, forever, unless someone forces a translation overwrite. Fix this in upsert_en() itself: when the incoming 'en_US' value differs from the one currently stored, drop every other language already stored for that row instead of merging into it. Those translations were made for a source string that no longer exists, so there is nothing worth protecting. Once dropped, they are simply missing from the jsonb, so TranslationImporter.save() naturally lets the freshly (re)loaded translations in on its next run, without needing to know anything about source drift, and `overwrite`/`noupdate` keep their exact original meaning for every other case. Task-6333080 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276725 Forward-Port-Of: odoo/odoo#275080
Issue: - After confirming a sale order with a reward applied (e.g. an ewallet reward), unlocking it, and editing it so the reward line's cost changes (e.g. adding a product and re-claiming the reward), the coupon's point balance updates correctly. - However, the corresponding loyalty.history record's used value is never refreshed, so it keeps showing the old cost instead of the new one. Steps to reproduce: - Enable Loyalty and Lock Confirmed Sales in Sales settings. - Create an ewallet ty
Original PR description
Issue: - After confirming a sale order with a reward applied (e.g. an ewallet reward), unlocking it, and editing it so the reward line's cost changes (e.g. adding a product and re-claiming the…
Issue: - After confirming a sale order with a reward applied (e.g. an ewallet reward), unlocking it, and editing it so the reward line's cost changes (e.g. adding a product and re-claiming the reward), the coupon's point balance updates correctly. - However, the corresponding loyalty.history record's used value is never refreshed, so it keeps showing the old cost instead of the new one. Steps to reproduce: - Enable Loyalty and Lock Confirmed Sales in Sales settings. - Create an ewallet type loyalty.program and generate an ewallet for a partner with e.g. 1000 points. - Create a sale order for that partner, add a product worth 100, and claim the ewallet reward (reward line created with points_cost = 100). - Confirm the order. loyalty.history shows used = 100 (correct), card balance shows 900 (correct). - Unlock the order, add a second product worth 100, and claim the reward again (same reward line updates to points_cost = 200). - Lock the order again. - Check the loyalty.card: balance is correctly 800. - Check loyalty.history for that order: used still shows 100 instead of 200. Fix: - Updated _update_loyalty_history() in sale_order.py to create a new history line if none exists for the given card and order combination. - Updated write() in sale_order_line.py to correctly sync history lines when a reward line is modified on a confirmed order, handling both same-coupon updates via delta and coupon changes by subtracting the old coupon cost and adding the new one separately. - Added test_loyalty_history_created_on_post_confirm_reward to verify that a history line is created when a reward is claimed on a confirmed order where no history line existed before. - Added test_loyalty_history_updated_on_points_cost_write to verify that history.used is updated by the correct delta when points_cost changes on a reward line of a confirmed order. Impact: - Ensures a coupon's usage history stays accurate after a confirmed order is unlocked and edited. - Prevents the loyalty.history used field from silently going stale while the actual point balance is correct. - Not scoped to ewallet specifically — since points_cost semantics are the same across program types, this also corrects the same class of staleness for other reward types (discount, gift_card, etc.) when a reward line's cost changes post-confirmation. Forward-Port-Of: odoo/odoo#276961 Forward-Port-Of: odoo/odoo#273688
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce email from will default to the bounce alias of the main company of the DB. In a multi company setup, where each company might have their own mail alias domain, this can be confusing for the receiving party that didn't send the original email to the main company's alias domain. ## Propo
Original PR description
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce…
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce email from will default to the bounce alias of the main company of the DB. In a multi company setup, where each company might have their own mail alias domain, this can be confusing for the receiving party that didn't send the original email to the main company's alias domain. ## Proposed solution: We call `_routing_create_bounce_email` with the right company in the context, since the journal company is already detected. Additionally, in the context of the accounting code calling the bounce private method, we pass an explicit value for `reply_to` ## How to reproduce it: 1) Set up DB with two companies (companyA and companyB). Install account. 2) Set up two mail alias domains (companya.com and companyb.com) 3) Send an email to the invoice alias of companyB (e.g. invoices@companyb.com). -> bounce email will have FROM as "notifications@companya.com" (the notifications alias of companyA) Example headers before: ``` Reply-To: "YourCompany" <catchall@companya.com> To: customer@example.com Date: Thu, 02 Jul 2026 11:18:49 -0000 Return-Path: bounce@companya.com From: "MAILER-DAEMON" <bounce@companya.com> ``` After fix: ``` Reply-To: info@companyb.com To: customer@example.com Date: Thu, 02 Jul 2026 11:16:19 -0000 Return-Path: bounce@companyb.com From: "MAILER-DAEMON" <notifications@companyb.com> ``` opw-6342778 Forward-Port-Of: odoo/odoo#273671
On touch devices (smartphones, tablets), the in-browser notification sound plays through the media audio channel, which is not affected by the device's silent mode. This causes unexpected sound playback when the device is set to silent. Suppress _playSound() on devices with maxTouchPoints > 1 (touch/mobile). Push notifications handle alerts on mobile and properly respect the device's silent mode. The side effect is that also laptops with touch screen are affected and need to use web push n
Original PR description
On touch devices (smartphones, tablets), the in-browser notification sound plays through the media audio channel, which is not affected by the device's silent mode. This causes unexpected sound playback when the device is set to silent. Suppress _playSound() on devices with maxTouchPoints > 1 (touch/mobile). Push notifications handle alerts on mobile and properly respect the device's silent mode. The side effect is that also laptops with touch screen are affected and need to use web push notifications in order to get notification sounds played. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276675 Forward-Port-Of: odoo/odoo#275797
**Steps to reproduce:** 1. Create a Sales Order. 2. Create a 50% down payment invoice. 3. Create a credit note for the down payment invoice. 4. Reset the credit note to Draft and cancel it. 5. Create the final invoice from the Sales Order. **Issue:** The final invoice is generated for 100% of the order amount, acting as if the down payment invoice does not exist. **Expected behavior:** The final invoice should only include the remaining 50% of the order amount because a valid 50% do
Original PR description
**Steps to reproduce:** 1. Create a Sales Order. 2. Create a 50% down payment invoice. 3. Create a credit note for the down payment invoice. 4. Reset the credit note to Draft and cancel it. 5. Create…
**Steps to reproduce:** 1. Create a Sales Order. 2. Create a 50% down payment invoice. 3. Create a credit note for the down payment invoice. 4. Reset the credit note to Draft and cancel it. 5. Create the final invoice from the Sales Order. **Issue:** The final invoice is generated for 100% of the order amount, acting as if the down payment invoice does not exist. **Expected behavior:** The final invoice should only include the remaining 50% of the order amount because a valid 50% down payment invoice still exists. **Why this happens:** - The `price_unit` on the Sales Order's down payment line is manually updated during `action_post()` based on the sum of posted invoices minus posted credit notes. - When the credit note is posted, `price_unit` drops to 0. However, when that credit note is subsequently reset to draft and cancelled, it triggers `button_cancel()` which only refreshed the line's display name and failed to recalculate `price_unit`. As a result, `price_unit` remained at 0 even though the credit note was no longer active, causing the final invoice to deduct nothing. opw-6373578 Forward-Port-Of: odoo/odoo#277043 Forward-Port-Of: odoo/odoo#275684
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/2b42707d52 [REL] 19.3.12 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/d7d612ef1c [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) https://github.com/odoo/o-spreadsheet/commit/67399059e8 [FIX] TableComputedStylePlugin: skip table style for invalid pivot [Task: 6306263](https://www.odoo.com/odoo/2328/tasks/
Original PR description
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/2b42707d52 [REL] 19.3.12 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0)…
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/2b42707d52 [REL] 19.3.12 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/d7d612ef1c [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) https://github.com/odoo/o-spreadsheet/commit/67399059e8 [FIX] TableComputedStylePlugin: skip table style for invalid pivot [Task: 6306263](https://www.odoo.com/odoo/2328/tasks/6306263) https://github.com/odoo/o-spreadsheet/commit/cb96765814 [FIX] squisher: adapted ranges that become cells [Task: 6390727](https://www.odoo.com/odoo/2328/tasks/6390727) https://github.com/odoo/o-spreadsheet/commit/1e513b5a3e [FIX] index: export `positionMap` [Task: 6159213](https://www.odoo.com/odoo/2328/tasks/6159213) https://github.com/odoo/o-spreadsheet/commit/3bcd6758a2 [FIX] Pivots: cache `getPivotIdFromPosition` for performance [Task: 6159213](https://www.odoo.com/odoo/2328/tasks/6159213) 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>
15 changes
Resolved issues and error corrections
Add a regression test ensuring that setting a relative date global filter on a dynamic Odoo pivot does not crash while the pivot table is reloading. Task: 6306263 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Original PR description
Add a regression test ensuring that setting a relative date global filter on a dynamic Odoo pivot does not crash while the pivot table is reloading. Task: 6306263 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Rather than assign `log_target` and blow up if the third branch is taken, use a `defaultdict` and just increment the count in each branch on hit. The continue in the third branch is not strictly necessary, but that way it protects us if anyone decides to add post-processing which also doesn't account for the third branch. Forward-Port-Of: odoo/odoo#276757 Forward-Port-Of: odoo/odoo#276481
Original PR description
Rather than assign `log_target` and blow up if the third branch is taken, use a `defaultdict` and just increment the count in each branch on hit. The continue in the third branch is not strictly necessary, but that way it protects us if anyone decides to add post-processing which also doesn't account for the third branch. Forward-Port-Of: odoo/odoo#276757 Forward-Port-Of: odoo/odoo#276481
Steps to reproduce --- 1. Install a carrier that requires a positive package shipping weight (e.g. UPS) and enable Packages. 2. From the Barcode app, process a delivery and Put in Pack the products. 3. Validate the transfer. Issue --- Validation is refused because the package has no positive shipping weight, even though its computed weight is correct. The package shipping weight is only set in `_post_put_in_pack_hook` from `context['weight']`, which is filled exclusively by the put i
Original PR description
Steps to reproduce --- 1. Install a carrier that requires a positive package shipping weight (e.g. UPS) and enable Packages. 2. From the Barcode app, process a delivery and Put in Pack the products.…
Steps to reproduce --- 1. Install a carrier that requires a positive package shipping weight (e.g. UPS) and enable Packages. 2. From the Barcode app, process a delivery and Put in Pack the products. 3. Validate the transfer. Issue --- Validation is refused because the package has no positive shipping weight, even though its computed weight is correct. The package shipping weight is only set in `_post_put_in_pack_hook` from `context['weight']`, which is filled exclusively by the put in pack wizard. Flows that bypass the wizard, like the Barcode app, never provide that key, so the stored `shipping_weight` stays 0 while carriers reading it directly reject the delivery. The Barcode app suppresses the wizard through the `barcode_view` context, so its put in pack goes straight to the hook without any weight. https://github.com/odoo/enterprise/blob/6b18215bfa98ef636b63f4fafbba1d25264f4883/stock_barcode/models/stock_move_line.py#L193-L196 This handling was centralized in this hook by 9a2ff6f4033e, so every wizard-bypassed pack leaves the field empty. Defaulting it to the package computed weight via `_get_weight` when the context did not provide one makes the stored value correct for any carrier, independently of the flow that created the package. https://github.com/odoo/odoo/blob/4235b48c86077bd5bceb9e817cc45b2eec8697e8/addons/stock_delivery/models/stock_move.py#L111-L115 opw-6297689 Forward-Port-Of: odoo/odoo#273876 Forward-Port-Of: odoo/odoo#272510
Steps to reproduce 1. Set the user timezone to one with a positive UTC offset, e.g. Europe/Brussels. 2. Open Payroll > Work Entries in pivot view and apply the built-in "Current Month" filter. 3. Expand an employee row and group by Date > Day. Expected: only days of the current month are visible (April 1..30 at the time of writing). Actual: the last day of the previous month (March 31) is included and the last day of the current month (April 30) is missing — the whole window
Original PR description
Steps to reproduce 1. Set the user timezone to one with a positive UTC offset, e.g. Europe/Brussels. 2. Open Payroll > Work Entries in pivot view and apply the built-in "Current Month" filter. 3.…
Steps to reproduce 1. Set the user timezone to one with a positive UTC offset, e.g. Europe/Brussels. 2. Open Payroll > Work Entries in pivot view and apply the built-in "Current Month" filter. 3. Expand an employee row and group by Date > Day. Expected: only days of the current month are visible (April 1..30 at the time of writing). Actual: the last day of the previous month (March 31) is included and the last day of the current month (April 30) is missing — the whole window is shifted one day earlier. Issue The relative-date DSL added in https://github.com/odoo/odoo/commit/d1ea43f6721116914762ea323d8a5987f043e87f operates on a tz-aware datetime starting from `now` when no explicit anchor is given. With the filter at https://github.com/odoo/odoo/blob/d58f4ed332af35f6de26a93f07adf05368731e20/addons/hr_work_entry/views/hr_work_entry_views.xml#L190-L192, `=1d` resets the clock to 00:00 and the day to the 1st in the user's timezone, evaluating to `2026-04-01 00:00:00+02:00` for Brussels. `parse_date` then normalizes that to UTC-naive `2026-03-31 22:00:00` (see https://github.com/odoo/odoo/blob/d58f4ed332af35f6de26a93f07adf05368731e20/odoo/tools/date_utils.py#L196-L199). When the ORM compares this to the `fields.Date` column `date` it truncates via `datetime.date()` (see https://github.com/odoo/odoo/blob/d58f4ed332af35f6de26a93f07adf05368731e20/odoo/orm/fields_temporal.py#L138-L158), yielding `2026-03-31`. The effective domain becomes `[2026-03-31, 2026-04-30)` instead of `[2026-04-01, 2026-05-01)`, which is why March 31 leaks in and April 30 disappears. Solution Anchor the filter to `today` instead of the implicit `now`. The DSL documents `today` as a starting point that yields a `date` value (see https://github.com/odoo/odoo/blob/d58f4ed332af35f6de26a93f07adf05368731e20/odoo/tools/date_utils.py#L108-L144), so `today =1d` resolves to the first of the month as a calendar date end-to-end and never goes through the tz-aware datetime → UTC-naive roundtrip that was silently shifting the window. opw-6123753 Forward-Port-Of: odoo/odoo#274105 Forward-Port-Of: odoo/odoo#260727
In the Outlook calendar sync, it's possible to have the same attendee twice. That's because the normalized email wasn't used to check for preexisting attendee. To reproduce, sync event with the organizer, also an attendee, using high case in the email such as: ORGANIZER: Mike@organizer.com ATTENDEE: Mike@organizer.com ATTENDEE: John@attendee.com opw-6186606 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#272258 For
Original PR description
In the Outlook calendar sync, it's possible to have the same attendee twice. That's because the normalized email wasn't used to check for preexisting attendee. To reproduce, sync event with the organizer, also an attendee, using high case in the email such as: ORGANIZER: Mike@organizer.com ATTENDEE: Mike@organizer.com ATTENDEE: John@attendee.com opw-6186606 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#272258 Forward-Port-Of: odoo/odoo#268201
From versions 18.3 to 19.3, company_id and siret refer to the same field. This caused an issue when connecting to the PDP using siret or siren could overwrite company_id. task-6327304 Forward-Port-Of: odoo/odoo#276805 Forward-Port-Of: odoo/odoo#275276
Original PR description
From versions 18.3 to 19.3, company_id and siret refer to the same field. This caused an issue when connecting to the PDP using siret or siren could overwrite company_id. task-6327304 Forward-Port-Of: odoo/odoo#276805 Forward-Port-Of: odoo/odoo#275276
Steps to reproduce: ------------------------------------ 1. Install barcode_gs1_nomenclature Module. 2. Go to Inventory > Configuration > Barcode Nomenclature. 3. Open any barcode nomenclature. Observation: ------------------------------------ The "Is GS1 Nomenclature" field is visible in the list header. Issue: ------------------------------------ The field uses the invisible attribute, which hides the field values but does not hide the corresponding list header, resulting in an e
Original PR description
Steps to reproduce: ------------------------------------ 1. Install barcode_gs1_nomenclature Module. 2. Go to Inventory > Configuration > Barcode Nomenclature. 3. Open any barcode nomenclature. Observation: ------------------------------------ The "Is GS1 Nomenclature" field is visible in the list header. Issue: ------------------------------------ The field uses the invisible attribute, which hides the field values but does not hide the corresponding list header, resulting in an empty header column. Solution: ------------------------------------ Replace the 'invisible' attribute with 'column_invisible' so that both the field values and the corresponding list header are hidden. opw-6390731 Forward-Port-Of: odoo/odoo#276532
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create Invoices` > select `Regular invoice` > `Create Draft`. (leave the invoice in the draft state). - Navigate to Accounting > Review > Invoiced Not Delivered. - Select the newly created invoice and click `Create Accrual Entries`. **Error:** `ZeroDivisionError: division by zero` **Root Ca
Original PR description
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create…
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create Invoices` > select `Regular invoice` > `Create Draft`. (leave the invoice in the draft state). - Navigate to Accounting > Review > Invoiced Not Delivered. - Select the newly created invoice and click `Create Accrual Entries`. **Error:** `ZeroDivisionError: division by zero` **Root Cause:** At [1], `processed_qty` is initialized with `0`. If all the selected invoices are in the draft state, the code never enters the for loop, so `processed_qty` remains 0. Later, it attempts to divide the `amount` by `processed_qty`, causing an error. **Fix:** This commit prevents errors when creating accrual entries for draft invoices. [1]: https://github.com/odoo/odoo/blob/a7d285a833f5b1d47a11a1b8ec31e3730c1c854c/addons/account/wizard/accrued_orders.py#L258-L265 opw-6332652 Forward-Port-Of: odoo/odoo#273330
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause: ====== The donation snippet posts its configuration (prefilled amounts, display options, descriptions) in the request body. If the user switches the website language on the payment page, it triggers a page reload via a GET request which drops the original form body (this also applies to
Original PR description
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause:…
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause: ====== The donation snippet posts its configuration (prefilled amounts, display options, descriptions) in the request body. If the user switches the website language on the payment page, it triggers a page reload via a GET request which drops the original form body (this also applies to a simple page refresh). As a result, the kwargs were empty and the page fell back to the default free-amount input, losing the configured options. Solution: ========= Implement Post/Redirect/Get: on POST, redirect to /donation/pay with the options in the query string so any later GET (language switch, refresh, iframe src reload) re-renders a fully configured page without losing data. Alternative Solution: ===================== We could also store the options in the session, but the current solution is much simpler and avoids session pollution. opw-6282391 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#271460
Issue: - After confirming a sale order with a reward applied (e.g. an ewallet reward), unlocking it, and editing it so the reward line's cost changes (e.g. adding a product and re-claiming the reward), the coupon's point balance updates correctly. - However, the corresponding loyalty.history record's used value is never refreshed, so it keeps showing the old cost instead of the new one. Steps to reproduce: - Enable Loyalty and Lock Confirmed Sales in Sales settings. - Create an ewallet ty
Original PR description
Issue: - After confirming a sale order with a reward applied (e.g. an ewallet reward), unlocking it, and editing it so the reward line's cost changes (e.g. adding a product and re-claiming the…
Issue: - After confirming a sale order with a reward applied (e.g. an ewallet reward), unlocking it, and editing it so the reward line's cost changes (e.g. adding a product and re-claiming the reward), the coupon's point balance updates correctly. - However, the corresponding loyalty.history record's used value is never refreshed, so it keeps showing the old cost instead of the new one. Steps to reproduce: - Enable Loyalty and Lock Confirmed Sales in Sales settings. - Create an ewallet type loyalty.program and generate an ewallet for a partner with e.g. 1000 points. - Create a sale order for that partner, add a product worth 100, and claim the ewallet reward (reward line created with points_cost = 100). - Confirm the order. loyalty.history shows used = 100 (correct), card balance shows 900 (correct). - Unlock the order, add a second product worth 100, and claim the reward again (same reward line updates to points_cost = 200). - Lock the order again. - Check the loyalty.card: balance is correctly 800. - Check loyalty.history for that order: used still shows 100 instead of 200. Fix: - Updated _update_loyalty_history() in sale_order.py to create a new history line if none exists for the given card and order combination. - Updated write() in sale_order_line.py to correctly sync history lines when a reward line is modified on a confirmed order, handling both same-coupon updates via delta and coupon changes by subtracting the old coupon cost and adding the new one separately. - Added test_loyalty_history_created_on_post_confirm_reward to verify that a history line is created when a reward is claimed on a confirmed order where no history line existed before. - Added test_loyalty_history_updated_on_points_cost_write to verify that history.used is updated by the correct delta when points_cost changes on a reward line of a confirmed order. Impact: - Ensures a coupon's usage history stays accurate after a confirmed order is unlocked and edited. - Prevents the loyalty.history used field from silently going stale while the actual point balance is correct. - Not scoped to ewallet specifically — since points_cost semantics are the same across program types, this also corrects the same class of staleness for other reward types (discount, gift_card, etc.) when a reward line's cost changes post-confirmation. Forward-Port-Of: odoo/odoo#276961 Forward-Port-Of: odoo/odoo#273688
On touch devices (smartphones, tablets), the in-browser notification sound plays through the media audio channel, which is not affected by the device's silent mode. This causes unexpected sound playback when the device is set to silent. Suppress _playSound() on devices with maxTouchPoints > 1 (touch/mobile). Push notifications handle alerts on mobile and properly respect the device's silent mode. The side effect is that also laptops with touch screen are affected and need to use web push n
Original PR description
On touch devices (smartphones, tablets), the in-browser notification sound plays through the media audio channel, which is not affected by the device's silent mode. This causes unexpected sound playback when the device is set to silent. Suppress _playSound() on devices with maxTouchPoints > 1 (touch/mobile). Push notifications handle alerts on mobile and properly respect the device's silent mode. The side effect is that also laptops with touch screen are affected and need to use web push notifications in order to get notification sounds played. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276675 Forward-Port-Of: odoo/odoo#275797
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through upsert_en()) can have their English source text changed by a module upgrade while keeping the same key. For example product.template's selection value 'consu' went from meaning "Consumable" to meaning "Goods" between 17.0 and 18.0, without the stored value itself changing. In that situation, upsert
Original PR description
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through…
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through upsert_en()) can have their English source text changed by a module upgrade while keeping the same key. For example product.template's selection value 'consu' went from meaning "Consumable" to meaning "Goods" between 17.0 and 18.0, without the stored value itself changing. In that situation, upsert_en() always refreshes the 'en_US' entry from the current code, so English is never wrong. But every other language keeps whatever value it had, since upsert_en() blindly merges the new 'en_US' entry into the existing jsonb without checking whether the previous 'en_US' value actually changed. When the module's .po file is later reloaded, TranslationImporter.save() finds a non-empty value already there for each language and, by design, does not overwrite it unless explicitly asked to (`overwrite`) on a record that isn't `noupdate`. That protection is correct when the English text hasn't changed (it keeps manual translation customizations across upgrades), but here it ends up protecting a translation of a source string that no longer exists. As a result, a value whose meaning changes across versions keeps showing its old label in every language except English, forever, unless someone forces a translation overwrite. Fix this in upsert_en() itself: when the incoming 'en_US' value differs from the one currently stored, drop every other language already stored for that row instead of merging into it. Those translations were made for a source string that no longer exists, so there is nothing worth protecting. Once dropped, they are simply missing from the jsonb, so TranslationImporter.save() naturally lets the freshly (re)loaded translations in on its next run, without needing to know anything about source drift, and `overwrite`/`noupdate` keep their exact original meaning for every other case. Task-6333080 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276725 Forward-Port-Of: odoo/odoo#275080
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/fba73fe75b [REL] 19.2.21 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/0266f73f3b [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) https://github.com/odoo/o-spreadsheet/commit/5bb486c814 [FIX] TableComputedStylePlugin: skip table style for invalid pivot [Task: 6306263](https://www.odoo.com/odoo/2328/tasks/
Original PR description
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/fba73fe75b [REL] 19.2.21 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0)…
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/fba73fe75b [REL] 19.2.21 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/0266f73f3b [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) https://github.com/odoo/o-spreadsheet/commit/5bb486c814 [FIX] TableComputedStylePlugin: skip table style for invalid pivot [Task: 6306263](https://www.odoo.com/odoo/2328/tasks/6306263) https://github.com/odoo/o-spreadsheet/commit/ecef954c0e [FIX] squisher: adapted ranges that become cells [Task: 6390727](https://www.odoo.com/odoo/2328/tasks/6390727) https://github.com/odoo/o-spreadsheet/commit/43bc2d9e1d [FIX] index: export `positionMap` [Task: 6159213](https://www.odoo.com/odoo/2328/tasks/6159213) https://github.com/odoo/o-spreadsheet/commit/1584d3c4b1 [FIX] Pivots: cache `getPivotIdFromPosition` for performance [Task: 6159213](https://www.odoo.com/odoo/2328/tasks/6159213) 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>
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce email from will default to the bounce alias of the main company of the DB. In a multi company setup, where each company might have their own mail alias domain, this can be confusing for the receiving party that didn't send the original email to the main company's alias domain. ## Propo
Original PR description
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce…
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce email from will default to the bounce alias of the main company of the DB. In a multi company setup, where each company might have their own mail alias domain, this can be confusing for the receiving party that didn't send the original email to the main company's alias domain. ## Proposed solution: We call `_routing_create_bounce_email` with the right company in the context, since the journal company is already detected. Additionally, in the context of the accounting code calling the bounce private method, we pass an explicit value for `reply_to` ## How to reproduce it: 1) Set up DB with two companies (companyA and companyB). Install account. 2) Set up two mail alias domains (companya.com and companyb.com) 3) Send an email to the invoice alias of companyB (e.g. invoices@companyb.com). -> bounce email will have FROM as "notifications@companya.com" (the notifications alias of companyA) Example headers before: ``` Reply-To: "YourCompany" <catchall@companya.com> To: customer@example.com Date: Thu, 02 Jul 2026 11:18:49 -0000 Return-Path: bounce@companya.com From: "MAILER-DAEMON" <bounce@companya.com> ``` After fix: ``` Reply-To: info@companyb.com To: customer@example.com Date: Thu, 02 Jul 2026 11:16:19 -0000 Return-Path: bounce@companyb.com From: "MAILER-DAEMON" <notifications@companyb.com> ``` opw-6342778 Forward-Port-Of: odoo/odoo#273671
The reversal wizard always injected a shared 'R4' onto every credit note it created, so a credit note of a simplified invoice never got the required 'R5', and reversing several invoices of mixed simplified status at once forced the same reason onto all of them. The wizard now derives the reason per original move ('R5' if it is simplified, 'R4' otherwise) instead of using one shared value, while still respecting an explicit manual choice. `l10n_es_is_simplified` already freezes correctly
Original PR description
The reversal wizard always injected a shared 'R4' onto every credit
note it created, so a credit note of a simplified invoice never got
the required 'R5', and reversing several invoices of mixed simplified
status at once forced the same reason onto all of them.
The wizard now derives the reason per original move ('R5' if it is
simplified, 'R4' otherwise) instead of using one shared value, while
still respecting an explicit manual choice. `l10n_es_is_simplified`
already freezes correctly on the credit note via its existing compute,
so no new compute is needed on `l10n_es_edi_verifactu_refund_reason`.
task-6358684
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#27618718 changes
Resolved issues and error corrections
Steps to reproduce: 1. Drop the Website Form snippet. 2. Add a checkbox field. 3. Change the label position to Top. > The Default Value option disappears. Cause: The `applyTo` selector relied on the `.col-sm` wrapper, which is only present for left/right label positions. As a result, it did not match checkbox fields with Top or None labels. This commit fix the applyTo selector so the Default Value option is displayed for checkbox fields regardless of the selected label position.
Original PR description
Steps to reproduce: 1. Drop the Website Form snippet. 2. Add a checkbox field. 3. Change the label position to Top. > The Default Value option disappears. Cause: The `applyTo` selector relied on the `.col-sm` wrapper, which is only present for left/right label positions. As a result, it did not match checkbox fields with Top or None labels. This commit fix the applyTo selector so the Default Value option is displayed for checkbox fields regardless of the selected label position. task-6373796 Forward-Port-Of: odoo/odoo#275823
### [FIX] web: fix mocked dates in tests Dates are mocked in all unit tests by default; either to a default static value, or to one defined in the test/suite. To do so, the `window.Date` constructor is overridden to have the arguments auto-filled with the current mock date paramters. Before this commit, an additional offset was always added to the given arguments to reflect: - the time elapsed from the beginning of the test; - any virtual offset added by a helper such as 'advanceTime
Original PR description
### [FIX] web: fix mocked dates in tests Dates are mocked in all unit tests by default; either to a default static value, or to one defined in the test/suite. To do so, the `window.Date` constructor…
### [FIX] web: fix mocked dates in tests Dates are mocked in all unit tests by default; either to a default static value, or to one defined in the test/suite. To do so, the `window.Date` constructor is overridden to have the arguments auto-filled with the current mock date paramters. Before this commit, an additional offset was always added to the given arguments to reflect: - the time elapsed from the beginning of the test; - any virtual offset added by a helper such as 'advanceTime'. The issue is that the spec of the Date constructor wants that the returned date object has to match all given arguments, and any omitted argument will be defaulted to the current date/time. This was not the case in tests, as the offset was always added, regardless of the given arguments. With this commit: only the arguments that are NOT given and that have been defaulted to current (mocked) date/time will be offset by the adequate value. Furthermore: as these mocked parameters are meant to reflect UTC values, the offset now also considers the *actual* browser offset to generate a local date from the mock date parameters. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#277041 Forward-Port-Of: odoo/odoo#275391
**Steps to reproduce:** 1. Create a Sales Order. 2. Create a 50% down payment invoice. 3. Create a credit note for the down payment invoice. 4. Reset the credit note to Draft and cancel it. 5. Create the final invoice from the Sales Order. **Issue:** The final invoice is generated for 100% of the order amount, acting as if the down payment invoice does not exist. **Expected behavior:** The final invoice should only include the remaining 50% of the order amount because a valid 50% do
Original PR description
**Steps to reproduce:** 1. Create a Sales Order. 2. Create a 50% down payment invoice. 3. Create a credit note for the down payment invoice. 4. Reset the credit note to Draft and cancel it. 5. Create…
**Steps to reproduce:** 1. Create a Sales Order. 2. Create a 50% down payment invoice. 3. Create a credit note for the down payment invoice. 4. Reset the credit note to Draft and cancel it. 5. Create the final invoice from the Sales Order. **Issue:** The final invoice is generated for 100% of the order amount, acting as if the down payment invoice does not exist. **Expected behavior:** The final invoice should only include the remaining 50% of the order amount because a valid 50% down payment invoice still exists. **Why this happens:** - The `price_unit` on the Sales Order's down payment line is manually updated during `action_post()` based on the sum of posted invoices minus posted credit notes. - When the credit note is posted, `price_unit` drops to 0. However, when that credit note is subsequently reset to draft and cancelled, it triggers `button_cancel()` which only refreshed the line's display name and failed to recalculate `price_unit`. As a result, `price_unit` remained at 0 even though the credit note was no longer active, causing the final invoice to deduct nothing. opw-6373578 Forward-Port-Of: odoo/odoo#277043 Forward-Port-Of: odoo/odoo#275684
In the Outlook calendar sync, it's possible to have the same attendee twice. That's because the normalized email wasn't used to check for preexisting attendee. To reproduce, sync event with the organizer, also an attendee, using high case in the email such as: ORGANIZER: Mike@organizer.com ATTENDEE: Mike@organizer.com ATTENDEE: John@attendee.com opw-6186606 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#272258 For
Original PR description
In the Outlook calendar sync, it's possible to have the same attendee twice. That's because the normalized email wasn't used to check for preexisting attendee. To reproduce, sync event with the organizer, also an attendee, using high case in the email such as: ORGANIZER: Mike@organizer.com ATTENDEE: Mike@organizer.com ATTENDEE: John@attendee.com opw-6186606 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#272258 Forward-Port-Of: odoo/odoo#268201
Before this commit, two issues affected custom snippet saves. First, the new builder allowed saving the Alternative Products block from product pages as a custom snippet. This block is generated from the current product alternatives, so it is page-specific content. Second, a block saved as a custom snippet could keep `oe_unremovable` or `oe_unmovable` on its root. A saved custom snippet is meant to become a normal reusable block. Once the user drops it on another page, they should be able
Original PR description
Before this commit, two issues affected custom snippet saves. First, the new builder allowed saving the Alternative Products block from product pages as a custom snippet. This block is generated from the current product alternatives, so it is page-specific content. Second, a block saved as a custom snippet could keep `oe_unremovable` or `oe_unmovable` on its root. A saved custom snippet is meant to become a normal reusable block. Once the user drops it on another page, they should be able to remove it or move it like any other block. After this commit, builder plugins can declare selectors that cannot be saved as custom snippets, and `website_sale` uses this to exclude the Alternative Products block. The saved copy also drops `oe_unremovable` and `oe_unmovable` on its root before calling `ir.ui.view.save_snippet`. task-6296872 Forward-Port-Of: odoo/odoo#275303
Currently, we only load the XML data from the exported POS order in the field `l10n_jo_edi_pos_computed_xml` when`l10n_jo_edi_pos_error` is False. It should be the opposite. The field `l10n_jo_edi_pos_computed_xml` is only used when we call `download_l10n_jo_edi_pos_computed_xml()` from the anchor "Download XML" , and that anchor is only visible when `l10n_jo_edi_pos_error` is True. If the request succeeds, then the XML file will be stored in the field `l10n_jo_edi_pos_xml_attachment_i
Original PR description
Currently, we only load the XML data from the exported POS order in the field `l10n_jo_edi_pos_computed_xml` when`l10n_jo_edi_pos_error` is False. It should be the opposite. The field `l10n_jo_edi_pos_computed_xml` is only used when we call `download_l10n_jo_edi_pos_computed_xml()` from the anchor "Download XML" , and that anchor is only visible when `l10n_jo_edi_pos_error` is True. If the request succeeds, then the XML file will be stored in the field `l10n_jo_edi_pos_xml_attachment_id` anyway. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276918
Steps to reproduce 1. Set the user timezone to one with a positive UTC offset, e.g. Europe/Brussels. 2. Open Payroll > Work Entries in pivot view and apply the built-in "Current Month" filter. 3. Expand an employee row and group by Date > Day. Expected: only days of the current month are visible (April 1..30 at the time of writing). Actual: the last day of the previous month (March 31) is included and the last day of the current month (April 30) is missing — the whole window
Original PR description
Steps to reproduce 1. Set the user timezone to one with a positive UTC offset, e.g. Europe/Brussels. 2. Open Payroll > Work Entries in pivot view and apply the built-in "Current Month" filter. 3.…
Steps to reproduce 1. Set the user timezone to one with a positive UTC offset, e.g. Europe/Brussels. 2. Open Payroll > Work Entries in pivot view and apply the built-in "Current Month" filter. 3. Expand an employee row and group by Date > Day. Expected: only days of the current month are visible (April 1..30 at the time of writing). Actual: the last day of the previous month (March 31) is included and the last day of the current month (April 30) is missing — the whole window is shifted one day earlier. Issue The relative-date DSL added in https://github.com/odoo/odoo/commit/d1ea43f6721116914762ea323d8a5987f043e87f operates on a tz-aware datetime starting from `now` when no explicit anchor is given. With the filter at https://github.com/odoo/odoo/blob/d58f4ed332af35f6de26a93f07adf05368731e20/addons/hr_work_entry/views/hr_work_entry_views.xml#L190-L192, `=1d` resets the clock to 00:00 and the day to the 1st in the user's timezone, evaluating to `2026-04-01 00:00:00+02:00` for Brussels. `parse_date` then normalizes that to UTC-naive `2026-03-31 22:00:00` (see https://github.com/odoo/odoo/blob/d58f4ed332af35f6de26a93f07adf05368731e20/odoo/tools/date_utils.py#L196-L199). When the ORM compares this to the `fields.Date` column `date` it truncates via `datetime.date()` (see https://github.com/odoo/odoo/blob/d58f4ed332af35f6de26a93f07adf05368731e20/odoo/orm/fields_temporal.py#L138-L158), yielding `2026-03-31`. The effective domain becomes `[2026-03-31, 2026-04-30)` instead of `[2026-04-01, 2026-05-01)`, which is why March 31 leaks in and April 30 disappears. Solution Anchor the filter to `today` instead of the implicit `now`. The DSL documents `today` as a starting point that yields a `date` value (see https://github.com/odoo/odoo/blob/d58f4ed332af35f6de26a93f07adf05368731e20/odoo/tools/date_utils.py#L108-L144), so `today =1d` resolves to the first of the month as a calendar date end-to-end and never goes through the tz-aware datetime → UTC-naive roundtrip that was silently shifting the window. opw-6123753 Forward-Port-Of: odoo/odoo#260727
Steps to reproduce: ------------------------------------ 1. Install barcode_gs1_nomenclature Module. 2. Go to Inventory > Configuration > Barcode Nomenclature. 3. Open any barcode nomenclature. Observation: ------------------------------------ The "Is GS1 Nomenclature" field is visible in the list header. Issue: ------------------------------------ The field uses the invisible attribute, which hides the field values but does not hide the corresponding list header, resulting in an e
Original PR description
Steps to reproduce: ------------------------------------ 1. Install barcode_gs1_nomenclature Module. 2. Go to Inventory > Configuration > Barcode Nomenclature. 3. Open any barcode nomenclature. Observation: ------------------------------------ The "Is GS1 Nomenclature" field is visible in the list header. Issue: ------------------------------------ The field uses the invisible attribute, which hides the field values but does not hide the corresponding list header, resulting in an empty header column. Solution: ------------------------------------ Replace the 'invisible' attribute with 'column_invisible' so that both the field values and the corresponding list header are hidden. opw-6390731 Forward-Port-Of: odoo/odoo#276532
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create Invoices` > select `Regular invoice` > `Create Draft`. (leave the invoice in the draft state). - Navigate to Accounting > Review > Invoiced Not Delivered. - Select the newly created invoice and click `Create Accrual Entries`. **Error:** `ZeroDivisionError: division by zero` **Root Ca
Original PR description
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create…
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create Invoices` > select `Regular invoice` > `Create Draft`. (leave the invoice in the draft state). - Navigate to Accounting > Review > Invoiced Not Delivered. - Select the newly created invoice and click `Create Accrual Entries`. **Error:** `ZeroDivisionError: division by zero` **Root Cause:** At [1], `processed_qty` is initialized with `0`. If all the selected invoices are in the draft state, the code never enters the for loop, so `processed_qty` remains 0. Later, it attempts to divide the `amount` by `processed_qty`, causing an error. **Fix:** This commit prevents errors when creating accrual entries for draft invoices. [1]: https://github.com/odoo/odoo/blob/a7d285a833f5b1d47a11a1b8ec31e3730c1c854c/addons/account/wizard/accrued_orders.py#L258-L265 opw-6332652 Forward-Port-Of: odoo/odoo#273330
**Issue 1:** Steps to reproduce: - Install the `l10n_ar` module. - Go to Customers and create a new customer with the country set to Argentina. - Set `Identification Type` to `CUIL` and `Identification Number` to `1234567890a`. **Error:** `ValueError: invalid literal for int() with base 10: '1234567890a'` **Issue 2:** - Set `Identification Type` to any type other than `CUIT`, `DNI`, or `CUIL`. - Set `Identification Number` to `1234567890a`. **Observation:** All non-digit chara
Original PR description
**Issue 1:** Steps to reproduce: - Install the `l10n_ar` module. - Go to Customers and create a new customer with the country set to Argentina. - Set `Identification Type` to `CUIL` and…
**Issue 1:** Steps to reproduce: - Install the `l10n_ar` module. - Go to Customers and create a new customer with the country set to Argentina. - Set `Identification Type` to `CUIL` and `Identification Number` to `1234567890a`. **Error:** `ValueError: invalid literal for int() with base 10: '1234567890a'` **Issue 2:** - Set `Identification Type` to any type other than `CUIT`, `DNI`, or `CUIL`. - Set `Identification Number` to `1234567890a`. **Observation:** All non-digit characters are stripped, and the identification number is silently changed to `1234567890`. **Expected behaviour:** Any Identification Type other than CUIT (80), CUIL (86), and DNI (96) should be kept unchanged without stripping alphabetic characters. **Root Cause:** At [1], `_get_id_number_sanitize` sanitizes identification numbers based on the selected Identification Type. - For `CUIT` and `CUIL`, `stdnum.ar.cuit.compact()` only removes separators (e.g., spaces and dashes). If the identification number contains alphabetic characters, they are preserved and called `int()` on the resulting value, raising a `ValueError`. - For all other identification types, valid alphanumeric values are unintentionally modified by stripping non-digit characters. **Fix:** This commit validates identification numbers before sanitization for `CUIT` (80), `CUIL` (86), and `DNI` (96), ensuring only valid numeric identification numbers are converted. For all other identification types, it preserves alphanumeric characters by removing only non-alphanumeric separators. [1]: https://github.com/odoo/odoo/blob/08b75d753c638e9d2d7418b55e9107bda471cb31/addons/l10n_ar/models/res_partner.py#L124-L136 Related enterrpise PR: https://github.com/odoo/enterprise/pull/123729 opw-6333998 Forward-Port-Of: odoo/odoo#272651
Login uniqueness is enforced by a `UNIQUE (login)` constraint that Postgres compares byte for byte, so signing up with foo@example.com and then Foo@example.com produces two separate accounts pointing at the same real mailbox. https://github.com/odoo/odoo/blob/66a6c16551041543b5addfe846f15e769b4e9afe/odoo/addons/base/models/res_users.py#L274 Even if the DB constraint did catch an exact-case duplicate and `_signup_create_user` re-raised it as a `SignupError`, the controller's friendly "already re
Original PR description
Login uniqueness is enforced by a `UNIQUE (login)` constraint that Postgres compares byte for byte, so signing up with foo@example.com and then Foo@example.com produces two separate accounts pointing…
Login uniqueness is enforced by a `UNIQUE (login)` constraint that Postgres compares byte for byte, so signing up with foo@example.com and then Foo@example.com produces two separate accounts pointing at the same real mailbox. https://github.com/odoo/odoo/blob/66a6c16551041543b5addfe846f15e769b4e9afe/odoo/addons/base/models/res_users.py#L274 Even if the DB constraint did catch an exact-case duplicate and `_signup_create_user` re-raised it as a `SignupError`, the controller's friendly "already registered" branch only triggers when the duplicate lookup finds a row, and that lookup goes through `_get_login_domain` with an exact `=` operator. Case variants would fall into the generic "Could not create a new account" branch instead. https://github.com/odoo/odoo/blob/66a6c16551041543b5addfe846f15e769b4e9afe/addons/auth_signup/controllers/main.py#L68-L75 https://github.com/odoo/odoo/blob/66a6c16551041543b5addfe846f15e769b4e9afe/odoo/addons/base/models/res_users.py#L749-L750 `_signup_create_user` now refuses creation when a user with the same email already exists, applying to both b2c free signup and token-based invitations. It raises `UserError` directly so the controller's `except UserError` surfaces the message without a redundant lookup. The check uses `_get_email_domain`, whose base implementation is switched from `=` to `=ilike` over a value escaped via `tools.escape_psql` so `%` and `_` are matched literally rather than as wildcards. Its only existing caller is `reset_password`, which already wants case-insensitive matching. Steps to reproduce: 1. In Settings, set Customer Account to "Free sign up" and save. 2. Log out, then on the login page click "Don't have an account?". 3. Register with foo@example.com. 4. Log out again and click "Don't have an account?". 5. Register with Foo@example.com. => Two distinct user accounts are created for the same mailbox. opw-6199441 Forward-Port-Of: odoo/odoo#263864
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause: ====== The donation snippet posts its configuration (prefilled amounts, display options, descriptions) in the request body. If the user switches the website language on the payment page, it triggers a page reload via a GET request which drops the original form body (this also applies to
Original PR description
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause:…
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause: ====== The donation snippet posts its configuration (prefilled amounts, display options, descriptions) in the request body. If the user switches the website language on the payment page, it triggers a page reload via a GET request which drops the original form body (this also applies to a simple page refresh). As a result, the kwargs were empty and the page fell back to the default free-amount input, losing the configured options. Solution: ========= Implement Post/Redirect/Get: on POST, redirect to /donation/pay with the options in the query string so any later GET (language switch, refresh, iframe src reload) re-renders a fully configured page without losing data. Alternative Solution: ===================== We could also store the options in the session, but the current solution is much simpler and avoids session pollution. opw-6282391 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#271460
Issue: - After confirming a sale order with a reward applied (e.g. an ewallet reward), unlocking it, and editing it so the reward line's cost changes (e.g. adding a product and re-claiming the reward), the coupon's point balance updates correctly. - However, the corresponding loyalty.history record's used value is never refreshed, so it keeps showing the old cost instead of the new one. Steps to reproduce: - Enable Loyalty and Lock Confirmed Sales in Sales settings. - Create an ewallet ty
Original PR description
Issue: - After confirming a sale order with a reward applied (e.g. an ewallet reward), unlocking it, and editing it so the reward line's cost changes (e.g. adding a product and re-claiming the…
Issue: - After confirming a sale order with a reward applied (e.g. an ewallet reward), unlocking it, and editing it so the reward line's cost changes (e.g. adding a product and re-claiming the reward), the coupon's point balance updates correctly. - However, the corresponding loyalty.history record's used value is never refreshed, so it keeps showing the old cost instead of the new one. Steps to reproduce: - Enable Loyalty and Lock Confirmed Sales in Sales settings. - Create an ewallet type loyalty.program and generate an ewallet for a partner with e.g. 1000 points. - Create a sale order for that partner, add a product worth 100, and claim the ewallet reward (reward line created with points_cost = 100). - Confirm the order. loyalty.history shows used = 100 (correct), card balance shows 900 (correct). - Unlock the order, add a second product worth 100, and claim the reward again (same reward line updates to points_cost = 200). - Lock the order again. - Check the loyalty.card: balance is correctly 800. - Check loyalty.history for that order: used still shows 100 instead of 200. Fix: - Updated _update_loyalty_history() in sale_order.py to create a new history line if none exists for the given card and order combination. - Updated write() in sale_order_line.py to correctly sync history lines when a reward line is modified on a confirmed order, handling both same-coupon updates via delta and coupon changes by subtracting the old coupon cost and adding the new one separately. - Added test_loyalty_history_created_on_post_confirm_reward to verify that a history line is created when a reward is claimed on a confirmed order where no history line existed before. - Added test_loyalty_history_updated_on_points_cost_write to verify that history.used is updated by the correct delta when points_cost changes on a reward line of a confirmed order. Impact: - Ensures a coupon's usage history stays accurate after a confirmed order is unlocked and edited. - Prevents the loyalty.history used field from silently going stale while the actual point balance is correct. - Not scoped to ewallet specifically — since points_cost semantics are the same across program types, this also corrects the same class of staleness for other reward types (discount, gift_card, etc.) when a reward line's cost changes post-confirmation. Forward-Port-Of: odoo/odoo#276961 Forward-Port-Of: odoo/odoo#273688
On touch devices (smartphones, tablets), the in-browser notification sound plays through the media audio channel, which is not affected by the device's silent mode. This causes unexpected sound playback when the device is set to silent. Suppress _playSound() on devices with maxTouchPoints > 1 (touch/mobile). Push notifications handle alerts on mobile and properly respect the device's silent mode. The side effect is that also laptops with touch screen are affected and need to use web push n
Original PR description
On touch devices (smartphones, tablets), the in-browser notification sound plays through the media audio channel, which is not affected by the device's silent mode. This causes unexpected sound playback when the device is set to silent. Suppress _playSound() on devices with maxTouchPoints > 1 (touch/mobile). Push notifications handle alerts on mobile and properly respect the device's silent mode. The side effect is that also laptops with touch screen are affected and need to use web push notifications in order to get notification sounds played. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276675 Forward-Port-Of: odoo/odoo#275797
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through upsert_en()) can have their English source text changed by a module upgrade while keeping the same key. For example product.template's selection value 'consu' went from meaning "Consumable" to meaning "Goods" between 17.0 and 18.0, without the stored value itself changing. In that situation, upsert
Original PR description
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through…
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through upsert_en()) can have their English source text changed by a module upgrade while keeping the same key. For example product.template's selection value 'consu' went from meaning "Consumable" to meaning "Goods" between 17.0 and 18.0, without the stored value itself changing. In that situation, upsert_en() always refreshes the 'en_US' entry from the current code, so English is never wrong. But every other language keeps whatever value it had, since upsert_en() blindly merges the new 'en_US' entry into the existing jsonb without checking whether the previous 'en_US' value actually changed. When the module's .po file is later reloaded, TranslationImporter.save() finds a non-empty value already there for each language and, by design, does not overwrite it unless explicitly asked to (`overwrite`) on a record that isn't `noupdate`. That protection is correct when the English text hasn't changed (it keeps manual translation customizations across upgrades), but here it ends up protecting a translation of a source string that no longer exists. As a result, a value whose meaning changes across versions keeps showing its old label in every language except English, forever, unless someone forces a translation overwrite. Fix this in upsert_en() itself: when the incoming 'en_US' value differs from the one currently stored, drop every other language already stored for that row instead of merging into it. Those translations were made for a source string that no longer exists, so there is nothing worth protecting. Once dropped, they are simply missing from the jsonb, so TranslationImporter.save() naturally lets the freshly (re)loaded translations in on its next run, without needing to know anything about source drift, and `overwrite`/`noupdate` keep their exact original meaning for every other case. Task-6333080 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276725 Forward-Port-Of: odoo/odoo#275080
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce email from will default to the bounce alias of the main company of the DB. In a multi company setup, where each company might have their own mail alias domain, this can be confusing for the receiving party that didn't send the original email to the main company's alias domain. ## Propo
Original PR description
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce…
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce email from will default to the bounce alias of the main company of the DB. In a multi company setup, where each company might have their own mail alias domain, this can be confusing for the receiving party that didn't send the original email to the main company's alias domain. ## Proposed solution: We call `_routing_create_bounce_email` with the right company in the context, since the journal company is already detected. Additionally, in the context of the accounting code calling the bounce private method, we pass an explicit value for `reply_to` ## How to reproduce it: 1) Set up DB with two companies (companyA and companyB). Install account. 2) Set up two mail alias domains (companya.com and companyb.com) 3) Send an email to the invoice alias of companyB (e.g. invoices@companyb.com). -> bounce email will have FROM as "notifications@companya.com" (the notifications alias of companyA) Example headers before: ``` Reply-To: "YourCompany" <catchall@companya.com> To: customer@example.com Date: Thu, 02 Jul 2026 11:18:49 -0000 Return-Path: bounce@companya.com From: "MAILER-DAEMON" <bounce@companya.com> ``` After fix: ``` Reply-To: info@companyb.com To: customer@example.com Date: Thu, 02 Jul 2026 11:16:19 -0000 Return-Path: bounce@companyb.com From: "MAILER-DAEMON" <notifications@companyb.com> ``` opw-6342778 Forward-Port-Of: odoo/odoo#273671
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/704eb0e91c [REL] 19.1.28 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/0dd5a84d0a [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) https://github.com/odoo/o-spreadsheet/commit/ebf457610e [FIX] index: export `positionMap` [Task: 6159213](https://www.odoo.com/odoo/2328/tasks/6159213) https://github.com/odoo/
Original PR description
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/704eb0e91c [REL] 19.1.28 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0)…
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/704eb0e91c [REL] 19.1.28 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/0dd5a84d0a [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) https://github.com/odoo/o-spreadsheet/commit/ebf457610e [FIX] index: export `positionMap` [Task: 6159213](https://www.odoo.com/odoo/2328/tasks/6159213) https://github.com/odoo/o-spreadsheet/commit/b948bf36ae [FIX] Pivots: cache `getPivotIdFromPosition` for performance [Task: 6159213](https://www.odoo.com/odoo/2328/tasks/6159213) 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>
Make sure to click on the correct action menu when tryin to delete the selected website page. If we do not specify this we could randomly click on the little gear menu that do not contain the delete option. It's actually already done like this in 19.3 here https://github.com/odoo/odoo/blob/c5d7a6a90be4730e18070826a9394ab10dfc6be8/addons/website/static/tests/tours/page_manager.js#L129 runbot-233357
Original PR description
Make sure to click on the correct action menu when tryin to delete the selected website page. If we do not specify this we could randomly click on the little gear menu that do not contain the delete option. It's actually already done like this in 19.3 here https://github.com/odoo/odoo/blob/c5d7a6a90be4730e18070826a9394ab10dfc6be8/addons/website/static/tests/tours/page_manager.js#L129 runbot-233357
11 changes
Resolved issues and error corrections
Invoice dashboard data, specifically DSO, was incorrectly aligned due to a mismatch in the fiscal year structure. Task-6049887 Forward-Port-Of: odoo/odoo#261037
Original PR description
Invoice dashboard data, specifically DSO, was incorrectly aligned due to a mismatch in the fiscal year structure. Task-6049887 Forward-Port-Of: odoo/odoo#261037
Currently, we only load the XML data from the exported POS order in the field `l10n_jo_edi_pos_computed_xml` when`l10n_jo_edi_pos_error` is False. It should be the opposite. The field `l10n_jo_edi_pos_computed_xml` is only used when we call `download_l10n_jo_edi_pos_computed_xml()` from the anchor "Download XML" , and that anchor is only visible when `l10n_jo_edi_pos_error` is True. If the request succeeds, then the XML file will be stored in the field `l10n_jo_edi_pos_xml_attachment_i
Original PR description
Currently, we only load the XML data from the exported POS order in the field `l10n_jo_edi_pos_computed_xml` when`l10n_jo_edi_pos_error` is False. It should be the opposite. The field `l10n_jo_edi_pos_computed_xml` is only used when we call `download_l10n_jo_edi_pos_computed_xml()` from the anchor "Download XML" , and that anchor is only visible when `l10n_jo_edi_pos_error` is True. If the request succeeds, then the XML file will be stored in the field `l10n_jo_edi_pos_xml_attachment_id` anyway. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276918
Steps to reproduce: ------------------------------------ 1. Install barcode_gs1_nomenclature Module. 2. Go to Inventory > Configuration > Barcode Nomenclature. 3. Open any barcode nomenclature. Observation: ------------------------------------ The "Is GS1 Nomenclature" field is visible in the list header. Issue: ------------------------------------ The field uses the invisible attribute, which hides the field values but does not hide the corresponding list header, resulting in an e
Original PR description
Steps to reproduce: ------------------------------------ 1. Install barcode_gs1_nomenclature Module. 2. Go to Inventory > Configuration > Barcode Nomenclature. 3. Open any barcode nomenclature. Observation: ------------------------------------ The "Is GS1 Nomenclature" field is visible in the list header. Issue: ------------------------------------ The field uses the invisible attribute, which hides the field values but does not hide the corresponding list header, resulting in an empty header column. Solution: ------------------------------------ Replace the 'invisible' attribute with 'column_invisible' so that both the field values and the corresponding list header are hidden. opw-6390731 Forward-Port-Of: odoo/odoo#276532
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause: ====== The donation snippet posts its configuration (prefilled amounts, display options, descriptions) in the request body. If the user switches the website language on the payment page, it triggers a page reload via a GET request which drops the original form body (this also applies to
Original PR description
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause:…
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause: ====== The donation snippet posts its configuration (prefilled amounts, display options, descriptions) in the request body. If the user switches the website language on the payment page, it triggers a page reload via a GET request which drops the original form body (this also applies to a simple page refresh). As a result, the kwargs were empty and the page fell back to the default free-amount input, losing the configured options. Solution: ========= Implement Post/Redirect/Get: on POST, redirect to /donation/pay with the options in the query string so any later GET (language switch, refresh, iframe src reload) re-renders a fully configured page without losing data. Alternative Solution: ===================== We could also store the options in the session, but the current solution is much simpler and avoids session pollution. opw-6282391 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#271460
Steps to reproduce the bug: - Load a database without demo data - Install `auth_ldap` - Run the `TestAuthLDAP.test_auth_ldap` test Problem: The test failed with a 404 error on `POST /web/login`: `requests.exceptions.HTTPError: 404 Client Error: NOT FOUND for url: http://127.0.0.1:8069/web/login`. The mocked `_get_ldap_dicts` hardcoded the LDAP config's template `user` as `(6, "Marc Demo")`, assuming the demo user `base.user_demo` exists with that id. Without demo data, `res.users(6,)`
Original PR description
Steps to reproduce the bug: - Load a database without demo data - Install `auth_ldap` - Run the `TestAuthLDAP.test_auth_ldap` test Problem: The test failed with a 404 error on `POST /web/login`: `requests.exceptions.HTTPError: 404 Client Error: NOT FOUND for url: http://127.0.0.1:8069/web/login`. The mocked `_get_ldap_dicts` hardcoded the LDAP config's template `user` as `(6, "Marc Demo")`, assuming the demo user `base.user_demo` exists with that id. Without demo data, `res.users(6,)` does not exist, so `_get_or_create_user`'s `SudoUser.browse(conf['user'][0]).copy(...)` raised a `MissingError`, which Odoo's HTTP dispatcher turns into a 404. Solution: Create a dedicated "user template" at the start of the test and use it as the LDAP template user, instead of hardcoding a demo-data record id. This makes the test self-contained and independent of whether demo data is loaded. runbot-243648
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through upsert_en()) can have their English source text changed by a module upgrade while keeping the same key. For example product.template's selection value 'consu' went from meaning "Consumable" to meaning "Goods" between 17.0 and 18.0, without the stored value itself changing. In that situation, upsert
Original PR description
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through…
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through upsert_en()) can have their English source text changed by a module upgrade while keeping the same key. For example product.template's selection value 'consu' went from meaning "Consumable" to meaning "Goods" between 17.0 and 18.0, without the stored value itself changing. In that situation, upsert_en() always refreshes the 'en_US' entry from the current code, so English is never wrong. But every other language keeps whatever value it had, since upsert_en() blindly merges the new 'en_US' entry into the existing jsonb without checking whether the previous 'en_US' value actually changed. When the module's .po file is later reloaded, TranslationImporter.save() finds a non-empty value already there for each language and, by design, does not overwrite it unless explicitly asked to (`overwrite`) on a record that isn't `noupdate`. That protection is correct when the English text hasn't changed (it keeps manual translation customizations across upgrades), but here it ends up protecting a translation of a source string that no longer exists. As a result, a value whose meaning changes across versions keeps showing its old label in every language except English, forever, unless someone forces a translation overwrite. Fix this in upsert_en() itself: when the incoming 'en_US' value differs from the one currently stored, drop every other language already stored for that row instead of merging into it. Those translations were made for a source string that no longer exists, so there is nothing worth protecting. Once dropped, they are simply missing from the jsonb, so TranslationImporter.save() naturally lets the freshly (re)loaded translations in on its next run, without needing to know anything about source drift, and `overwrite`/`noupdate` keep their exact original meaning for every other case. Task-6333080 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276725 Forward-Port-Of: odoo/odoo#275080
**Problem:** For an hour-based time off allocation, changing the employee's working schedule leaves the allocation duration (in days) stale, so the balance shown on the Time Off dashboard becomes wrong. **Steps to reproduce:** 1. Give an employee a working schedule of 8 hours/day. 2. Create an hour-based allocation (time off type with Request Unit = Hours) granting e.g. 8 hours (1 day). 3. Change the employee's working schedule to one with a different Hours per Day (e.g. 4 hours/day). 4.
Original PR description
**Problem:** For an hour-based time off allocation, changing the employee's working schedule leaves the allocation duration (in days) stale, so the balance shown on the Time Off dashboard becomes…
**Problem:** For an hour-based time off allocation, changing the employee's working schedule leaves the allocation duration (in days) stale, so the balance shown on the Time Off dashboard becomes wrong. **Steps to reproduce:** 1. Give an employee a working schedule of 8 hours/day. 2. Create an hour-based allocation (time off type with Request Unit = Hours) granting e.g. 8 hours (1 day). 3. Change the employee's working schedule to one with a different Hours per Day (e.g. 4 hours/day). 4. Check the allocation / the Time Off dashboard balance. **Current behavior:** number_of_days stays at its old value (1), so the balance is recomputed as 1 day x 4 hours = 4 hours instead of the 8 hours actually accrued. **Expected behavior:** The accrued hours stay constant; the duration in days follows the new schedule (8 hours / 4 hours-per-day = 2 days). **Cause of the issue:** `number_of_days` and `number_of_hours_display` compute from each other (`number_of_days = number_of_hours_display / hours_per_day` and `number_of_hours_display = number_of_days * hours_per_day`), forming a dependency cycle, and neither depends on the employee's working schedule. So a schedule change never recomputes either field. Adding the schedule to `_compute_number_of_days`' depends does not help: because of the cycle it recomputes `number_of_hours_display` from the stale `number_of_days` first, which silently destroys the accrued hours. **Fix:** When the employee's working schedule changes, the accrued hours are the quantity that must be preserved, so the duration is recomputed explicitly from the still-stored `number_of_hours_display` (setting `number_of_days` first, exactly as a manual `_compute_number_of_days()` does). Driving the order by hand is necessary because the cyclic compute graph cannot guarantee `number_of_days` is computed before `number_of_hours_display`. opw-6276242 Forward-Port-Of: odoo/odoo#270129
Issue: In a multi-company environment, when an attachment is uploaded in the chatter, the resulting `ir.attachment` record incorrectly sets the company_id to the current user's default company. This occurs regardless of the active company or the company associated with the record the attachment is linked to. Steps to reproduce: In a multi-company setup, activate only Company B. Open any chatter and attach a file. Navigate to Settings > Technical > Data Structure > Attachments. Current b
Original PR description
Issue: In a multi-company environment, when an attachment is uploaded in the chatter, the resulting `ir.attachment` record incorrectly sets the company_id to the current user's default company. This…
Issue: In a multi-company environment, when an attachment is uploaded in the chatter, the resulting `ir.attachment` record incorrectly sets the company_id to the current user's default company. This occurs regardless of the active company or the company associated with the record the attachment is linked to. Steps to reproduce: In a multi-company setup, activate only Company B. Open any chatter and attach a file. Navigate to Settings > Technical > Data Structure > Attachments. Current behaviour: The created attachment has its company_id set to the user's default company, rather than the currently active company, or the company of the record, even when the record has a company_id field. This behavior introduces inconsistencies in data visibility, particularly when attachments appear to belong to a company different from the one associated with the related business record. Behaviour After Fix: If the related record (i.e., the model the attachment is linked to) contains a company_id field, its value will be used as the attachment's company_id otherwise, the attachment's company_id will be set to the currently active company's id. This logic ensures proper alignment between attachments and their related business records and also maintaining consistency in multi-company scenarios. task-4563173 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#235665
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/a8d15b9b06 [REL] 18.3.56 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/af4e112c59 [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) 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
Original PR description
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/a8d15b9b06 [REL] 18.3.56 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/af4e112c59 [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) 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>
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce email from will default to the bounce alias of the main company of the DB. In a multi company setup, where each company might have their own mail alias domain, this can be confusing for the receiving party that didn't send the original email to the main company's alias domain. ## Propo
Original PR description
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce…
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce email from will default to the bounce alias of the main company of the DB. In a multi company setup, where each company might have their own mail alias domain, this can be confusing for the receiving party that didn't send the original email to the main company's alias domain. ## Proposed solution: We call `_routing_create_bounce_email` with the right company in the context, since the journal company is already detected. Additionally, in the context of the accounting code calling the bounce private method, we pass an explicit value for `reply_to` ## How to reproduce it: 1) Set up DB with two companies (companyA and companyB). Install account. 2) Set up two mail alias domains (companya.com and companyb.com) 3) Send an email to the invoice alias of companyB (e.g. invoices@companyb.com). -> bounce email will have FROM as "notifications@companya.com" (the notifications alias of companyA) Example headers before: ``` Reply-To: "YourCompany" <catchall@companya.com> To: customer@example.com Date: Thu, 02 Jul 2026 11:18:49 -0000 Return-Path: bounce@companya.com From: "MAILER-DAEMON" <bounce@companya.com> ``` After fix: ``` Reply-To: info@companyb.com To: customer@example.com Date: Thu, 02 Jul 2026 11:16:19 -0000 Return-Path: bounce@companyb.com From: "MAILER-DAEMON" <notifications@companyb.com> ``` opw-6342778 Forward-Port-Of: odoo/odoo#273671
The footer slideout state was computed only once during interaction setup. If the main content height changed afterward, e.g. in edit mode: dropping or removing snippets, or resizing the window, the effect could remain enabled/disabled even though the content had become taller/shorter than the viewport. Steps to reproduce: - Go into edit mode - Add two snippets on the page - On the footer, set the "Slideout Effect" option to "Slide Hover" - Remove one snippet - Half of the footer is
Original PR description
The footer slideout state was computed only once during interaction setup. If the main content height changed afterward, e.g. in edit mode: dropping or removing snippets, or resizing the window, the effect could remain enabled/disabled even though the content had become taller/shorter than the viewport. Steps to reproduce: - Go into edit mode - Add two snippets on the page - On the footer, set the "Slideout Effect" option to "Slide Hover" - Remove one snippet - Half of the footer is hidden by the hover effect, which should not happen task-6117257
3 changes
Resolved issues and error corrections
**Problem:** For an hour-based time off allocation, changing the employee's working schedule leaves the allocation duration (in days) stale, so the balance shown on the Time Off dashboard becomes wrong. **Steps to reproduce:** 1. Give an employee a working schedule of 8 hours/day. 2. Create an hour-based allocation (time off type with Request Unit = Hours) granting e.g. 8 hours (1 day). 3. Change the employee's working schedule to one with a different Hours per Day (e.g. 4 hours/day). 4.
Original PR description
**Problem:** For an hour-based time off allocation, changing the employee's working schedule leaves the allocation duration (in days) stale, so the balance shown on the Time Off dashboard becomes…
**Problem:** For an hour-based time off allocation, changing the employee's working schedule leaves the allocation duration (in days) stale, so the balance shown on the Time Off dashboard becomes wrong. **Steps to reproduce:** 1. Give an employee a working schedule of 8 hours/day. 2. Create an hour-based allocation (time off type with Request Unit = Hours) granting e.g. 8 hours (1 day). 3. Change the employee's working schedule to one with a different Hours per Day (e.g. 4 hours/day). 4. Check the allocation / the Time Off dashboard balance. **Current behavior:** number_of_days stays at its old value (1), so the balance is recomputed as 1 day x 4 hours = 4 hours instead of the 8 hours actually accrued. **Expected behavior:** The accrued hours stay constant; the duration in days follows the new schedule (8 hours / 4 hours-per-day = 2 days). **Cause of the issue:** `number_of_days` and `number_of_hours_display` compute from each other (`number_of_days = number_of_hours_display / hours_per_day` and `number_of_hours_display = number_of_days * hours_per_day`), forming a dependency cycle, and neither depends on the employee's working schedule. So a schedule change never recomputes either field. Adding the schedule to `_compute_number_of_days`' depends does not help: because of the cycle it recomputes `number_of_hours_display` from the stale `number_of_days` first, which silently destroys the accrued hours. **Fix:** When the employee's working schedule changes, the accrued hours are the quantity that must be preserved, so the duration is recomputed explicitly from the still-stored `number_of_hours_display` (setting `number_of_days` first, exactly as a manual `_compute_number_of_days()` does). Driving the order by hand is necessary because the cyclic compute graph cannot guarantee `number_of_days` is computed before `number_of_hours_display`. opw-6276242
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/ea26af9191 [REL] 18.0.75 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/5850ea078b [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) 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
Original PR description
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/ea26af9191 [REL] 18.0.75 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/5850ea078b [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) 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>
Reverse charge taxes (Inversione Contabile) should have the translated code ending with 'IC' (the acronym), not 'CI'. Suggested by partner: eLBati task-none Forward-Port-Of: odoo/odoo#276270
Original PR description
Reverse charge taxes (Inversione Contabile) should have the translated code ending with 'IC' (the acronym), not 'CI'. Suggested by partner: eLBati task-none Forward-Port-Of: odoo/odoo#276270
3 changes
Resolved issues and error corrections
Helpers for the enterprise PR opw-5862529 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
Original PR description
Helpers for the enterprise PR opw-5862529 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
Seven entries of the Mexican chart of accounts template carry a name belonging to a **different** group, copied from a neighbouring entry. Each record's XML ID still states the intended name, which is what this restores. | Code | Field | Before | After | |---|---|---|---| | `6` | `name@es` | Gastos generales | Gastos | | `252.07` | `name@es` | `account_subgroup_hipotecas_por_pagar_a_largo_plazo_nacional` | Hipotecas por pagar a largo plazo nacional | | `602` | `name`, `name@es` | Cost of sales
Original PR description
Seven entries of the Mexican chart of accounts template carry a name belonging to a **different** group, copied from a neighbouring entry. Each record's XML ID still states the intended name, which…
Seven entries of the Mexican chart of accounts template carry a name belonging
to a **different** group, copied from a neighbouring entry. Each record's XML ID
still states the intended name, which is what this restores.
| Code | Field | Before | After |
|---|---|---|---|
| `6` | `name@es` | Gastos generales | Gastos |
| `252.07` | `name@es` | `account_subgroup_hipotecas_por_pagar_a_largo_plazo_nacional` | Hipotecas por pagar a largo plazo nacional |
| `602` | `name`, `name@es` | Cost of sales / Costo de venta | Selling expenses / Gastos de venta |
| `613` | `name@es` | Amortización contable | Depreciación contable |
| `614` | `name` | Accounting depreciation | Accounting amortisation |
| `701.06` | `name`, `name@es` | Interest on foreign bank charges / Intereses a cargo bancario extranjero | Interest payable by national natural persons / Intereses a cargo de personas físicas nacional |
| `702` | `name@es` | Utilidad cambiaria | Productos financieros |
### Why it is not cosmetic
The electronic accounting Chart of Accounts XML takes the `Desc` attribute of
every `<Ctas>` element from the *account group name* — `cfdicoa.xml`
(`t-att-Desc="account.get('name')"`), fed by `trial_balance.py`
`_l10n_mx_get_coa_values()`. Any `es_*` database therefore declares:
```xml
<catalogocuentas:Ctas CodAgrup="702" NumCta="702" Desc="Utilidad cambiaria" Nivel="1" Natur="A"/>
```
whereas the SAT catalogue (Anexo 24) publishes `702` as *Productos financieros*,
with `702.01 Utilidad cambiaria` … `702.10 Otros productos financieros` beneath
it. `CodAgrup` comes from `code_prefix_start` and stays correct, so the file
still validates against the XSD, but the declared description does not match the
official nomenclature. Trial Balance and Pólizas are unaffected — neither
exports group names.
### Evidence
- `252.07` contains its own XML ID as the Spanish name.
- `602` duplicates `501.01`, yet its children are `Sueldos y Salarios`,
`Compensaciones`, `Tiempos extras`.
- `613` and `614` are swapped in one language each: `613`'s children are
depreciations, `614`'s are amortisations.
- `701.06` duplicates `701.05` in both languages; the correct name is symmetric
to `701.07` and to `702.06`.
- `6` is the only single-digit root group whose Spanish name does not match its
XML ID (`account_group_gastos`).
### Notes
Introduced in d782b8b92557; correct in 15.0, where the names lived in
`account.account.tag.csv`. Still present in 18.0, 19.0 and master, hence
targeting 17.0. Template data only — existing databases are unaffected until the
chart is (re)installed, and renaming a group moves no balance.### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/6143cd9985 [REL] 17.0.103 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/7ffa322974 [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) 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-authore
Original PR description
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/6143cd9985 [REL] 17.0.103 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/7ffa322974 [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) 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>