Daily updates from Odoo
Monday, July 20, 2026
31 changes · saas-19.1
Resolved issues and error corrections
Argentine electronic invoices for final consumers now validate the customer's identification type before processing. This prevents a technical error during invoice confirmation and instead shows a clear warning when the ID type is not accepted.
Original PR description
After applying the community fix, a new issue appears in enterprise. **Steps to reproduce:** - Install the `l10n_ar_edi` module. - Go to Customers and create a new customer with: - Country: Argentina…
After applying the community fix, a new issue appears in enterprise. **Steps to reproduce:** - Install the `l10n_ar_edi` module. - Go to Customers and create a new customer with: - Country: Argentina - Identification Type: `Passport` - Identification Number: `1234567890a` - AFIP Responsibility Type: `Consumidor Final` - Create a new invoice for this customer with: - Journal: `Electronic Invoice (FE)` - Document Type: `(6) INVOICES B` - Try to confirm the invoice. **Error:** ValueError: invalid literal for int() with base 10: '1234567890a' **Expected behaviour:** For Argentinean contacts with AFIP Responsibility Type `Consumidor Final`, the identification type must be `DNI` or `CUIL/CUIT`. If another identification type is used, the user should receive a proper warning instead of a traceback. (see [1]) **Root Cause:** After the community fix, `_get_id_number_sanitize()` can return an alphanumeric value, which is later converted with `int()` at [2] and [3], causing an error. **Fix:** This commit adds an explicit validation for Argentinean contacts with AFIP Responsibility Type Consumidor Final before converting the identification number to an integer, ensuring the user receives the proper warning message. [1]: https://www.odoo.com/mail/message/1118392033 [2]: https://github.com/odoo/enterprise/blob/064812dd9f1074f287fee677ba35ea4aeef3ab3f/l10n_ar_edi/models/account_move.py#L780-L798 [3]: https://github.com/odoo/enterprise/blob/064812dd9f1074f287fee677ba35ea4aeef3ab3f/l10n_ar_edi/models/account_move.py#L927-L931 Related community PR: https://github.com/odoo/odoo/pull/272651 opw-6333998 Forward-Port-Of: odoo/enterprise#123729
The planning kanban card now shows allocated time in a simpler, consistent format such as (4h30). Removing the separate percentage display fixes uneven spacing and makes schedules easier to scan.
Original PR description
Currently, the allocated hours and allocated percentage are misaligned in the planning kanban card, causing them to appear uneven or have inconsistent spacing. This fix removes the allocated percentage and formats the allocated hours to display like (4h30). task-5085363 Forward-Port-Of: odoo/enterprise#98776
Envia shipping rate requests now include insurance in the format expected by the carrier API. This ensures quotes reflect the selected insurance amount, so customers and sales teams see more accurate delivery costs.
Original PR description
Issue ----- Insurance request is not reflected on the rating given by Envia. Steps to reproduce ----- - Create a MX company - Set up Envia - Fedex Nacional Economico (ground) - Create a MX client - Create a product (with some weight) - Create a SO - Add the product - Add envia delivery and get a rate - Update Envia delivery method - Set insurance to 50% - Go back to the SO - Add envia delivery and get a rate > The rate is the same both times Cause ----- We are passing the insurance value as a `insurance` field on the shipment, which is not what the API expects. We should instead pass it in `additionalServices` as shown in the example of https://docs.envia.com/docs/additional-services#how-to-add-services-to-a-shipment Follow up of 7258e4a ----- Ticket: opw-5254952 Forward-Port-Of: odoo/enterprise#124700
Barcode transfers now apply a default limit when loading reusable packages, preventing very large package lists from slowing down the screen. This reduces long wait times for warehouses with tens of thousands of packages and makes transfer opening more reliable.
Original PR description
# How to reproduce - Have a lot of reusable & locationless packages (e.g. > 10 000) - Go to any transfer via the barcode application # The issue There is a very long loading time, even in local…
# How to reproduce - Have a lot of reusable & locationless packages (e.g. > 10 000) - Go to any transfer via the barcode application # The issue There is a very long loading time, even in local testing. The client of the tickets experiences loadings up to 120 seconds with 50k packages # Cause When opening a transfer, we load barcode data by doing an API call to `_get_stock_barcode_data` : https://github.com/odoo/enterprise/blob/fe058ef501767b7ed9758fc9264f664b32c6bae8/stock_barcode/models/stock_picking.py#L85 During this we preload a lot of records, notably packages : https://github.com/odoo/enterprise/blob/fe058ef501767b7ed9758fc9264f664b32c6bae8/stock_barcode/models/stock_picking.py#L128 The issue is that in the fields we read for the packages, two of them (`location_dest_id` & `contained_quant_ids`) have a `_read_group` in their compute (or in the compute of one of the fields they depend on) : https://github.com/odoo/odoo/blob/625e6bcbd66c45ea2f699df14e2ea12e2e28a893/addons/stock/models/stock_package.py#L65 https://github.com/odoo/odoo/blob/625e6bcbd66c45ea2f699df14e2ea12e2e28a893/addons/stock/models/stock_package.py#L146 Fortunately, this does not mean that we make a query for every records. Instead, in Odoo, we fetch records in batch of 1000. So, for the case of the client, every time he loads the database, the backend does 50 000 / 1000 x 2 = 100 queries, which hinders performance a lot A [PERF] commit was done to limit the number of packages that are fetched base on a config parameter. The problem is that this parameter does not have a default value, so clients still end up with the problem. [PERF]: https://github.com/odoo/enterprise/commit/efe18bc1ea479270e42846986d7ed449b0865617 # Proposed Solution Add a default value for that config parameter. The exact value is up to discussion opw-6200730 Forward-Port-Of: odoo/enterprise#123696
Philippine check printing now rounds the cents portion of amounts in words to two decimal places, even when the currency is configured with more precision. This prevents checks from showing incorrect fractional text such as 1268/100 instead of 13/100, reducing confusion and payment errors.
Original PR description
Current behavior: --- When paying with checks, if the currency has more than 2 decimals, the decimal amount is printed with more than 2 decimals. Steps to reproduce: --- 1. Switch to PH company 2. Set setting Check Layout as "Print Check - PH" 3. In the PHP currency, change rounding factor to 0.0001 4. In Decimal accuracy > product price, set 4 digits 5. Create a new Vendor Payment, payment method Check, amount 100.1268 PHP 6. Results: One Hundred and 1268/100, should be 13/100 Expected behavior: --- The xx/100 part of amount in words text in the check should always be rounded to 2 decimals. opw-6302337 Forward-Port-Of: odoo/enterprise#122581 Forward-Port-Of: odoo/enterprise#121913
This fixes an issue where tax returns could keep an outdated status after the allowed workflow steps were changed, which could cause errors when viewing return lists. Existing returns are now adjusted to valid statuses when workflows change, helping upgrades and country-specific tax processes run more reliably.
Original PR description
To reproduce the issue: 1) Create a company in Belgium 2) Instantiate its returns and review, submit and pay one of the VAT returns 3) Change the states_worklfow of the VAT return so that it only…
To reproduce the issue: 1) Create a company in Belgium 2) Instantiate its returns and review, submit and pay one of the VAT returns 3) Change the states_worklfow of the VAT return so that it only accepts "review" and "submit" stages, not "paid" anymore 4) Go to the list of returns, remove the TODO filter => traceback The problem is here that the existing returns don't recompute their state when the workflow of the type is modified. In some cases, this is fine, but it others, it's annoying. In our example, the terminal state changed, so all the returns in that terminal stage should change their state to the new terminal one. "paid" is not an accepted value anymore, it should become "submitted". Moreover, when the workflow is changed, the selection field actually containing the state must also change. As it is, it seems to work because "state" of account.return is stored, but the value it's based on (the workflow field) won't be consistent with it. It's not annoying now, but those inconsistencies could become a big source of trouble in the future (we know that from experience ... I'm looking at you, version 8 ! è-é). This issue typically happens at upgrade. We had cases in FR and AE already. We solve that by a generic override of the write to sort things out when such change needs to happen. An upgrade PR will also be done to adapt the script so that we eventually solve the inconsistencies on dbs that have already migrated to 19.0. Forward-Port-Of: odoo/enterprise#124144
Users can no longer trigger a server error by creating a new entry directly from the Add to or Reduce From fields in Sales Commission Adjustments. The change disables that shortcut so adjustments can be edited safely without interrupting the workflow.
Original PR description
Steps to reproduce: - Go to Sales > Commission > Adjustments - In Add to/Reduce From, type a name with no match - Click Create '<name>' -> server crashes (RPC_ERROR) Cause: `sale.commission.plan.user._rec_name` is a Many2one (`user_id`). Default `name_create` wrote the typed text into that integer column, which Postgres rejected. Fix: - Disable quick-create on `add_user_id`/`reduce_user_id` in the Adjustments list view Version: saas-18.3 to master opw-6384909 Forward-Port-Of: odoo/enterprise#124431
Payment references for Swiss QR-IBAN vendor payments are now sanitized before generating ISO 20022 payment files. This helps prevent bank rejections caused by unsupported characters while keeping valid QR payment references intact.
Original PR description
**Steps to reproduce:** * Configure a Swiss company with the Swiss accounting localization. * Install `account_sepa`. * Configure a bank journal with a valid Swiss **QR-IBAN**. * Create a vendor with…
**Steps to reproduce:** * Configure a Swiss company with the Swiss accounting localization. * Install `account_sepa`. * Configure a bank journal with a valid Swiss **QR-IBAN**. * Create a vendor with a valid Swiss bank account. * Under **Accounting -> Configuration -> Settings** enable QR Payments. * Go to the vendor and enter the correct QR-IBAN number under the Accounting Section. * Navigate to **Accounting -> Configuration -> Journals** and in the **bank** journal, add the account number of QR-IBAN. * Create and post a vendor payment using the bank journal. * Set the payment reference (`ref`) to contain the `°` character. * Add the payment to a batch payment and generate the ISO 20022 payment file. * Inspect the generated XML. **Observed Behaviour:** The `°` character is preserved in the payment reference for QR-IBAN payments, producing an ISO 20022 file that may be rejected by Swiss banks because it does not comply with the EPC minimum character set. **Cause:** The QR-IBAN branch validates the structured reference before sanitizing it, allowing unsupported characters to remain in the generated XML. **Fix:** Sanitize the payment reference before validating it as a QR structured reference, ensuring invalid characters are removed while preserving valid QR references. opw - 6350931 Forward-Port-Of: odoo/enterprise#124299 Forward-Port-Of: odoo/enterprise#123267
Employee appraisals now use the template assigned to the employee's department instead of keeping a generic default. This helps HR teams apply the right appraisal questions and process for each department without manual correction.
Original PR description
**Problem:** When a template is restricted to a department, appraisals for employees of that department do not use it — a generic (global) template is used instead. **Steps to reproduce:** 1. Create…
**Problem:** When a template is restricted to a department, appraisals for employees of that department do not use it — a generic (global) template is used instead. **Steps to reproduce:** 1. Create an appraisal template and restrict it to a department. 2. Make sure at least one global (no department) template also exists. 3. Open a new appraisal and select an employee of that department. 4. Observe the appraisal template stays on the global one. **Current behavior:** The global template that was auto-selected before an employee was chosen is kept, ignoring the department-restricted template. **Expected behavior:** The template restricted to the employee's department is selected. **Cause of the issue:** On a new appraisal the template is computed before an employee (and therefore a department) is set, so a global template is pre-selected. When the employee is then chosen, `_compute_appraisal_template` reruns but the `appraisal.appraisal_template_id or ...` clause short-circuits on the already set global template and never falls through to the department's template. **Fix:** Evaluating the department's template before the existing value ensures a department-restricted template takes precedence over a template that was only auto-selected as a default. The existing value is still preserved when the department has no template of its own, so deliberate choices are untouched. opw-6293936 Forward-Port-Of: odoo/enterprise#121377
This PR hides the Upgrade button for modules imported via file, as these modules cannot be upgraded without re-importing a file. Displaying the upgrade button in such cases could lead to confusion or unexpected behavior. Task: 4643890 Forward-Port-Of: odoo/odoo#213122
Original PR description
This PR hides the Upgrade button for modules imported via file, as these modules cannot be upgraded without re-importing a file. Displaying the upgrade button in such cases could lead to confusion or unexpected behavior. Task: 4643890 Forward-Port-Of: odoo/odoo#213122
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#270544
Steps to reproduce: --- Case 1: 1. Add a carousel at the top of the page. 2. Apply the parallax effect to all slides. 3. Reload the page and navigate to the second slide without scrolling. 4. Scroll slightly and observe the jump in background position. 5. Scroll back to the top and notice the changed background position. Case 2: 1. Add a carousel to the page. 2. Apply the parallax effect to each slide. 3. Add text over the carousel and enable animation on appearance and on eve
Original PR description
Steps to reproduce: --- Case 1: 1. Add a carousel at the top of the page. 2. Apply the parallax effect to all slides. 3. Reload the page and navigate to the second slide without scrolling. 4. Scroll…
Steps to reproduce: --- Case 1: 1. Add a carousel at the top of the page. 2. Apply the parallax effect to all slides. 3. Reload the page and navigate to the second slide without scrolling. 4. Scroll slightly and observe the jump in background position. 5. Scroll back to the top and notice the changed background position. Case 2: 1. Add a carousel to the page. 2. Apply the parallax effect to each slide. 3. Add text over the carousel and enable animation on appearance and on every time. 4. Slide between images and observe the bouncing effect. https://github.com/user-attachments/assets/d11ee430-8b95-4903-b001-1b9ba8ee58cd Why this change? --- When the parallax effect is applied to carousel slides, only the first slide is initialized with the correct [parallaxHeight](https://github.com/odoo/odoo/blob/616e82d7b3a53b1facf481e783baed3e99393d3c/addons/website/static/src/interactions/parallax/parallax.js#L49). The remaining slides are initialized while hidden, resulting in a computed height of 0. When scrolling, the background height is recalculated with the correct value, producing a visible jump effect. Additionally, when text or any animated content is added on top of the carousel (especially animations triggered on appearance or on every slide change), a [resize event](https://github.com/odoo/odoo/blob/616e82d7b3a53b1facf481e783baed3e99393d3c/addons/website/static/src/interactions/animation.js#L111) is triggered on each transition. This causes `updateBackgroundHeight()` to run again. Since hidden slides were initialized with height 0 and later receive their actual height, the recalculation produces a noticeable bouncing effect. After this commit: --- The parallax computation now relies on the parent element’s `getBoundingClientRect().height` when used inside a carousel. Since the carousel slide height is already defined and consistent, this ensures correct initialization even when the slide is not yet visible. OPW: 5909351 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#250290
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. 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