Daily updates from Odoo
Friday, July 17, 2026
42 changes · saas-19.1
New functionality added to Odoo
A new German localization feature lets businesses submit tax returns directly to Elster from within Odoo. This reduces manual export and submission steps, helping German companies streamline compliance workflows.
Original PR description
This module enables users to submit their tax returns to Elster directly from Odoo. see IAP: https://github.com/odoo/iap-apps/pull/1513 task-5193637 Forward-Port-Of: odoo/enterprise#121805
### Description of the issue/feature this PR addresses: Adds the Saudi ZATCA identification fields (building number, plot identification, identification scheme/number) to the portal billing address form so customers can fill them themselves. ### Current behavior before PR: The fields exist only in the backend; portal customers can't enter them. ### Desired behavior after PR is merged: Saudi-company customers can fill the fields on the portal (shown only when the billing country is SA
Original PR description
### Description of the issue/feature this PR addresses: Adds the Saudi ZATCA identification fields (building number, plot identification, identification scheme/number) to the portal billing address form so customers can fill them themselves. ### Current behavior before PR: The fields exist only in the backend; portal customers can't enter them. ### Desired behavior after PR is merged: Saudi-company customers can fill the fields on the portal (shown only when the billing country is SA; number disabled for the TIN scheme, with server-side validation). task-5188361 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#270973
Enhancements to existing features
Peruvian e-invoicing now includes the additional product classification codes required by SUNAT's upcoming validation rules. This helps businesses avoid invoice rejections for affected goods once the new requirements take effect.
Original PR description
SUNAT is updating its validation rules 2026-08-01, adding three mandatory annexes (25.1, 25.2, 25.3) to Product Catalog N25. E-invoices for these goods are rejected when the required UNSPSC code is not available in the database. Most of the required codes already exist and are active. The rest are handled here: two missing UNSPSC codes were added to the shared catalog, while the fourteen codes that exist but are inactive and the SUNAT-only code 11111111 (which is not part of the UNSPSC standard) are activated from the Peru localization instead. Doing the Peru-specific part in the l10n_pe_edi install hook and upgrade script, the way l10n_mx_edi and l10n_ke_edi_oscu do for their own codes, keeps these activations out of databases that do not use the Peruvian localization. Task-6366907 Forward-Port-Of: odoo/enterprise#124200 Forward-Port-Of: odoo/enterprise#123577
Move French e-invoicing technical statuses out of the main invoice UI and into the chatter. The PPF status is kept available in debug mode for troubleshooting, while regular users get a concise chatter summary with the PA status, PPF status, and any returned errors. Duplicate Lifecycle XML attachments are no longer posted in the chatter. Task-6273270 Forward-Port-Of: odoo/odoo#276770 Forward-Port-Of: odoo/odoo#269465
Original PR description
Move French e-invoicing technical statuses out of the main invoice UI and into the chatter. The PPF status is kept available in debug mode for troubleshooting, while regular users get a concise chatter summary with the PA status, PPF status, and any returned errors. Duplicate Lifecycle XML attachments are no longer posted in the chatter. Task-6273270 Forward-Port-Of: odoo/odoo#276770 Forward-Port-Of: odoo/odoo#269465
No task ID
Original PR description
No task ID
Test lints should be deterministic so retrying them doesn't make sense. While lints are not the slowest, pylint can take 10~15mn in later branches, and with retrying that's half an hour (and some) to get a failure, which is all that in wasted staging and build time. Forward-Port-Of: odoo/odoo#276600
Original PR description
Test lints should be deterministic so retrying them doesn't make sense. While lints are not the slowest, pylint can take 10~15mn in later branches, and with retrying that's half an hour (and some) to get a failure, which is all that in wasted staging and build time. Forward-Port-Of: odoo/odoo#276600
The Nilvera client returns base64-encoded PDF content from the GET pdf endpoint. A previous change https://github.com/odoo-dev/odoo/commit/d2c2dc0bc33c369369ba748a513fad0c6edcaf6a incorrectly stored this base64 string directly into the `ir.attachment.raw` field without decoding. Because the field is binary, the base64 ASCII text was UTF-8 encoded on the disk, leaving the files unreadable in browsers. This commit introduces a migration script to fix this. For newly downloaded attachment
Original PR description
The Nilvera client returns base64-encoded PDF content from the GET pdf endpoint. A previous change https://github.com/odoo-dev/odoo/commit/d2c2dc0bc33c369369ba748a513fad0c6edcaf6a incorrectly stored this base64 string directly into the `ir.attachment.raw` field without decoding. Because the field is binary, the base64 ASCII text was UTF-8 encoded on the disk, leaving the files unreadable in browsers. This commit introduces a migration script to fix this. For newly downloaded attachments, 35fd6ce handles the decoding. task-6377121 Forward-Port-Of: odoo/odoo#275376
Resolved issues and error corrections
Odoo Studio now handles field labels written with non-Latin characters, such as Arabic, without creating an invalid internal field name. This prevents users from seeing an error when renaming custom fields in these languages, making Studio more reliable for multilingual teams.
Original PR description
Steps: - Install web_studio - Add any field (example char field) to any view - Rename it in arabic, example `السَّلَامُ عَلَيْكُمْ` - Error Custom field names cannot contain double underscores Webclient (view_editor_model) escape every non-alphabetic chars, so new label value contains nothing but a space which will be replaced by a _ this new label value will be concatenated to `x_studio_`. Resulting to the string `x_studio__`. A solution should be to prevent changing the technical name if the new label value (escaped) is empty. opw-6311027 Forward-Port-Of: odoo/enterprise#122094 Forward-Port-Of: odoo/enterprise#121343
Studio approval rules now work when their conditions reference fields that the acting user cannot directly access. This prevents sales order confirmations and similar actions from being blocked by an access rights error, improving reliability for users with limited permissions.
Original PR description
Issue: A studio.approval.rule.domain includes a related field that calls an access rights group that the user who used the action isn't apart of, Is blocked by the filtered_domain. To Replicate: 1) Install studio, sale, Accounting and make sure "account_followup" is installed 2) create a related field on the sales.order form related to "customer -> follow up status" 3) Save 4) Create a "Studio Approval Rule" (studio.approval.rule) with a domain using the new related studio field -> method : "action_confirm" -> approver:admin 5) create a test user with no accounting access rights 6) in an incognito browser try and create a sales order, and then confirm it. it will throw the access rights error Fix: add a sudo to the filtered_domain opw-6316069 Forward-Port-Of: odoo/enterprise#121856
Bank reconciliation list views now show the same supporting attachments as the kanban view. This helps accounting users see the right documents consistently when reviewing and reconciling bank statement lines.
Original PR description
The aim of this commit is showing the same attachment in the bank reconciliation list view than in the kanban view. Before this commit, the field used to display the attachments was attachment_ids, this field were a related on the attachment_ids from account.move. This fix, removes the related to only keep a domain on the One2Many field. Thanks to the relational database, Odoo is giving us the right attachments when we want to display the field. task-6153002
Polish currency rates no longer need an extra one-day date adjustment because the system already uses the previous day’s rate. This prevents rates from being dated too far back and helps keep accounting calculations aligned with Polish requirements.
Original PR description
In Poland, it is mandatory to use the previous day's currency rate. We used to achieve this by shifting the rate's date by one day. Since https://github.com/odoo/odoo/pull/231948, we already use the previous day's rate, so the shift is no longer needed. See: https://www.odoo.com/mail/message/1118975071
Preparation display orders no longer jump around when staff interact with individual order lines. Orders now keep their position until they move to a new preparation stage, making kitchen workflows easier to follow after a refresh.
Original PR description
**Steps to reproduce:** - Setup a preparation display - Go to the restaurant - Send an order to the kitchen, with 2 lines - Go to another table and send an order with 2 lines to the kitchen - On the display, click the first line of the first order - Reload the page - Order 1 and order 2 have swapped places **Why the fix:** We are currently sorting the orders based on their write_date, meaning that when we click a line, the write date is updated, and it goes to the end of the line. To prevent this, we are now using **last_stage_change** that is only updated when going from one stage to another. This means the cards will stay in the same order, and go to the back of the line once they change stage. To make it so that they are last when changing stage, we update the **last_stage_change** in the frontend as well when changing stage, because it was only done in the backend before this commit. opw-6361046 Forward-Port-Of: odoo/enterprise#124048
Invoices marked as excluded from follow-up are now also left out of follow-up email attachments and printed follow-up letters. This prevents customers from receiving documents for invoices the business intentionally removed from collection reminders.
Original PR description
Steps to reproduce: 1. Install Accounting and create an invoice for a customer which has a due date in the past 2. Make sure the payment term for the invoice is "Immediate Payment" and Send the…
Steps to reproduce: 1. Install Accounting and create an invoice for a customer which has a due date in the past 2. Make sure the payment term for the invoice is "Immediate Payment" and Send the invoice. 3. Open the contact form and click on the Customer Statement smart button 4. Exclude the invoice using the 'No Follow-Up' toggle 5. In the Accounting tab in the contact form, click on send 6. Open the internal link of the Content Template, go to the options tab and select 'Print Follow-up Letter' in Dynamic Reports 7. Save the configuration and send the email Issue: Excluded invoices still appeared as PDF attachments in the follow-up email and were merged into the printed follow-up letter PDF. Why this happens: Both `default_get` in `account_followup.manual_reminder` and `_get_invoices_to_print` in `res.partner` traversed `unreconciled_aml_ids` without filtering out lines where `no_followup = True`, so excluded invoices were included regardless. opw-6310602 Forward-Port-Of: odoo/enterprise#124541 Forward-Port-Of: odoo/enterprise#122249
Email buttons for appointments and event registrations now use the website tied to the appointment or event instead of a generic or recently used website address. This prevents customers on multi-website setups from being sent to the wrong site when managing bookings or registrations.
Original PR description
In Multiwebsite settings, when the public user interactions needs email generation (appointment or event flow), the email links are generated with a base url that does not corresponds to the one from…
In Multiwebsite settings, when the public user interactions needs email generation (appointment or event flow), the email links are generated with a base url that does not corresponds to the one from which the request started. Case 1: - Have website A and website B - Create an appointment page website A - Log in via website B - As public user, make an appointment in Website A - Check the generated email Issue: button links in the email will redirect to the wrong website, so users will encounter an issue when managing the appointment. This occurs because when an user log in, the system parameter 'web.base_url' is updated with the current url. This parameter is then used as fallback when we need to retrieve the base url without an active record Case 2: - Have website A and website B - Create an event and assign it to website B - As public user, access the event and register to it - Check the generated email Issue: button links in the email will redirect to the wrong website, so users will encounter an issue when managing the event. This occurs because the record `event.registration` has no website_id field and the base url is taken from the company default website (website A) Backport with improvements of 15bae202d8f1b5bf70bbc63b2d89025e9237e6cf opw-4146760 opw-4336369 Forward-Port-Of: odoo/enterprise#124555 Forward-Port-Of: odoo/enterprise#122669
Fixes how salary contract updates choose their template, using the current contract version when available or the offer's template otherwise. This helps HR teams generate contract updates with the expected document structure and reduces manual corrections.
Original PR description
contract update template should come from current version if any or from the offer's contract template. Task-6094733 Forward-Port-Of: odoo/enterprise#124638 Forward-Port-Of: odoo/enterprise#123450
Before this commit: If a user, with crm.leads linked to it, decided to request a password reset AND during that password reset process decided to activate the google oauth for their account, it would cause a crash. The reason is, during the password rest + oauth activation, self.env.user is an empty record set, which obviously will fail during the _is_portal check, due to its call to ensure_one() opw-6347228 Forward-Port-Of: odoo/odoo#275375
Original PR description
Before this commit: If a user, with crm.leads linked to it, decided to request a password reset AND during that password reset process decided to activate the google oauth for their account, it would cause a crash. The reason is, during the password rest + oauth activation, self.env.user is an empty record set, which obviously will fail during the _is_portal check, due to its call to ensure_one() opw-6347228 Forward-Port-Of: odoo/odoo#275375
Currently, overlays remain open when the POS screensaver is loaded [^1]. #### Steps to reproduce: - Open the POS interface. - Open a dropdown/popover menu (e.g., the navbar hamburger menu). - Wait for the screensaver (SaverScreen) to trigger due to inactivity. - The open dropdown menu remains visible on top of the screensaver. #### Issue Dropdowns and popovers are rendered as active overlays outside the main screen container. While the screensaver setup closes active dialogs, it doe
Original PR description
Currently, overlays remain open when the POS screensaver is loaded [^1]. #### Steps to reproduce: - Open the POS interface. - Open a dropdown/popover menu (e.g., the navbar hamburger menu). - Wait for the screensaver (SaverScreen) to trigger due to inactivity. - The open dropdown menu remains visible on top of the screensaver. #### Issue Dropdowns and popovers are rendered as active overlays outside the main screen container. While the screensaver setup closes active dialogs, it does not handle active overlays. #### Fix Retrieve the overlay service in SaverScreen and close all active overlays during its setup phase using a dedicated `closeAllOverlays` method. [^1]:  Forward-Port-Of: odoo/odoo#276672 Forward-Port-Of: odoo/odoo#274716
#### Description of the issue this PR addresses: - Tables containing only a `<caption>` (or a `<thead>` without a `<tbody>`) could reach the editor with no `<tbody>`. - Since table width and margin are moved to the `<tbody>` during setup in 19.0–19.2, such tables caused the editor to fail. - Table operations such as resizing and adding rows or columns also expect a `<tbody>` to exist. #### Desired behavior after PR is merged: - Tables without a `<tbody>` are normalized during editor setup
Original PR description
#### Description of the issue this PR addresses: - Tables containing only a `<caption>` (or a `<thead>` without a `<tbody>`) could reach the editor with no `<tbody>`. - Since table width and margin are moved to the `<tbody>` during setup in 19.0–19.2, such tables caused the editor to fail. - Table operations such as resizing and adding rows or columns also expect a `<tbody>` to exist. #### Desired behavior after PR is merged: - Tables without a `<tbody>` are normalized during editor setup. - `<thead>` is converted or merged into `<tbody>`. - A missing `<tbody>` is created when necessary, preventing the editor from crashing. task-6391354 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276588
### Issue: The 'Schedule an appointment' and 'Next Events' CTA buttons were not updated even when their conditions were satisfied. ### Steps to reproduce: - Install only Website. - In the configurator, choose 'Schedule Appointments' as the main objective. - Complete the setup and create the website. - The CTA button remains 'Contact Us' instead of 'Schedule an appointment'. ### Reason: The `get_cta_data()` method is overridden in specific modules to update the CTA button base
Original PR description
### Issue: The 'Schedule an appointment' and 'Next Events' CTA buttons were not updated even when their conditions were satisfied. ### Steps to reproduce: - Install only Website. - In the configurator, choose 'Schedule Appointments' as the main objective. - Complete the setup and create the website. - The CTA button remains 'Contact Us' instead of 'Schedule an appointment'. ### Reason: The `get_cta_data()` method is overridden in specific modules to update the CTA button based on conditions. However, it is called before those modules are installed, so the overridden logic is never executed. ### Fix: Ensure that `get_cta_data()` is called and the CTA button is updated after the required modules are installed. task-[6383681](https://www.odoo.com/odoo/project/974/tasks/6383681) --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#261781
Steps to reproduce: ------------------- - Install `mrp` and `sale_management` modules - Enable Units of Measure from settings - Create a storable product configured as a Kit: - Set UoM to Units - Add component and it's UoM in Kg in Product form. - Create and confirm a Sales Order with the kit product - Validate the generated delivery order - Print the delivery slip Issue: ------ The delivery slip correctly displays component quantities in Kg, but also shows an additional conve
Original PR description
Steps to reproduce: ------------------- - Install `mrp` and `sale_management` modules - Enable Units of Measure from settings - Create a storable product configured as a Kit: - Set UoM to Units - Add…
Steps to reproduce:
-------------------
- Install `mrp` and `sale_management` modules
- Enable Units of Measure from settings
- Create a storable product configured as a Kit:
- Set UoM to Units
- Add component and it's UoM in Kg in Product form.
- Create and confirm a Sales Order with the kit product
- Validate the generated delivery order
- Print the delivery slip
Issue:
------
The delivery slip correctly displays component quantities in Kg,
but also shows an additional converted quantity in Units (e.g., 1000 Units),
which is incorrect and misleading.
Cause:
------
During sale order confirmation, the following flow is executed:
`action_confirm → _action_confirm → _action_launch_stock_rule → _prepare_procurement_values`
In `_prepare_procurement_values`, the `packaging_uom_id` is set from the
sale order line UoM (Units) and propagated to the generated stock move:
https://github.com/odoo/odoo/blob/647febbf46160c000bf11af8325cc80d0916eb67/addons/sale_stock/models/sale_order_line.py#L296
When the delivery (picking) is created, kit components generate stock moves where:
- `product_uom` is defined in the component’s UoM (e.g., Kg)
- `packaging_uom_id` remains in Units (inherited from the sale order line)
While generating the delivery slip, `_get_aggregated_product_quantities`
computes `packaging_quantity` using `packaging_uom_id`:
https://github.com/odoo/odoo/blob/647febbf46160c000bf11af8325cc80d0916eb67/addons/stock/models/stock_move_line.py#L888
In Mrp this calls the template:
`stock_report_delivery_aggregated_move_lines`
https://github.com/odoo/odoo/blob/647febbf46160c000bf11af8325cc80d0916eb67/addons/mrp/report/report_deliveryslip.xml#L60
In this template, a condition renders packaging quantities when
`packaging_uom_id` differs from `product_uom`. As a result, quantities are
converted from the component UoM (Kg) into the packaging UoM (Units).
https://github.com/odoo/odoo/blob/647febbf46160c000bf11af8325cc80d0916eb67/addons/stock/report/report_deliveryslip.xml#L261
For kit components, this conversion is not meaningful and leads to incorrect
values (e.g., Kg → Units resulting in 1000 Units), causing misleading output
in the delivery slip.
Fix:
----
Add a `_compute_packaging_uom_id` override in `sale_mrp` and
`purchase_mrp` that resets `packaging_uom_id` back to
the component's own `product_uom` whenever the move originates from a
phantom BoM line, without touching `sale_line_id`/`purchase_line_id`
themselves.
<details>
<summary>Click here to see the results:</summary>
<p><strong>Before:</strong></p>
<div class="image-row">
<img src="https://github.com/user-attachments/assets/5d8b2154-d794-4ce4-90a6-1a0aeaca8604" />
</div>
<p><strong>After:</strong></p>
<div class="image-row">
<img src="https://github.com/user-attachments/assets/79d708de-19e3-452d-9033-322a297c38e9" />
</div>
</details>
---
opw-6136928
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#262705It can happen that _ref_vat has some lazy translate object. Without the self.env._ the translation would be ignored. (no translation language detected, skipping translation) runbot-941504 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#275561
Original PR description
It can happen that _ref_vat has some lazy translate object. Without the self.env._ the translation would be ignored. (no translation language detected, skipping translation) runbot-941504 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#275561
Problem: After updating a file name, the link popover still shows the original file name. Cause: The link popover always displays the attachment name instead of the current link content. Solution: Use the link content as the popover title so it reflects the updated file title. Steps to reproduce: - Go to To-Do → Create New. - Upload a file. - Change its title. - Observe that the title shown in the link popover still uses the original file name. task-6213840 --- I confirm I
Original PR description
Problem: After updating a file name, the link popover still shows the original file name. Cause: The link popover always displays the attachment name instead of the current link content. Solution: Use the link content as the popover title so it reflects the updated file title. Steps to reproduce: - Go to To-Do → Create New. - Upload a file. - Change its title. - Observe that the title shown in the link popover still uses the original file name. task-6213840 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276687 Forward-Port-Of: odoo/odoo#264127
The cron to send e-invoices might be stuck in an infinite loop if the error received is considered as "Networking error". zeep.exceptions.Fault hinerits from zeep.exceptions.Error so it will be catch as a "Networking error" and will retry to send the invoice in the next cron run. This PR proposes a way to set those documents sent to "error" state if a fault exception is detected. opw-6085114 Forward-Port-Of: odoo/odoo#276225
Original PR description
The cron to send e-invoices might be stuck in an infinite loop if the error received is considered as "Networking error". zeep.exceptions.Fault hinerits from zeep.exceptions.Error so it will be catch as a "Networking error" and will retry to send the invoice in the next cron run. This PR proposes a way to set those documents sent to "error" state if a fault exception is detected. opw-6085114 Forward-Port-Of: odoo/odoo#276225
Steps to reproduce: - Create a partner without pincode - Create a picking and Challan for that partner - Click print Will result in the following traceback- ```py Traceback (most recent call last): File "<1103>", line 710, in template_l10n_in_ewaybill_report_ewaybill_1103 File "<1103>", line 692, in template_l10n_in_ewaybill_report_ewaybill_1103_content File "<1103>", line 674, in template_l10n_in_ewaybill_report_ewaybill_1103_t_call_0 File "<1103>", line 86, in template_l10n
Original PR description
Steps to reproduce: - Create a partner without pincode - Create a picking and Challan for that partner - Click print Will result in the following traceback- ```py Traceback (most recent call last):…
Steps to reproduce:
- Create a partner without pincode
- Create a picking and Challan for that partner
- Click print
Will result in the following traceback-
```py
Traceback (most recent call last):
File "<1103>", line 710, in template_l10n_in_ewaybill_report_ewaybill_1103
File "<1103>", line 692, in template_l10n_in_ewaybill_report_ewaybill_1103_content
File "<1103>", line 674, in template_l10n_in_ewaybill_report_ewaybill_1103_t_call_0
File "<1103>", line 86, in template_l10n_in_ewaybill_report_ewaybill_1103_t_call_1
File "/home/odoo/odoo18/community/addons/l10n_in_ewaybill/models/l10n_in_ewaybill.py", line 727, in _ewaybill_generate_direct_json
**self._prepare_ewaybill_base_json_payload(),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/odoo18/community/addons/l10n_in_ewaybill_stock/models/l10n_in_ewaybill.py", line 289, in _prepare_ewaybill_base_json_payload
ewaybill_json = super()._prepare_ewaybill_base_json_payload()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/odoo18/community/addons/l10n_in_ewaybill/models/l10n_in_ewaybill.py", line 669, in _prepare_ewaybill_base_json_payload
**prepare_details(
^^^^^^^^^^^^^^^^
File "/home/odoo/odoo18/community/addons/l10n_in_ewaybill/models/l10n_in_ewaybill.py", line 641, in prepare_details
f"{place}{key}": fun(partner, place) if key == "StateCode" else fun(partner)
^^^^^^^^^^^^
File "/home/odoo/odoo18/community/addons/l10n_in_ewaybill/models/l10n_in_ewaybill.py", line 674, in <lambda>
"Pincode": lambda p: int(p.zip) if p.country_id.code == "IN" else 999999,
^^^^^^^^^^
ValueError: invalid literal for int() with base 10: ''
```
In this commit, we resolve the traceback
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#276180Before this commit: - When importing a **UBL** or **Factur-X (CII)** invoice, Odoo determines whether the document should be imported as an invoice or a credit note based on the `TaxExclusiveAmount (UBL)` /` TaxBasisTotalAmount (Factur-X)`. - In some rare cases, a valid invoice can contain a negative `TaxExclusiveAmount` / `TaxBasisTotalAmount` while still having a positive `TaxInclusiveAmount` / `GrandTotalAmount`. - In such situations, Odoo incorrectly imports the document as a credit note
Original PR description
Before this commit: - When importing a **UBL** or **Factur-X (CII)** invoice, Odoo determines whether the document should be imported as an invoice or a credit note based on the `TaxExclusiveAmount…
Before this commit: - When importing a **UBL** or **Factur-X (CII)** invoice, Odoo determines whether the document should be imported as an invoice or a credit note based on the `TaxExclusiveAmount (UBL)` /` TaxBasisTotalAmount (Factur-X)`. - In some rare cases, a valid invoice can contain a negative `TaxExclusiveAmount` / `TaxBasisTotalAmount` while still having a positive `TaxInclusiveAmount` / `GrandTotalAmount`. - In such situations, Odoo incorrectly imports the document as a credit note. Technical reason: - The method `_get_import_document_amount_sign()` uses `TaxExclusiveAmount` / `TaxBasisTotalAmount `to determine whether the imported document is an invoice or a refund. After this commit: - **UBL** now uses `TaxInclusiveAmount` instead of `TaxExclusiveAmount`, and **Factur-X** now uses `GrandTotalAmount `instead of `TaxBasisTotalAmount` to determine whether the document should be imported as an invoice or a credit note. - Prevent valid invoices with negative `TaxExclusiveAmount` / `TaxBasisTotalAmount` from being incorrectly converted into credit notes. Task-6321262 Forward-Port-Of: odoo/odoo#276307 Forward-Port-Of: odoo/odoo#271829
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#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#275276
_reset_inventory() counter balances the stock implied by the move history when a product becomes storable, to reset the valuation of goods received while untracked. It assumed the product had no quants. But unticking Track Inventory does not clear the existing quants, so toggling it off then on again counter balances stock that is still on hand. The quants then desynchronize from their moves and the historical stock and valuation reports show quantities before the product ever existed. Onl
Original PR description
_reset_inventory() counter balances the stock implied by the move history when a product becomes storable, to reset the valuation of goods received while untracked. It assumed the product had no quants. But unticking Track Inventory does not clear the existing quants, so toggling it off then on again counter balances stock that is still on hand. The quants then desynchronize from their moves and the historical stock and valuation reports show quantities before the product ever existed. Only counter balance the part of the move history that is not already on hand. Steps to reproduce: - Create a storable product tracked by lots, 10 units on hand - Untick then re-tick "Track Inventory" on the product - Inventory > Reporting > Inventory at Date, pick a date before the product existed > The report shows 10 units on hand although there was no stock at that date. opw-6373051 Forward-Port-Of: odoo/odoo#275565
Steps to reproduce: - Create a promotions program with a rule granting 1 point per currency unit spent (minimum 2 items and 50.00 spent) and a reward "10% discount on the cheapest product" for 1 point - Open a PoS session and add 2 products to trigger the program - Keep adding products to the order Issue: A new "10% on the cheapest product" line is added for every product added. In eCommerce and Sales the discount is only applied once. Cause: The rule grants far more points than the r
Original PR description
Steps to reproduce: - Create a promotions program with a rule granting 1 point per currency unit spent (minimum 2 items and 50.00 spent) and a reward "10% discount on the cheapest product" for 1…
Steps to reproduce: - Create a promotions program with a rule granting 1 point per currency unit spent (minimum 2 items and 50.00 spent) and a reward "10% discount on the cheapest product" for 1 point - Open a PoS session and add 2 products to trigger the program - Keep adding products to the order Issue: A new "10% on the cheapest product" line is added for every product added. In eCommerce and Sales the discount is only applied once. Cause: The rule grants far more points than the reward costs, so the reward remains claimable after being applied. The auto-claim loop of `updateRewards` therefore re-applies it on every order change, stacking one discount line per change. The already-applied check in `getClaimableRewards` only covered 'coupons' programs, while `_get_claimable_rewards` in sale_loyalty also skips discount rewards already present on the order lines. Solution: When auto-claiming, skip discount rewards that are already applied on the order, unless they belong to a payment program (ewallet, gift card). The reward can still be claimed manually several times through the Reward button by spending more points. opw-6380421 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276300
Steps to reproduce: 1. install mail 2. Send a voice message to anyone from the discuss app 3. Open the ui in mobile and see the voice messege duration Issue: - time is showing in two lines Solution: - Adjust the spacing of the voice player controls for small screens using responsive Bootstrap utility classes and prevent the duration text from shrinking, ensuring it remains on a single line while preserving the existing layout on larger screens. <table width="100%"> <tr> <th
Original PR description
Steps to reproduce:
1. install mail
2. Send a voice message to anyone from the discuss app
3. Open the ui in mobile and see the voice messege duration
Issue:
- time is showing in two lines
Solution:
- Adjust the spacing of the voice player controls for small screens using responsive Bootstrap utility classes and prevent the duration text from shrinking, ensuring it remains on a single line while preserving the existing layout on larger screens.
<table width="100%">
<tr>
<th>Before</th>
<th>After</th>
</tr>
<tr>
<td align="center">
<img alt="After" src="https://github.com/user-attachments/assets/98f41d1a-9082-4d5c-a34b-c9181e643e0a">
</td>
<td align="center">
<img alt="Before" src="https://github.com/user-attachments/assets/0f879988-b2e6-46c9-ba5a-0f935fde40ec">
</td>
</tr>
</table>
opw-6328609
Forward-Port-Of: odoo/odoo#271768When sending a batch of invoice, we first generate an unique PDF file regrouping every invoice. When at least one invoice needs more than one page to be rendered entirely, the number of pages will be greater than the number of invoices. In this case, we use <hX> tags to split invoices: https://github.com/odoo/odoo/blob/5ba945cdba6be3ff8838e56784ade16c6200de84/odoo/addons/base/models/ir_actions_report.py#L916-L923 This tag is added for PDF files, using the invoice's title: https://g
Original PR description
When sending a batch of invoice, we first generate an unique PDF file regrouping every invoice. When at least one invoice needs more than one page to be rendered entirely, the number of pages will be…
When sending a batch of invoice, we first generate an unique PDF file regrouping every invoice. When at least one invoice needs more than one page to be rendered entirely, the number of pages will be greater than the number of invoices. In this case, we use <hX> tags to split invoices: https://github.com/odoo/odoo/blob/5ba945cdba6be3ff8838e56784ade16c6200de84/odoo/addons/base/models/ir_actions_report.py#L916-L923 This tag is added for PDF files, using the invoice's title: https://github.com/odoo/odoo/blob/5ba945cdba6be3ff8838e56784ade16c6200de84/addons/web/views/report_templates.xml#L627 In some localization modules, this title shouldn't be displayed and it is removed. Example for Chile: https://github.com/odoo/odoo/blob/5ba945cdba6be3ff8838e56784ade16c6200de84/addons/l10n_cl/views/report_invoice.xml#L149 In this case, no `<h3>` tag will be added and an error is raised as we cannot separate invoices. We propose to add an empty `<h3>` tag if the document title is not defined. opw-6281187 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#269100
Update the XPath of the empty header of the 'Pay Now' button column to the last header of the table. Use a deterministic XPath expression for the 'Pay Now' button column cell. as the last cell of the table. Forward-Port-Of: odoo/odoo#272710
Original PR description
Update the XPath of the empty header of the 'Pay Now' button column to the last header of the table. Use a deterministic XPath expression for the 'Pay Now' button column cell. as the last cell of the table. Forward-Port-Of: odoo/odoo#272710
Registration token now needs to be valid for much longer than 1 hour due to manual verifications Forward-Port-Of: odoo/odoo#276550
Original PR description
Registration token now needs to be valid for much longer than 1 hour due to manual verifications Forward-Port-Of: odoo/odoo#276550
`forEach` is a synchronous operation, so it doesn't support promises. We refactor its usage to use `for` loops. task-4794299 Forward-Port-Of: odoo/odoo#276590 Forward-Port-Of: odoo/odoo#275262
Original PR description
`forEach` is a synchronous operation, so it doesn't support promises. We refactor its usage to use `for` loops. task-4794299 Forward-Port-Of: odoo/odoo#276590 Forward-Port-Of: odoo/odoo#275262
In Multiwebsite settings, when the public user interactions needs email generation (appointment or event flow), the email links are generated with a base url that does not corresponds to the one from which the request started. - Have website A and website B - Create an event and assign it to website B - As public user, access the event and register to it - Check the generated email Issue: button links in the email will redirect to the wrong website, so users will encounter an issue when
Original PR description
In Multiwebsite settings, when the public user interactions needs email generation (appointment or event flow), the email links are generated with a base url that does not corresponds to the one from which the request started. - Have website A and website B - Create an event and assign it to website B - As public user, access the event and register to it - Check the generated email Issue: button links in the email will redirect to the wrong website, so users will encounter an issue when managing the event. This occurs because the record `event.registration` has no website_id field and the base url is taken from the company default website (website A) opw-4146760 opw-4336369 Forward-Port-Of: odoo/odoo#276070 Forward-Port-Of: odoo/odoo#274051
Before this commit, in the test `test_ticket_price_with_currency_conversion` the exchange rate was not correctly assigned because `rate_ids` was not initialized when running tests on app install. As a result the the rate was left empty and the currency conversion was not applied. This commit creates the rate_ids in the test to make sure the rates are correctly applied. Runbot error: https://runbot.odoo.com/odoo/error/242431 --- I confirm I have signed the CLA and read the PR guidelines
Original PR description
Before this commit, in the test `test_ticket_price_with_currency_conversion` the exchange rate was not correctly assigned because `rate_ids` was not initialized when running tests on app install. As a result the the rate was left empty and the currency conversion was not applied. This commit creates the rate_ids in the test to make sure the rates are correctly applied. Runbot error: https://runbot.odoo.com/odoo/error/242431 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276218 Forward-Port-Of: odoo/odoo#265503
Steps to reproduce: - Install `project`, open a project and switch to the Gantt view - Start dragging a task while holding Ctrl (copy mode), then switch browser tab with Ctrl+Tab (or Ctrl+Shift+Tab, Ctrl+PgUp/PgDn) - Come back to the Gantt tab and drop the task without holding Ctrl Issue: The task is duplicated instead of rescheduled. Cause: The copy/reschedule behavior is tracked through window keydown/keyup listeners on the Control key. While the document is hidden, the keyup for Co
Original PR description
Steps to reproduce: - Install `project`, open a project and switch to the Gantt view - Start dragging a task while holding Ctrl (copy mode), then switch browser tab with Ctrl+Tab (or Ctrl+Shift+Tab, Ctrl+PgUp/PgDn) - Come back to the Gantt tab and drop the task without holding Ctrl Issue: The task is duplicated instead of rescheduled. Cause: The copy/reschedule behavior is tracked through window keydown/keyup listeners on the Control key. While the document is hidden, the keyup for Control is never received, so the drag sequence resumes with a stale "Ctrl pressed" state and the drop is treated as a copy. Fix: Keyboard and pointer states cannot be reliably tracked while the document is hidden, so cancel any ongoing drag sequence from `makeDraggableHook` as soon as the tab is no longer visible (through the `visibilitychange` event). This applies to every drag and drop instance built on the hook builder. opw-6298440 Forward-Port-Of: odoo/odoo#276859
Only admin users have read access to the `payment.provider` model. Opening the PoS payment method form as a non-admin would raise an access error because the `online_payment_provider_ids` many2many field tries to fetch `payment.provider` records on form load. Grant read-only access on `payment.provider` to `group_pos_manager` so POS admins can use the field. Restrict the field's group in the form view to `point_of_sale.group_pos_manager,base.group_system` so it is not rendered for users witho
Original PR description
Only admin users have read access to the `payment.provider` model. Opening the PoS payment method form as a non-admin would raise an access error because the `online_payment_provider_ids` many2many field tries to fetch `payment.provider` records on form load. Grant read-only access on `payment.provider` to `group_pos_manager` so POS admins can use the field. Restrict the field's group in the form view to `point_of_sale.group_pos_manager,base.group_system` so it is not rendered for users without either role. opw-6208656 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276678 Forward-Port-Of: odoo/odoo#263837
Follow-up to #247929, which limited `forever` recurrences to a 15-year horizon (`calendar.max_recurrence_years`) for *yearly* and *monthly* frequencies but left *weekly* and *daily* on the hard cap `MAX_RECURRENT_EVENT` (720), ignoring the parameter. A weekly `forever` event therefore always materializes 720 occurrences (~14 years) regardless of `calendar.max_recurrence_years`, and a daily one ignores the parameter too. This applies the year limit to weekly and daily as well, accounting for th
Original PR description
Follow-up to #247929, which limited `forever` recurrences to a 15-year horizon (`calendar.max_recurrence_years`) for *yearly* and *monthly* frequencies but left *weekly* and *daily* on the hard cap `MAX_RECURRENT_EVENT` (720), ignoring the parameter. A weekly `forever` event therefore always materializes 720 occurrences (~14 years) regardless of `calendar.max_recurrence_years`, and a daily one ignores the parameter too. This applies the year limit to weekly and daily as well, accounting for the number of selected weekdays and the interval, while still capping at `MAX_RECURRENT_EVENT`. Steps to reproduce: 1. Set `calendar.max_recurrence_years` to e.g. 2. 2. Create a weekly event repeating "forever". 3. Before: 720 occurrences (~14 years). After: ~106 (2 years). task / context: extends #247929. Forward-Port-Of: odoo/odoo#270348
there is a typo in https://github.com/odoo/odoo/pull/275945 leading to a traceback. opw-6398050 Forward-Port-Of: odoo/odoo#277223
Original PR description
there is a typo in https://github.com/odoo/odoo/pull/275945 leading to a traceback. opw-6398050 Forward-Port-Of: odoo/odoo#277223
Features or functions removed from Odoo
Description of the issue/feature this PR addresses: Removed obsolete code checking "this.config.self_ordering_mode !== 'qr_code'". Since "qr_code" is not a valid value of the "self_ordering_mode" selection field. The condition is always true. Forward-Port-Of: odoo/odoo#276321 Forward-Port-Of: odoo/odoo#273245
Original PR description
Description of the issue/feature this PR addresses: Removed obsolete code checking "this.config.self_ordering_mode !== 'qr_code'". Since "qr_code" is not a valid value of the "self_ordering_mode" selection field. The condition is always true. Forward-Port-Of: odoo/odoo#276321 Forward-Port-Of: odoo/odoo#273245
Miscellaneous changes
Forward-Port-Of: odoo/enterprise#124221
Original PR description
Forward-Port-Of: odoo/enterprise#124221
all_sm is referenced by two branches of the outer UNION, so PostgreSQL materializes it (a CTE used more than once is an optimization fence). Every query on report_stock_quantity therefore builds the forecast for all products/warehouses first and filters afterwards, so predicates like product_id can't reach the stock_move scan. This hurts single-product lookups such as _read_group() in mrp_report_bom_structure._get_stock_availability(), called repeatedly per component while rendering a BoM rep
Original PR description
all_sm is referenced by two branches of the outer UNION, so PostgreSQL materializes it (a CTE used more than once is an optimization fence). Every query on report_stock_quantity therefore builds the…
all_sm is referenced by two branches of the outer UNION, so PostgreSQL materializes it (a CTE used more than once is an optimization fence). Every query on report_stock_quantity therefore builds the forecast for all products/warehouses first and filters afterwards, so predicates like product_id can't reach the stock_move scan.
This hurts single-product lookups such as _read_group() in mrp_report_bom_structure._get_stock_availability(), called repeatedly per component while rendering a BoM report: each call does a full stock_move scan plus two sorts that spill to disk.
Marking all_sm NOT MATERIALIZED lets the planner inline it and push the product_id/warehouse_id filter down to an index scan, removing the full scan, the GENERATE_SERIES expansion and the on-disk sorts. warehouse_cte stays materialized (small and cheap).
Query generated from the _read_group:
```SQL
SELECT MIN("report_stock_quantity"."date")
FROM "report_stock_quantity"
WHERE (
(
(
(
("report_stock_quantity"."state" = 'forecast')
AND ("report_stock_quantity"."date" >= '2026-07-08')
)
AND ("report_stock_quantity"."product_id" = 15743)
)
AND ("report_stock_quantity"."product_qty" >= 1.0)
)
AND ("report_stock_quantity"."warehouse_id" = 4)
)
AND ("report_stock_quantity"."company_id" IN (1))
```
Before:
800 ms by _read_group in _get_stock_availability().
For opening a BoM overview with hundred of components takes 17 sec.
After:
25 ms by _read_group in _get_stock_availability().
Same BoM overview drops to 3 sec to open.
cc @Aurelienvd
I can send you EXPLAIN ANALYZE, but I prefer to not putting them here to avoid data leakage.
Forward-Port-Of: odoo/odoo#275457
Forward-Port-Of: odoo/odoo#275022