Monday, August 17, 2026
19 changes · saas-19.4
Enhancements to existing features
The Belgian payroll meal voucher report now calculates total voucher value correctly when vouchers are postponed. This helps payroll teams produce more accurate reports and avoids incorrect meal voucher amounts for affected employees.
Original PR description
-Adjust the total value for meal voucher report in case of postponed meal vouchers. Forward-Port-Of: odoo/enterprise#127941
Before this commit, ensuring the web client updated its context when a warehouse was created relied on intercepting RPC responses on the frontend. This approach was inefficient and relied on the client to guess when backend access groups were modified. With this commit, we shift the responsibility to the backend. The server now explicitly sends a bus notification (`stock_group_sync`) to user when the `group_stock_multi_warehouses` or `group_stock_multi_locations` groups are assigned/una
Original PR description
Before this commit, ensuring the web client updated its context when a warehouse was created relied on intercepting RPC responses on the frontend. This approach was inefficient and relied on the client to guess when backend access groups were modified. With this commit, we shift the responsibility to the backend. The server now explicitly sends a bus notification (`stock_group_sync`) to user when the `group_stock_multi_warehouses` or `group_stock_multi_locations` groups are assigned/unassigned. The `stock_warehouse` frontend service now simply subscribes to this event to perform a `reload_context`, making the process much more robust and performant. task-6381795
Before this commit, when importing and invoice/bill, we predicted the invoice line account based on previous invoices/bills. If the predicted account had default tax, it was ignored during tax matching. With this commit, first checks whether the predicted account has a default tax. If it finds one that matches the tax percentage from the imported XML, that tax is applied. Otherwise, or if the account has no default tax, the existing tax matching logic is used. task-6345661 Forward-Po
Original PR description
Before this commit, when importing and invoice/bill, we predicted the invoice line account based on previous invoices/bills. If the predicted account had default tax, it was ignored during tax matching. With this commit, first checks whether the predicted account has a default tax. If it finds one that matches the tax percentage from the imported XML, that tax is applied. Otherwise, or if the account has no default tax, the existing tax matching logic is used. task-6345661 Forward-Port-Of: odoo/odoo#281424 Forward-Port-Of: odoo/odoo#279940
Resolved issues and error corrections
Fixed an error that prevented users from exporting the Deferred Revenue Report to Excel when report lines included annotations. This ensures accounting teams can keep and share annotated deferred revenue data without hitting a server error.
Original PR description
**Steps to reproduce:** * Install the **Accounting** module. * Unhide the **Start Date** and **End Date** fields on invoice lines. * Create and post a customer invoice with deferred dates. * Go to…
**Steps to reproduce:** * Install the **Accounting** module. * Unhide the **Start Date** and **End Date** fields on invoice lines. * Create and post a customer invoice with deferred dates. * Go to **Accounting → Reports → Deferred Revenue Report**. * Add an annotation to a deferred revenue line by clicking the **annotate** from three dots next to the account. * Export the report in **XLSX** format. **Observed behavior:** * The export fails with a server error: `UnboundLocalError: cannot access local variable 'annotations_x_offset' where it is not associated with a value` **Cause:** * The variable `annotations_x_offset` is assigned inside the `for header_level_index, header_level in enumerate(options['column_headers'])` loop, which writes the "Annotations" column header for each header level. * The Deferred Revenue Report produces an empty `column_headers` list, so the loop body never executes and `annotations_x_offset` is never assigned. * When the code later tries to write annotation data for each report line, it references the unassigned variable, causing Python to raise `UnboundLocalError`. **Fix:** * Introduce a boolean flag `annotations_header_written = False` before the header loop to explicitly track whether the "Annotations" column header has already been written. * Inside the header loop, set `annotations_header_written = True` after writing the header. * After writing all individual column headers (where `x_offset` already points to the first free column after all data columns), add a fallback: if `report_annotations` is set but `annotations_header_written` is still `False`, assign `annotations_x_offset` from the current `x_offset` and write the "Annotations" header. opw-6354473 Forward-Port-Of: odoo/enterprise#127840 Forward-Port-Of: odoo/enterprise#122768
Miscellaneous changes
Allow resetting sent moves to draft. Ensures a rectificative flow exists or is created. Task: 6273211 Forward-Port-Of: odoo/odoo#282246 Forward-Port-Of: odoo/odoo#273013
Original PR description
Allow resetting sent moves to draft. Ensures a rectificative flow exists or is created. Task: 6273211 Forward-Port-Of: odoo/odoo#282246 Forward-Port-Of: odoo/odoo#273013
This fixes a payroll pay run automated test that was failing because the screen adds empty placeholder rows. The test now checks only rows containing payroll data, making payroll validation more reliable without changing user-facing behavior.
Original PR description
Issue: The original trigger was searching for 2 table rows, when it enforces 4 with added empty rows. The [getEmptyRowIds](https://github.com/odoo/odoo/blob/33dc65bbac165f33030ad3da59ea785b69482b3f/addons/web/static/src/views/list/list_renderer.js#L1104-L1110) enforces max of 4 rows. The condtional (one up the stack) !ctx["this"].props.list.isGrouped&&!ctx["this"].props.noContentHelp returns true, and it adds empty rows. Fix: Since this enforces 4 rows with empty rows we check the rows that have data instead of how many rows are added. Because anything less than or equal to 4 but greater than 0 records it will always be 4 table rows while the conditional above returns true . opw-6349513 <img width="1337" height="674" alt="Screenshot 2026-07-15 at 4 53 51 PM" src="https://github.com/user-attachments/assets/76f53521-ec9f-4807-9083-95533904b5de" /> Forward-Port-Of: odoo/enterprise#125091
Timesheet assistant entries now display durations more clearly without overlapping nearby text. Long titles wrap properly, spacing is improved, and the chronological view is adjusted so users can read timesheet details more easily.
Original PR description
- enforce duration in one line - add a gap between the title and duration - wrap title if so long - adapt flex direction of chronological view to avoid overlapping of title and start time --- task-6432434 Forward-Port-Of: odoo/enterprise#128001 Forward-Port-Of: odoo/enterprise#126236
Users can now create a warehouse directly while setting up stock by vehicle mappings. This removes an unnecessary setup blocker and makes configuring vehicle-based stock operations smoother.
Original PR description
Before this commit, users could not create a new warehouse directly from the 'stock by vehicle' mapping view because the `warehouse_id` field had the `no_create` option enabled.
With this commit, we remove `options="{'no_create': True}"` from the `warehouse_id` field in both the list and form views. This enables on-the-fly warehouse creation directly from the vehicle mapping settings.
task-6381795Steps to reproduce: --- - Install `website_sale_collect` without demo data. - Create and publish a product. - Add the product to the cart and proceed to checkout. - Complete the main address. - Select a `Pick Up in Store` delivery method and click checkout. Issue: --- The checkout redirects back to the address form instead of continuing to the payment step. Root cause: --- During checkout, `shop/checkout`[1] calls `_check_cart_and_addresses()`, which eventually invokes `_check_ad
Original PR description
Steps to reproduce: --- - Install `website_sale_collect` without demo data. - Create and publish a product. - Add the product to the cart and proceed to checkout. - Complete the main address. -…
Steps to reproduce: --- - Install `website_sale_collect` without demo data. - Create and publish a product. - Add the product to the cart and proceed to checkout. - Complete the main address. - Select a `Pick Up in Store` delivery method and click checkout. Issue: --- The checkout redirects back to the address form instead of continuing to the payment step. Root cause: --- During checkout, `shop/checkout`[1] calls `_check_cart_and_addresses()`, which eventually invokes `_check_addresses()`[2]. That method then calls `_check_delivery_address()`[3] to verify that all mandatory delivery address fields are present. When db is initialized without demo data, the pickup location address may not contain all mandatory fields (such as ZIP code). As a result, the validation fails and the checkout incorrectly redirects the customer back to the address form, even though the delivery address is a pickup location that should not be edited by the customer. Solution: --- Override `_can_be_edited_by_current_customer()` to treat the selected pickup location as a non-editable address. Since the pickup location belongs to the store, it does not make sense to ask the customer to edit or complete its address. This prevents the checkout flow from requesting address completion and allows the customer to proceed directly to the payment step. [1]https://github.com/odoo/odoo/blob/815de3f1a43bccdb5714436da2060f7a45aa385e/addons/website_sale/controllers/main.py#L1141-L1142 [2]https://github.com/odoo/odoo/blob/815de3f1a43bccdb5714436da2060f7a45aa385e/addons/website_sale/controllers/main.py#L1894-L1895 [3]https://github.com/odoo/odoo/blob/815de3f1a43bccdb5714436da2060f7a45aa385e/addons/website_sale/controllers/main.py#L1945 opw-6394166 I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#277189
On highly loaded runbots, waiting a single animation frame might not be sufficient for the table deselection to be available in the DOM after a key press. This commit waits for the table to be deselected before continuing the test. runbot-944722 Forward-Port-Of: odoo/odoo#282008
Original PR description
On highly loaded runbots, waiting a single animation frame might not be sufficient for the table deselection to be available in the DOM after a key press. This commit waits for the table to be deselected before continuing the test. runbot-944722 Forward-Port-Of: odoo/odoo#282008
**Steps to reproduce:** 1. Install Accounting 2. Import a new invoice with more than 1000 lines (xlsx file found in ticket attachments) 3. Test the imported records **Issue:** - `RecursionError: maximum recursion depth exceeded`. **Cause:** - In a previous commit (3e32d7b9eace62dfa7334009707a93967906c726) aimed at fixing stale analytic distribution totals, the assignment loop in `_compute_discount_allocation_needed` was changed from iterating over `self` to `self.move_id.line_ids`. -
Original PR description
**Steps to reproduce:** 1. Install Accounting 2. Import a new invoice with more than 1000 lines (xlsx file found in ticket attachments) 3. Test the imported records **Issue:** - `RecursionError:…
**Steps to reproduce:** 1. Install Accounting 2. Import a new invoice with more than 1000 lines (xlsx file found in ticket attachments) 3. Test the imported records **Issue:** - `RecursionError: maximum recursion depth exceeded`. **Cause:** - In a previous commit (3e32d7b9eace62dfa7334009707a93967906c726) aimed at fixing stale analytic distribution totals, the assignment loop in `_compute_discount_allocation_needed` was changed from iterating over `self` to `self.move_id.line_ids`. - While this ensured all lines generated updated distribution ratios, it violated the compute logic: assigning values to records outside the current compute batch (`self`). - By executing `line.discount_allocation_dirty = True` on external sibling lines, the method forced the ORM to trigger out-of-band `write()` calls. These writes re-triggered dependency checks (`_field_will_change`), which invoked the compute method again, leading to a recursive loop. **Fix:** 1. Revert the assignment iteration back to `for line in self:`. 2. To preserve the intention of the previous commit (ensuring all lines recompute their shared distribution pool when one line changes), modify the method's `@api.depends` to be `move_id.line_ids.discount` and `move_id.line_ids.analytic_distribution`. By declaring these relational dependencies, modifying a single line now batches all sibling lines into `self` from the start. This allows the lines to synchronize properly without triggering new ORM writes, eliminating the recursion. opw-6451854 Forward-Port-Of: odoo/odoo#282050
### Steps to reproduce: - Enable "Lots & Serial Numbers" in the Inventory settings - Create a storable product tracked by lots - Create a lot for that product and leave it without any quantity - Open the lot form > cog menu > Scrap #### > ValueError: Expected singleton: stock.location() ### Cause of the issue: A lot is only given a `location_id` when all its positive quants lay in a single location, so a lot with no quant at all has none: https://github.com/odoo/odoo/blob/8b8852f1c3
Original PR description
### Steps to reproduce: - Enable "Lots & Serial Numbers" in the Inventory settings - Create a storable product tracked by lots - Create a lot for that product and leave it without any quantity - Open…
### Steps to reproduce: - Enable "Lots & Serial Numbers" in the Inventory settings - Create a storable product tracked by lots - Create a lot for that product and leave it without any quantity - Open the lot form > cog menu > Scrap #### > ValueError: Expected singleton: stock.location() ### Cause of the issue: A lot is only given a `location_id` when all its positive quants lay in a single location, so a lot with no quant at all has none: https://github.com/odoo/odoo/blob/8b8852f1c3ae5b78a4a3e99c99eacab83f0be5c9/addons/stock/models/stock_lot.py#L169-L172 The `action_scrap` nevertheless always forwards that value to the scrap move, so the form is opened with `default_location_id` set to `False`: https://github.com/odoo/odoo/blob/8b8852f1c3ae5b78a4a3e99c99eacab83f0be5c9/addons/stock/models/stock_lot.py#L415-L426 Now, the issue is that the `_onchange_lot_ids` of the new stock move then evaluates the reservation of a move whose source location is still empty which raises the traceback because of a `self.ensure_one` required on the location to determine if the move `should_bypass_reservation`: https://github.com/odoo/odoo/blob/8b8852f1c3ae5b78a4a3e99c99eacab83f0be5c9/addons/stock/models/stock_move.py#L1515 https://github.com/odoo/odoo/blob/8b8852f1c3ae5b78a4a3e99c99eacab83f0be5c9/addons/stock/models/stock_move.py#L1989-L1992 https://github.com/odoo/odoo/blob/8b8852f1c3ae5b78a4a3e99c99eacab83f0be5c9/addons/stock/models/stock_location.py#L414-L416 Note that if the default key was not present, the traceback would not be triggered and the scrap would be performed from the `default_stock_location` of the company: https://github.com/odoo/odoo/blob/4c4219a7d9d51f703b15e83ab755faf1f2c8a71d/addons/stock/models/res_company.py#L21-L22 Indeed the issue is that the `default_get` looks the context key up by *membership*, so that a falsy `default_location_id` is still returned as a default value for the field if provided: https://github.com/odoo/odoo/blob/8b8852f1c3ae5b78a4a3e99c99eacab83f0be5c9/odoo/orm/models.py#L1313-L1317 And therefore will not be computed in the first onchange: https://github.com/odoo/odoo/blob/8b8852f1c3ae5b78a4a3e99c99eacab83f0be5c9/addons/web/models/models.py#L1974-L1988 Note: The `stock.lot.action_scrap`, and this `default_location_id` line with it, were introduced by 1c7d80a10b5d, which replaced the `stock.scrap` model by scrap `stock.move` records. That commit is only present from saas-19.2 onwards, so earlier versions are not affected. opw-6441937 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280666
Steps: - Install sale_management - Make sure you have 100 quotations with 1 activity each - Open activity view - Default pager displays `1-100/100` - Activity count display `To-do 80` ActivityController uses `useModel` which passes the raw `component.props` to `model.load()`, including the limit from `ir.actions.act_window` (default 80). This value ended up in `fetchActivityData` via `params.limit || this.initialLimit`, overriding `ActivityModel.DEFAULT_LIMIT` (100). The records li
Original PR description
Steps: - Install sale_management - Make sure you have 100 quotations with 1 activity each - Open activity view - Default pager displays `1-100/100` - Activity count display `To-do 80`…
Steps: - Install sale_management - Make sure you have 100 quotations with 1 activity each - Open activity view - Default pager displays `1-100/100` - Activity count display `To-do 80` ActivityController uses `useModel` which passes the raw `component.props` to `model.load()`, including the limit from `ir.actions.act_window` (default 80). This value ended up in `fetchActivityData` via `params.limit || this.initialLimit`, overriding `ActivityModel.DEFAULT_LIMIT` (100). The records list was not affected because `RelationalModel._getNextConfig` never reads `params.limit` (limit is not a `SEARCH_KEY`), so it always loaded 100 records correctly. But `fetchActivityData` used 80, causing a mismatch between the records shown and the activity counts in the column headers. ```js export const SEARCH_KEYS = ["comparison", "context", "domain", "groupBy", "orderBy"]; ``` The fix strips `params.limit` in `ActivityModel.load()` before passing params to `fetchActivityData`, so it falls back to `this.initialLimit (100)`. The pager `onUpdate` handler calls `fetchActivityData` directly with its own `params.limit` and is not affected. However, `ActivityController` never forwards `limit` to the model. This is why we always have `ActivityModel.DEFAULT_LIMIT (100)` without taking into account actions's limit. To fix this we have to add the limit via `this.props.limit`, as `ListController`. `useModelWithSampleData` already had the correct behavior by calling `model.load(getSearchParams(props))` which filters out non-search params like limit. In 19.0 useModel was updated to do the same, so the issue does not exist there. Link to 19.0 fix: https://github.com/odoo/odoo/pull/211697 opw-6281125 Forward-Port-Of: odoo/odoo#281227 Forward-Port-Of: odoo/odoo#273929
## Issue When filtering projects using the "Timesheets >100%" filter, some projects with negative remaining hours (and with their `is_project_overtime` field set to True) won't be displayed, even though their expected hours are completed. This happens with projects which have tasks set to the "Done" or "Cancelled" state. The timesheets entries in those tasks are not taken into account when searching using the "Timesheets >100%" filter. ## Steps to reproduce 1. Install *Task Logs* (`hr_ti
Original PR description
## Issue When filtering projects using the "Timesheets >100%" filter, some projects with negative remaining hours (and with their `is_project_overtime` field set to True) won't be displayed, even…
## Issue
When filtering projects using the "Timesheets >100%" filter, some projects with negative remaining hours (and with their `is_project_overtime` field set to True) won't be displayed, even though their expected hours are completed.
This happens with projects which have tasks set to the "Done" or "Cancelled" state. The timesheets entries in those tasks are not taken into account when searching using the "Timesheets >100%" filter.
## Steps to reproduce
1. Install *Task Logs* (`hr_timesheet`)
2. Create a Project P (with Timehseets enabled)
3. Set the allocated hours of the project to 3:00 (3 hours)
4. Create two tasks:
- T1: State "In progress", and one timesheet entry of 2:00 (2 hours)
- T2: State "Done", and one timesheet entry of 2:00 (2 hours)
5. Back to the project view, set the filter to "Timesheets >100%"
6. **Project P is not shown, even though the total time spent on the project is 4 hours, completing the allocated hours set on the project.**
## Cause
The `_search_is_project_overtime` method filters out the tasks in "closed" states (Done/Cancelled) when computing the amount of time spent on the project.
https://github.com/odoo/odoo/blob/126b5bdd1e85771549198976f8570cd2ff167608/addons/hr_timesheet/models/project_project.py#L103-L114
This does not match with the behavior of the `_compute_is_project_overtime`, which does not take into account the state of the tasks to determine the value of the field:
https://github.com/odoo/odoo/blob/126b5bdd1e85771549198976f8570cd2ff167608/addons/hr_timesheet/models/project_project.py#L85-L94
This leads to a confusing behavior, where a project can have its `is_project_overtime` field set to True, but will still not be shown when using the "Timsheets >100%", even though that filter is defined as `[("is_project_overtime", "=", True)]`.
The compute method was updated by https://github.com/odoo/odoo/commit/d4252825f52a3172420dcda0ea394e42da9f8853, but the related search method was left unchanged, leading to this slight incoherence between the two methods.
opw-6422173
Forward-Port-Of: odoo/odoo#282338
Forward-Port-Of: odoo/odoo#281997Before this commit, a week off on a schedule mixing working time and absence attendances, like a partial incapacity, was counted as 5 days instead of 2.5. The hours worked on a day were compared to hours_per_day, which already leaves the absence attendances out, so a worked morning looked like a full day. After this commit, the absence attendances are taken back in that comparison, so a worked morning counts as half a day again. taskid-6453601 Forward-Port-Of: odoo/odoo#281994
Original PR description
Before this commit, a week off on a schedule mixing working time and absence attendances, like a partial incapacity, was counted as 5 days instead of 2.5. The hours worked on a day were compared to hours_per_day, which already leaves the absence attendances out, so a worked morning looked like a full day. After this commit, the absence attendances are taken back in that comparison, so a worked morning counts as half a day again. taskid-6453601 Forward-Port-Of: odoo/odoo#281994
At installation of the l10n_pl_bank_verification module or when upgrading from a lower version, the field l10n_pl_verification_id on the account.payment model gets computed and it makes the upgrade crash out. Adding a init to the model to create the column to prevent the ORM from computing the field at module installation no-task Forward-Port-Of: odoo/odoo#282504
Original PR description
At installation of the l10n_pl_bank_verification module or when upgrading from a lower version, the field l10n_pl_verification_id on the account.payment model gets computed and it makes the upgrade crash out. Adding a init to the model to create the column to prevent the ORM from computing the field at module installation no-task Forward-Port-Of: odoo/odoo#282504
Steps to reproduce --- 1. Create and confirm a sale order. 2. Create a down payment invoice on it and post it: the down payment line reads "Down Payment (ref: INV/... on ...)". 3. Open that invoice and use Reverse and Create Invoice, then post the newly created draft down payment invoice. 4. Open the sale order: the down payment line has lost its reference and now reads only "Down Payment", and that empty label also carries over to the down payment section when generating the final invoice.
Original PR description
Steps to reproduce --- 1. Create and confirm a sale order. 2. Create a down payment invoice on it and post it: the down payment line reads "Down Payment (ref: INV/... on ...)". 3. Open that invoice…
Steps to reproduce --- 1. Create and confirm a sale order. 2. Create a down payment invoice on it and post it: the down payment line reads "Down Payment (ref: INV/... on ...)". 3. Open that invoice and use Reverse and Create Invoice, then post the newly created draft down payment invoice. 4. Open the sale order: the down payment line has lost its reference and now reads only "Down Payment", and that empty label also carries over to the down payment section when generating the final invoice. Issue --- The down payment line description is built by `_get_downpayment_description`, which only produces the "Down Payment (ref: ... on ...)" label when exactly one customer invoice is linked to the down payment `sale.order.line`, guarded by `len(invoice) == 1`. https://github.com/odoo/odoo/blob/3a5f7431effd4b2b2eb8ce3eed81aaba42fcd8ea/addons/sale/models/sale_order_line.py#L484-L509 Reverse and Create Invoice runs `account.move.reversal.modify_moves`, which copies the reversed invoice with `include_business_fields=True`, so the copied line keeps its `sale_line_ids` and the new draft invoice is attached to the very same down payment line as the reversed original. https://github.com/odoo/odoo/blob/3a5f7431effd4b2b2eb8ce3eed81aaba42fcd8ea/addons/account/wizard/account_move_reversal.py#L142-L149 That down payment line then references two `out_invoice` moves (the reversed one and the re-issued one), so `len(invoice) == 1` is false and the label silently falls back to the bare "Down Payment", losing the reference that the final invoice's down payment section reuses. Going back to the sale order to raise a fresh down payment instead creates a new line, which keeps a single invoice and is why the slower flow is unaffected. The `len(invoice) == 1` guard was introduced in ba954604e529. Discarding the reversed invoice (`payment_state == 'reversed'`) leaves the active re-issued invoice as the single match, so its reference is shown again; when the only linked invoice is itself reversed, the fallback keeps displaying it so existing descriptions are preserved. opw-6353384 Forward-Port-Of: odoo/odoo#282481 Forward-Port-Of: odoo/odoo#277802
Steps to produce: --- - Install the Sales and Inventory modules. - Enable Allow Spontaneous Returns in Settings. - Create a product and a sales order for it, then confirm the order. - Open the delivery from the smart button and validate it. - Preview the sales order, click return, increase the quantity, select a return reason, and download the return label. - Open the sales order chatter. Issue: --- The chatter message posted after downloading the return label displays HTML tags a
Original PR description
Steps to produce: --- - Install the Sales and Inventory modules. - Enable Allow Spontaneous Returns in Settings. - Create a product and a sales order for it, then confirm the order. - Open the…
Steps to produce:
---
- Install the Sales and Inventory modules.
- Enable Allow Spontaneous Returns in Settings.
- Create a product and a sales order for it, then confirm the order.
- Open the delivery from the smart button and validate it.
- Preview the sales order, click return, increase the quantity, select a
return reason, and download the return label.
- Open the sales order chatter.
Issue:
---
The chatter message posted after downloading the return label displays HTML tags as plain text instead of rendering line breaks.
Cause:
---
In [commit], `_build_return_log_message()`[1] builds the message as a plain str, concatenating literal HTML tags (`<br/>`) with dynamic content (product names, picking names, and the return reason).
The [caller] then posts the message using `Markup("%s") % return_message`. Since return_message is a plain str
rather than a Markup object, MarkupSafe treats it as untrusted input and escapes the entire string during interpolation, causing the HTML tags to be displayed literally.
Solution:
---
Build the message as a Markup object directly in `_build_return_log_message()`, using `Markup("<literal template>") % value` for each fragment. This ensures that the HTML structure is trusted while all dynamic values (product names,
picking names, and the return reason) are safely escaped by MarkupSafe during interpolation.
The method now returns a Markup object, allowing the caller to post it directly without wrapping it again in `Markup("%s") % return_message`, which would otherwise escape the already-safe HTML.
[commit]: https://github.com/odoo/odoo/commit/5f490ffc36a1156b927f2cf4f9b0aa00a83394d5
[1]https://github.com/odoo/odoo/blob/6521b207713c641172b13ac929aeeb1c20f09de8/addons/sale_stock/controllers/return_order.py#L130-L154
[caller]https://github.com/odoo/odoo/blob/6521b207713c641172b13ac929aeeb1c20f09de8/addons/sale_stock/controllers/return_order.py#L119-L120
Before:
---
<img width="645" height="101" alt="image" src="https://github.com/user-attachments/assets/54782171-ee64-4601-bfd9-e62621ab705f" />
After:
---
<img width="576" height="161" alt="image" src="https://github.com/user-attachments/assets/6b1ae04f-b35b-4aae-aa6d-88b7337a0100" />
opw-6367175
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-prChanging the request unit of a time type should not recompute existing leaves, as their computed dates and duration must be preserved. This behavior was introduced in hr_holidays by removing the request unit from the dependencies of the leave date and duration computations. The French localization still declared `work_entry_type_request_unit` as a dependency of `_compute_date_from_to`, causing existing leaves to be invalidated and recomputed when the time type configuration changed. Thi
Original PR description
Changing the request unit of a time type should not recompute existing leaves, as their computed dates and duration must be preserved. This behavior was introduced in hr_holidays by removing the request unit from the dependencies of the leave date and duration computations. The French localization still declared `work_entry_type_request_unit` as a dependency of `_compute_date_from_to`, causing existing leaves to be invalidated and recomputed when the time type configuration changed. This commit removes this dependency to align the French computation with the base behavior and preserve historical leave values. Related: https://github.com/odoo/odoo/pull/261036 [error-243674 ](https://runbot.odoo.com/odoo/error/243674) --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#281690