Daily updates from Odoo
Monday, July 20, 2026
28 changes · saas-19.2
Enhancements to existing features
The timesheet assistant now supports keyboard controls for moving through and selecting suggestions. This improves accessibility and helps users manage timesheet entries more efficiently without relying on a mouse.
Original PR description
Implement full keyboard controls for managing timesheet suggestions to improve accessibility and user efficiency. This adds support for the following interactions: - ArrowUp / ArrowDown to navigate focus through rows - Space to select/deselect the focused item (and set the selection anchor) - Shift + Arrows to select continuous ranges of suggestions task: 6267620
The French balance sheet report now places establishment costs before fixed assets and includes additional impairment accounts in the relevant tangible fixed asset lines. This improves report consistency and helps ensure fixed asset values are presented more completely for French reporting.
Original PR description
Move establishment costs before fixed assets in the French balance sheet, and include the missing 2912, 2913, 2914, and 2915 impairment accounts in the relevant tangible fixed asset amortization/provision lines. task-6226138 Forward-Port-Of: odoo/enterprise#124562 Forward-Port-Of: odoo/enterprise#117689
No task ID Forward-Port-Of: odoo/odoo#276936
Original PR description
No task ID Forward-Port-Of: odoo/odoo#276936
Resolved issues and error corrections
WhatsApp message lists no longer fail when a related calendar appointment or contact has been deleted. This keeps reminder history readable and prevents one broken reminder from stopping other queued WhatsApp messages from being sent.
Original PR description
Root cause: WhatsApp reminders are logged as mail.message records on calendar.attendee, which is not a mail.thread. Deleting a calendar event or a partner removes the attendees through the database…
Root cause: WhatsApp reminders are logged as mail.message records on calendar.attendee, which is not a mail.thread. Deleting a calendar event or a partner removes the attendees through the database cascades on calendar.attendee.event_id and calendar.attendee.partner_id, which do not call calendar.attendee.unlink, so each reminder keeps a model and res_id pointing to an attendee that no longer exists. mail.message access filters the related documents through exists() since https://github.com/odoo/odoo/commit/617d4a32409b730e0fb7d351553e4c6e10bf6c5f, so a reminder whose attendee is gone is no longer readable by anyone but its author or recipient. whatsapp.message.body reads mail.message.body without sudo, so opening WhatsApp > Messages raises an access error on the orphaned reminders, and the queue retry cron hits the same error and stops sending the rest of the queue. Fix: Read the related body with sudo. Orphaned messages are not specific to whatsapp, any model that is not a mail.thread keeps its messages after a record is deleted, so cleaning them up is a mail framework concern rather than a whatsapp one. One message the user cannot read should not block the whole message list. Who can read the body stays controlled by the access rights of whatsapp.message itself. Steps to reproduce: 1. Install WhatsApp and configure an account 2. Create an Appointment Type with a WhatsApp reminder 3. Book an appointment to create the calendar event 4. Settings > Technical > Automation > Scheduled Actions > Calendar: Event Reminder > Run Manually so the reminder is sent 5. Open the calendar event and delete it with the trash icon, not Cancel 6. Open WhatsApp > Messages => Access error, the user has no access to the mail.message Ticket [link](https://www.odoo.com/odoo/project.task/6297099) opw-6297099
QR-IBAN payment references are now sanitized before validation when generating ISO 20022 payment files. This helps prevent Swiss bank rejections caused by unsupported characters in payment references while keeping valid QR 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
The restaurant appointment point-of-sale test was adjusted to avoid repeating a data reload step that could cause false test failures. This improves test reliability without changing the normal user experience, since users are not expected to perform these immediate back-to-back reloads.
Original PR description
The `RestaurantAppointmentTour` performed two consecutive "Reload Data" actions. The first reload starts synchronizing data from the server to IndexedDB. If the second reload is triggered before the synchronization completes, it deletes the IndexedDB while it is still in use, causing the synchronization process to crash. This race condition can be reproduced locally by running the tour with `cpu_throttling` enabled. In practice, users never trigger two consecutive reloads, so the second reload step in the tour is unnecessary. Remove it to avoid the artificial race condition while preserving the intended test coverage. Task-[6364951](https://www.odoo.com/odoo/project/1737/tasks/6364951) Runbot Error-[941343](https://runbot.odoo.com/odoo/error/941343), [941344](https://runbot.odoo.com/odoo/error/941344), [941345](https://runbot.odoo.com/odoo/error/941345)
This fix keeps existing tax returns aligned when their allowed workflow steps are changed, such as during upgrades. It prevents errors when viewing return lists and moves returns out of statuses that are no longer valid.
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 typing an unknown name in the Add to or Reduce From fields on commission adjustments. The change removes an unsupported quick-create option, keeping the adjustment workflow stable and avoiding confusing error messages.
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
Invoices marked as excluded from follow-up will no longer be attached to reminder emails or included in printed follow-up letters. This prevents customers from receiving payment reminders for invoices the business intentionally left out.
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#124750 Forward-Port-Of: odoo/enterprise#122249
Polish currency rates from NBP are no longer shifted by an extra day because the system already uses the required previous day's rate. This prevents rates from being dated too far back and helps Polish accounting use the correct exchange rate dates.
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 Forward-Port-Of: odoo/enterprise#123849
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 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
Add a regression test ensuring that setting a relative date global filter on a dynamic Odoo pivot does not crash while the pivot table is reloading. Task: 6306263 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Original PR description
Add a regression test ensuring that setting a relative date global filter on a dynamic Odoo pivot does not crash while the pivot table is reloading. Task: 6306263 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Rather than assign `log_target` and blow up if the third branch is taken, use a `defaultdict` and just increment the count in each branch on hit. The continue in the third branch is not strictly necessary, but that way it protects us if anyone decides to add post-processing which also doesn't account for the third branch. Forward-Port-Of: odoo/odoo#276757 Forward-Port-Of: odoo/odoo#276481
Original PR description
Rather than assign `log_target` and blow up if the third branch is taken, use a `defaultdict` and just increment the count in each branch on hit. The continue in the third branch is not strictly necessary, but that way it protects us if anyone decides to add post-processing which also doesn't account for the third branch. Forward-Port-Of: odoo/odoo#276757 Forward-Port-Of: odoo/odoo#276481
Steps to reproduce --- 1. Install a carrier that requires a positive package shipping weight (e.g. UPS) and enable Packages. 2. From the Barcode app, process a delivery and Put in Pack the products. 3. Validate the transfer. Issue --- Validation is refused because the package has no positive shipping weight, even though its computed weight is correct. The package shipping weight is only set in `_post_put_in_pack_hook` from `context['weight']`, which is filled exclusively by the put i
Original PR description
Steps to reproduce --- 1. Install a carrier that requires a positive package shipping weight (e.g. UPS) and enable Packages. 2. From the Barcode app, process a delivery and Put in Pack the products.…
Steps to reproduce --- 1. Install a carrier that requires a positive package shipping weight (e.g. UPS) and enable Packages. 2. From the Barcode app, process a delivery and Put in Pack the products. 3. Validate the transfer. Issue --- Validation is refused because the package has no positive shipping weight, even though its computed weight is correct. The package shipping weight is only set in `_post_put_in_pack_hook` from `context['weight']`, which is filled exclusively by the put in pack wizard. Flows that bypass the wizard, like the Barcode app, never provide that key, so the stored `shipping_weight` stays 0 while carriers reading it directly reject the delivery. The Barcode app suppresses the wizard through the `barcode_view` context, so its put in pack goes straight to the hook without any weight. https://github.com/odoo/enterprise/blob/6b18215bfa98ef636b63f4fafbba1d25264f4883/stock_barcode/models/stock_move_line.py#L193-L196 This handling was centralized in this hook by 9a2ff6f4033e, so every wizard-bypassed pack leaves the field empty. Defaulting it to the package computed weight via `_get_weight` when the context did not provide one makes the stored value correct for any carrier, independently of the flow that created the package. https://github.com/odoo/odoo/blob/4235b48c86077bd5bceb9e817cc45b2eec8697e8/addons/stock_delivery/models/stock_move.py#L111-L115 opw-6297689 Forward-Port-Of: odoo/odoo#273876 Forward-Port-Of: odoo/odoo#272510
Steps to reproduce 1. Set the user timezone to one with a positive UTC offset, e.g. Europe/Brussels. 2. Open Payroll > Work Entries in pivot view and apply the built-in "Current Month" filter. 3. Expand an employee row and group by Date > Day. Expected: only days of the current month are visible (April 1..30 at the time of writing). Actual: the last day of the previous month (March 31) is included and the last day of the current month (April 30) is missing — the whole window
Original PR description
Steps to reproduce 1. Set the user timezone to one with a positive UTC offset, e.g. Europe/Brussels. 2. Open Payroll > Work Entries in pivot view and apply the built-in "Current Month" filter. 3.…
Steps to reproduce 1. Set the user timezone to one with a positive UTC offset, e.g. Europe/Brussels. 2. Open Payroll > Work Entries in pivot view and apply the built-in "Current Month" filter. 3. Expand an employee row and group by Date > Day. Expected: only days of the current month are visible (April 1..30 at the time of writing). Actual: the last day of the previous month (March 31) is included and the last day of the current month (April 30) is missing — the whole window is shifted one day earlier. Issue The relative-date DSL added in https://github.com/odoo/odoo/commit/d1ea43f6721116914762ea323d8a5987f043e87f operates on a tz-aware datetime starting from `now` when no explicit anchor is given. With the filter at https://github.com/odoo/odoo/blob/d58f4ed332af35f6de26a93f07adf05368731e20/addons/hr_work_entry/views/hr_work_entry_views.xml#L190-L192, `=1d` resets the clock to 00:00 and the day to the 1st in the user's timezone, evaluating to `2026-04-01 00:00:00+02:00` for Brussels. `parse_date` then normalizes that to UTC-naive `2026-03-31 22:00:00` (see https://github.com/odoo/odoo/blob/d58f4ed332af35f6de26a93f07adf05368731e20/odoo/tools/date_utils.py#L196-L199). When the ORM compares this to the `fields.Date` column `date` it truncates via `datetime.date()` (see https://github.com/odoo/odoo/blob/d58f4ed332af35f6de26a93f07adf05368731e20/odoo/orm/fields_temporal.py#L138-L158), yielding `2026-03-31`. The effective domain becomes `[2026-03-31, 2026-04-30)` instead of `[2026-04-01, 2026-05-01)`, which is why March 31 leaks in and April 30 disappears. Solution Anchor the filter to `today` instead of the implicit `now`. The DSL documents `today` as a starting point that yields a `date` value (see https://github.com/odoo/odoo/blob/d58f4ed332af35f6de26a93f07adf05368731e20/odoo/tools/date_utils.py#L108-L144), so `today =1d` resolves to the first of the month as a calendar date end-to-end and never goes through the tz-aware datetime → UTC-naive roundtrip that was silently shifting the window. opw-6123753 Forward-Port-Of: odoo/odoo#274105 Forward-Port-Of: odoo/odoo#260727
In the Outlook calendar sync, it's possible to have the same attendee twice. That's because the normalized email wasn't used to check for preexisting attendee. To reproduce, sync event with the organizer, also an attendee, using high case in the email such as: ORGANIZER: Mike@organizer.com ATTENDEE: Mike@organizer.com ATTENDEE: John@attendee.com opw-6186606 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#272258 For
Original PR description
In the Outlook calendar sync, it's possible to have the same attendee twice. That's because the normalized email wasn't used to check for preexisting attendee. To reproduce, sync event with the organizer, also an attendee, using high case in the email such as: ORGANIZER: Mike@organizer.com ATTENDEE: Mike@organizer.com ATTENDEE: John@attendee.com opw-6186606 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#272258 Forward-Port-Of: odoo/odoo#268201
From versions 18.3 to 19.3, company_id and siret refer to the same field. This caused an issue when connecting to the PDP using siret or siren could overwrite company_id. task-6327304 Forward-Port-Of: odoo/odoo#276805 Forward-Port-Of: odoo/odoo#275276
Original PR description
From versions 18.3 to 19.3, company_id and siret refer to the same field. This caused an issue when connecting to the PDP using siret or siren could overwrite company_id. task-6327304 Forward-Port-Of: odoo/odoo#276805 Forward-Port-Of: odoo/odoo#275276
Steps to reproduce: ------------------------------------ 1. Install barcode_gs1_nomenclature Module. 2. Go to Inventory > Configuration > Barcode Nomenclature. 3. Open any barcode nomenclature. Observation: ------------------------------------ The "Is GS1 Nomenclature" field is visible in the list header. Issue: ------------------------------------ The field uses the invisible attribute, which hides the field values but does not hide the corresponding list header, resulting in an e
Original PR description
Steps to reproduce: ------------------------------------ 1. Install barcode_gs1_nomenclature Module. 2. Go to Inventory > Configuration > Barcode Nomenclature. 3. Open any barcode nomenclature. Observation: ------------------------------------ The "Is GS1 Nomenclature" field is visible in the list header. Issue: ------------------------------------ The field uses the invisible attribute, which hides the field values but does not hide the corresponding list header, resulting in an empty header column. Solution: ------------------------------------ Replace the 'invisible' attribute with 'column_invisible' so that both the field values and the corresponding list header are hidden. opw-6390731 Forward-Port-Of: odoo/odoo#276532
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create Invoices` > select `Regular invoice` > `Create Draft`. (leave the invoice in the draft state). - Navigate to Accounting > Review > Invoiced Not Delivered. - Select the newly created invoice and click `Create Accrual Entries`. **Error:** `ZeroDivisionError: division by zero` **Root Ca
Original PR description
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create…
**Steps to reproduce:** - Install the `accountant` and `sale_management` modules. - Create and confirm a SO with a product whose `Invoicing Policy` is set to `Ordered quantities`. - Click `Create Invoices` > select `Regular invoice` > `Create Draft`. (leave the invoice in the draft state). - Navigate to Accounting > Review > Invoiced Not Delivered. - Select the newly created invoice and click `Create Accrual Entries`. **Error:** `ZeroDivisionError: division by zero` **Root Cause:** At [1], `processed_qty` is initialized with `0`. If all the selected invoices are in the draft state, the code never enters the for loop, so `processed_qty` remains 0. Later, it attempts to divide the `amount` by `processed_qty`, causing an error. **Fix:** This commit prevents errors when creating accrual entries for draft invoices. [1]: https://github.com/odoo/odoo/blob/a7d285a833f5b1d47a11a1b8ec31e3730c1c854c/addons/account/wizard/accrued_orders.py#L258-L265 opw-6332652 Forward-Port-Of: odoo/odoo#273330
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause: ====== The donation snippet posts its configuration (prefilled amounts, display options, descriptions) in the request body. If the user switches the website language on the payment page, it triggers a page reload via a GET request which drops the original form body (this also applies to
Original PR description
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause:…
Steps to reproduce: ================== 1. Add a Donation snippet on a page 2. Click "Donate Now" 3. Switch the website language on the payment page => Configured amounts/descriptions disappear Cause: ====== The donation snippet posts its configuration (prefilled amounts, display options, descriptions) in the request body. If the user switches the website language on the payment page, it triggers a page reload via a GET request which drops the original form body (this also applies to a simple page refresh). As a result, the kwargs were empty and the page fell back to the default free-amount input, losing the configured options. Solution: ========= Implement Post/Redirect/Get: on POST, redirect to /donation/pay with the options in the query string so any later GET (language switch, refresh, iframe src reload) re-renders a fully configured page without losing data. Alternative Solution: ===================== We could also store the options in the session, but the current solution is much simpler and avoids session pollution. opw-6282391 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#271460
Issue: - After confirming a sale order with a reward applied (e.g. an ewallet reward), unlocking it, and editing it so the reward line's cost changes (e.g. adding a product and re-claiming the reward), the coupon's point balance updates correctly. - However, the corresponding loyalty.history record's used value is never refreshed, so it keeps showing the old cost instead of the new one. Steps to reproduce: - Enable Loyalty and Lock Confirmed Sales in Sales settings. - Create an ewallet ty
Original PR description
Issue: - After confirming a sale order with a reward applied (e.g. an ewallet reward), unlocking it, and editing it so the reward line's cost changes (e.g. adding a product and re-claiming the…
Issue: - After confirming a sale order with a reward applied (e.g. an ewallet reward), unlocking it, and editing it so the reward line's cost changes (e.g. adding a product and re-claiming the reward), the coupon's point balance updates correctly. - However, the corresponding loyalty.history record's used value is never refreshed, so it keeps showing the old cost instead of the new one. Steps to reproduce: - Enable Loyalty and Lock Confirmed Sales in Sales settings. - Create an ewallet type loyalty.program and generate an ewallet for a partner with e.g. 1000 points. - Create a sale order for that partner, add a product worth 100, and claim the ewallet reward (reward line created with points_cost = 100). - Confirm the order. loyalty.history shows used = 100 (correct), card balance shows 900 (correct). - Unlock the order, add a second product worth 100, and claim the reward again (same reward line updates to points_cost = 200). - Lock the order again. - Check the loyalty.card: balance is correctly 800. - Check loyalty.history for that order: used still shows 100 instead of 200. Fix: - Updated _update_loyalty_history() in sale_order.py to create a new history line if none exists for the given card and order combination. - Updated write() in sale_order_line.py to correctly sync history lines when a reward line is modified on a confirmed order, handling both same-coupon updates via delta and coupon changes by subtracting the old coupon cost and adding the new one separately. - Added test_loyalty_history_created_on_post_confirm_reward to verify that a history line is created when a reward is claimed on a confirmed order where no history line existed before. - Added test_loyalty_history_updated_on_points_cost_write to verify that history.used is updated by the correct delta when points_cost changes on a reward line of a confirmed order. Impact: - Ensures a coupon's usage history stays accurate after a confirmed order is unlocked and edited. - Prevents the loyalty.history used field from silently going stale while the actual point balance is correct. - Not scoped to ewallet specifically — since points_cost semantics are the same across program types, this also corrects the same class of staleness for other reward types (discount, gift_card, etc.) when a reward line's cost changes post-confirmation. Forward-Port-Of: odoo/odoo#276961 Forward-Port-Of: odoo/odoo#273688
On touch devices (smartphones, tablets), the in-browser notification sound plays through the media audio channel, which is not affected by the device's silent mode. This causes unexpected sound playback when the device is set to silent. Suppress _playSound() on devices with maxTouchPoints > 1 (touch/mobile). Push notifications handle alerts on mobile and properly respect the device's silent mode. The side effect is that also laptops with touch screen are affected and need to use web push n
Original PR description
On touch devices (smartphones, tablets), the in-browser notification sound plays through the media audio channel, which is not affected by the device's silent mode. This causes unexpected sound playback when the device is set to silent. Suppress _playSound() on devices with maxTouchPoints > 1 (touch/mobile). Push notifications handle alerts on mobile and properly respect the device's silent mode. The side effect is that also laptops with touch screen are affected and need to use web push notifications in order to get notification sounds played. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276675 Forward-Port-Of: odoo/odoo#275797
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through upsert_en()) can have their English source text changed by a module upgrade while keeping the same key. For example product.template's selection value 'consu' went from meaning "Consumable" to meaning "Goods" between 17.0 and 18.0, without the stored value itself changing. In that situation, upsert
Original PR description
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through…
Translatable rows that are identified by a stable key rather than by their text (e.g. selection option labels in ir.model.fields.selection, and other module-reflected metadata written through upsert_en()) can have their English source text changed by a module upgrade while keeping the same key. For example product.template's selection value 'consu' went from meaning "Consumable" to meaning "Goods" between 17.0 and 18.0, without the stored value itself changing. In that situation, upsert_en() always refreshes the 'en_US' entry from the current code, so English is never wrong. But every other language keeps whatever value it had, since upsert_en() blindly merges the new 'en_US' entry into the existing jsonb without checking whether the previous 'en_US' value actually changed. When the module's .po file is later reloaded, TranslationImporter.save() finds a non-empty value already there for each language and, by design, does not overwrite it unless explicitly asked to (`overwrite`) on a record that isn't `noupdate`. That protection is correct when the English text hasn't changed (it keeps manual translation customizations across upgrades), but here it ends up protecting a translation of a source string that no longer exists. As a result, a value whose meaning changes across versions keeps showing its old label in every language except English, forever, unless someone forces a translation overwrite. Fix this in upsert_en() itself: when the incoming 'en_US' value differs from the one currently stored, drop every other language already stored for that row instead of merging into it. Those translations were made for a source string that no longer exists, so there is nothing worth protecting. Once dropped, they are simply missing from the jsonb, so TranslationImporter.save() naturally lets the freshly (re)loaded translations in on its next run, without needing to know anything about source drift, and `overwrite`/`noupdate` keep their exact original meaning for every other case. Task-6333080 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276725 Forward-Port-Of: odoo/odoo#275080
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/fba73fe75b [REL] 19.2.21 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/0266f73f3b [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) https://github.com/odoo/o-spreadsheet/commit/5bb486c814 [FIX] TableComputedStylePlugin: skip table style for invalid pivot [Task: 6306263](https://www.odoo.com/odoo/2328/tasks/
Original PR description
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/fba73fe75b [REL] 19.2.21 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0)…
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/fba73fe75b [REL] 19.2.21 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/0266f73f3b [FIX] scorecardPanel: mismatch baseline/keyValue [Task: 6380393](https://www.odoo.com/odoo/2328/tasks/6380393) https://github.com/odoo/o-spreadsheet/commit/5bb486c814 [FIX] TableComputedStylePlugin: skip table style for invalid pivot [Task: 6306263](https://www.odoo.com/odoo/2328/tasks/6306263) https://github.com/odoo/o-spreadsheet/commit/ecef954c0e [FIX] squisher: adapted ranges that become cells [Task: 6390727](https://www.odoo.com/odoo/2328/tasks/6390727) https://github.com/odoo/o-spreadsheet/commit/43bc2d9e1d [FIX] index: export `positionMap` [Task: 6159213](https://www.odoo.com/odoo/2328/tasks/6159213) https://github.com/odoo/o-spreadsheet/commit/1584d3c4b1 [FIX] Pivots: cache `getPivotIdFromPosition` for performance [Task: 6159213](https://www.odoo.com/odoo/2328/tasks/6159213) Co-authored-by: Florian Damhaut (flda) <flda@odoo.com> Co-authored-by: Anthony Hendrickx (anhe) <anhe@odoo.com> Co-authored-by: Alexis Lacroix (laa) <laa@odoo.com> Co-authored-by: Lucas Lefèvre (lul) <lul@odoo.com> Co-authored-by: Adrien Minne (adrm) <adrm@odoo.com> Co-authored-by: Ronak Mukeshbhai Bharadiya (rmbh) <rmbh@odoo.com> Co-authored-by: Dhrutik Patel (dhrp) <dhrp@odoo.com> Co-authored-by: Rémi Rahir (rar) <rar@odoo.com> Co-authored-by: Pierre Rousseau (pro) <pro@odoo.com> Co-authored-by: Vincent Schippefilt (vsc) <vsc@odoo.com> Co-authored-by: Marceline Thomas (matho) <matho@odoo.com>
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce email from will default to the bounce alias of the main company of the DB. In a multi company setup, where each company might have their own mail alias domain, this can be confusing for the receiving party that didn't send the original email to the main company's alias domain. ## Propo
Original PR description
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce…
When sending an email to an invoice mail alias without valid attachment, a custom bounce mail is sent to inform the customer that it was bounced because of a missing attachment. Currently, the bounce email from will default to the bounce alias of the main company of the DB. In a multi company setup, where each company might have their own mail alias domain, this can be confusing for the receiving party that didn't send the original email to the main company's alias domain. ## Proposed solution: We call `_routing_create_bounce_email` with the right company in the context, since the journal company is already detected. Additionally, in the context of the accounting code calling the bounce private method, we pass an explicit value for `reply_to` ## How to reproduce it: 1) Set up DB with two companies (companyA and companyB). Install account. 2) Set up two mail alias domains (companya.com and companyb.com) 3) Send an email to the invoice alias of companyB (e.g. invoices@companyb.com). -> bounce email will have FROM as "notifications@companya.com" (the notifications alias of companyA) Example headers before: ``` Reply-To: "YourCompany" <catchall@companya.com> To: customer@example.com Date: Thu, 02 Jul 2026 11:18:49 -0000 Return-Path: bounce@companya.com From: "MAILER-DAEMON" <bounce@companya.com> ``` After fix: ``` Reply-To: info@companyb.com To: customer@example.com Date: Thu, 02 Jul 2026 11:16:19 -0000 Return-Path: bounce@companyb.com From: "MAILER-DAEMON" <notifications@companyb.com> ``` opw-6342778 Forward-Port-Of: odoo/odoo#273671
The reversal wizard always injected a shared 'R4' onto every credit note it created, so a credit note of a simplified invoice never got the required 'R5', and reversing several invoices of mixed simplified status at once forced the same reason onto all of them. The wizard now derives the reason per original move ('R5' if it is simplified, 'R4' otherwise) instead of using one shared value, while still respecting an explicit manual choice. `l10n_es_is_simplified` already freezes correctly
Original PR description
The reversal wizard always injected a shared 'R4' onto every credit
note it created, so a credit note of a simplified invoice never got
the required 'R5', and reversing several invoices of mixed simplified
status at once forced the same reason onto all of them.
The wizard now derives the reason per original move ('R5' if it is
simplified, 'R4' otherwise) instead of using one shared value, while
still respecting an explicit manual choice. `l10n_es_is_simplified`
already freezes correctly on the credit note via its existing compute,
so no new compute is needed on `l10n_es_edi_verifactu_refund_reason`.
task-6358684
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#276187Miscellaneous changes
Steps to reproduce ------------------ 1. On a bank payment method, enable "Identify Customer". 2. Create a lot of orders paid with this method (the client reporting the issue had 1700), and put a customer on each order. 3. Close the session. With "Identify Customer" enabled, closing the session creates one `account.payment` for each payment and posts it one by one, then reconciles each payment separately. With many payments this is slow and the close takes several minutes and the worker i
Original PR description
Steps to reproduce ------------------ 1. On a bank payment method, enable "Identify Customer". 2. Create a lot of orders paid with this method (the client reporting the issue had 1700), and put a customer on each order. 3. Close the session. With "Identify Customer" enabled, closing the session creates one `account.payment` for each payment and posts it one by one, then reconciles each payment separately. With many payments this is slow and the close takes several minutes and the worker is stopped by its time limit, so we get the error "Cursor already closed". Now we create and post all these payments at once in a single batch, and reconcile the payments in one call too. Benchmark --------- These numbers come from closing a session that has 1700 orders paid with such a payment method, on a test database: - before: the close did not finish after more than 10 minutes. - after: the close takes around 75 seconds. opw-6242303 Forward-Port-Of: odoo/odoo#269306