Monday, August 17, 2026
21 changes · saas-19.4
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
Invoices in Peru with missing tax information now produce a clear validation error during batch sending instead of causing a background processing failure. This prevents one problematic invoice from blocking other invoices from being submitted to SUNAT.
Original PR description
In l10n_pe_edi, invoices containing lines without tax can't be submitted to SUNAT.
When sending a single invoice, an error message is displayed. However, sending multiple invoices processes them in the background by a cron job. In this case, EDI document creation fails without error handling, raising a generic parsing error and blocking the cron from processing other invoices.
Steps to reproduce:
1. Create and post two invoices with no tax on some lines.
2. From the list view, select both invoices and click "Send" and mark "SUNAT".
3. An exception is raised: `ValueError: XMLSyntaxError("Start tag expected, '<' not found, line 1, column 1")`.
opw-6390480
Forward-Port-Of: odoo/enterprise#127947
Forward-Port-Of: odoo/enterprise#125143The planning field service onboarding tour now matches recent interface changes, helping users complete guided setup without getting stuck. The complete action is also shown in the right place depending on whether users are working in Kanban or Gantt views, reducing confusion during field service scheduling.
Original PR description
## [FIX] planning_field_service: display complete button in popover footer Before this commit, the complete button is displayed in the card even in the gantt popover instead of displaying it in the…
## [FIX] planning_field_service: display complete button in popover footer Before this commit, the complete button is displayed in the card even in the gantt popover instead of displaying it in the footer of the gantt popover. This commit makes sure the complete button in the card is only displayed in the kanban view and that button is displayed in the footer of the gantt view. ## [FIX] planning_field_service: adapt onboarding tour based on recent changes Before this commit, the quick create on resource_ids field in planning.slot has been replaced by a form view inside a modal. The Sign in button in gantt/calendar popover no longer automatically redirects the user to the form view of the intervention and so the user cannot directly complete the shift. This commit adapts the onboarding tour based on the recent changes. It also forces a reload in the gantt view when the user signs in a intervention via the Sign in button in the gantt popover. runbot-error-941063 task-[6353582](https://www.odoo.com/odoo/project/4105/tasks/6353582)
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
The Documents app now handles the Info & Tags panel more reliably on mobile, especially after reloading, switching views, previewing files, or clearing a selection. This prevents users from seeing an enabled button that opens a hidden or inaccessible panel, reducing confusion when managing documents on phones.
Original PR description
**Steps to reproduce:** - Go to Documents app in mobile - Go to the kanban view - Add some files and select one - Click on `Info & Tags` button in the control panel - Reload the page - Chatter is not…
**Steps to reproduce:** - Go to Documents app in mobile - Go to the kanban view - Add some files and select one - Click on `Info & Tags` button in the control panel - Reload the page - Chatter is not displayed but the button is still enabled - Switching to the list view properly shows it **Issue:** Original fix (see [1]) was not enough for every case. Additional issues: - Chatter hidden on init even when its panel has `visible = true` - State desynchronized with the view when switching menu type (kanban/list) or by previewing a document and coming back - When using the button with an open preview, chatter shows up in the background but is not accessible (and going back discards it) - Removing selection with an open chatter disable the related action **Fix:** - Disable the chatter on mobile init by default to avoid having to manually move it back - Reset chatter on selection removal to avoid getting stuck in the menu - Reset chatter on view switch to avoid being in the wrong state afterwards (and revert the previous css changes) Not a great fix (quite mobile-specific) and there might still be some edge cases. [1] original fix: https://github.com/odoo/enterprise/commit/cf3c2fce8a6b7b2d7547d44a0e4423f887986d52 opw-6061993 Forward-Port-Of: odoo/enterprise#121521
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
Historical Luxembourg payslips now calculate indexed wages using the wage index that was active at the payslip period end date, rather than today's index. This helps ensure past payroll calculations remain accurate when wage indexes change over time.
Original PR description
Historical payslips incorrectly used today's wage index instead of the index active during the payslip period. Now, salary rules evaluate the indexed wage using `payslip.date_to` via the new `_get_l10n_lu_indexed_wage(date)` contract method. Task: 6395557 Forward-Port-Of: odoo/enterprise#125861
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-6381795The Dutch reporting module now skips over status records that are missing the accounting entry needed for a chatter message. This prevents one incomplete tax return record from blocking status updates for all Digipoort tax returns.
Original PR description
The `l10n_nl_reports_sbr_status_info` contains the `l10n_nl_reports_sbr.status.service` class. The class is responsible for fetching the status of sent Digipoort tax returns. The status is then posted as a chatter message to the tax return's closing entry. Issues can arise when one of the status service records is, for whatever reason, missing a closing entry. In such case, the message cannot be posted, resulting in an exception being raised. Since the records are processed in a loop without a try-catch, this causes the whole action to fail. This can lead to one broken record effectively shutting down the whole module's functionality. This PR adds some if-else checks to gracefully handle the case where the closing entry is missing. Related tickets: opw-5901446 and opw-6410082 Forward-Port-Of: odoo/enterprise#127844 Forward-Port-Of: odoo/enterprise#125996
When receiving lot-tracked products with putaway rules, the Barcode app now keeps the intended storage shelf for additional scanned lots. This prevents items from being shown in the wrong stock location and helps warehouse teams process receipts accurately.
Original PR description
Steps to reproduce --- 1. Enable Storage Locations and Lots & Serial Numbers. 2. Add a putaway rule sending a lot-tracked product from WH/Stock to WH/Stock/Shelf 1. 3. Confirm a receipt reserving 2…
Steps to reproduce --- 1. Enable Storage Locations and Lots & Serial Numbers. 2. Add a putaway rule sending a lot-tracked product from WH/Stock to WH/Stock/Shelf 1. 3. Confirm a receipt reserving 2 units of that product; putaway sets the reserved move line destination to WH/Stock/Shelf 1. 4. In the Barcode app, scan a first lot, then a second lot. The second lot lands on a separate line at WH/Stock instead of WH/Stock/Shelf 1. Issue --- The first lot reuses the reserved line and keeps its Shelf 1 destination. The second lot cannot reuse it because its tracking number differs, so `_findLine` returns nothing and `_getNewLineDefaultValues` builds a new line with `location_dest_id` set to `_defaultDestLocation()`, the picking's default destination (WH/Stock). https://github.com/odoo/enterprise/blob/314a79b774f30dc9377b2971492576c4b84483e1/stock_barcode/static/src/models/barcode_picking_model.js#L1591-L1601 Putaway relocates the destination on the move line at reservation, never on the picking, so only the reserved line carries Shelf 1. Since `groupKey` includes `location_dest_id`, the new line does not group with the first lot and shows separately at WH/Stock. This is not a regression: new lines have always defaulted to the operation destination. https://github.com/odoo/enterprise/blob/314a79b774f30dc9377b2971492576c4b84483e1/stock_barcode/static/src/models/barcode_picking_model.js#L239-L241 The new line now inherits the selected line's `location_dest_id`, already relocated by putaway, instead of the default. opw-6317077 Forward-Port-Of: odoo/enterprise#127906 Forward-Port-Of: odoo/enterprise#125309
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_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