Wednesday, July 29, 2026
11 changes · 18.0
Resolved issues and error corrections
The Timesheets overtime indicator now shows the same unit after users switch languages. This prevents confusion where remaining time could appear as hours instead of days for companies using day or half-day timesheet entry.
Original PR description
Steps to reproduce: ------------------- 1. Install Timesheets. 2. Create a new employee with a fully fixed working schedule (40h/week). 3. Open Timesheets > All Timesheets and create a new timesheet…
Steps to reproduce: ------------------- 1. Install Timesheets. 2. Create a new employee with a fully fixed working schedule (40h/week). 3. Open Timesheets > All Timesheets and create a new timesheet in the past week for this employee (e.g. 8 hours). 4. Observe the overtime indicator for the employee (it shows 32 hrs) (Click the left arrow to display it). 5. Change the timesheet encoding unit to "Days / Half-Days". 6. Return to Timesheets and observe the overtime indicator (it now correctly shows 4 days). 7. Install a language other than English (e.g. French). 8. Return to Timesheets and observe the overtime indicator again. Issue: -------- The remaining time value changes unexpectedly and displays 32 hours instead of 4 days. Cause: --------- In `get_timesheet_and_working_hours_for_employees`, the code determines whether the timesheet UoM is expressed in days by comparing the UoM name with the string `"days"`. Since UoM names are translatable, this comparison becomes invalid when the user language changes (e.g. `"jours"` in French), causing the logic to skip the day conversion and return values in hours instead. https://github.com/odoo/enterprise/blob/c48290e90fdeadf4f9ca8c44b035e601e7ed380a/timesheet_grid/models/hr_employee.py#L165-L169 Solution: ----------- Compare the timesheet UoM record with the day UoM record directly instead of relying on translated string values. see commit: https://github.com/odoo/enterprise/commit/5fbf194c5056566453a124812fd2614edfe19a82 opw-6279133 Forward-Port-Of: odoo/enterprise#120595
This fix adjusts the Swiss payroll declaration status check so BVG-LPP pension fund requests no longer include an unnecessary fund number. This helps prevent incorrect or rejected status checks during Swiss payroll electronic transmissions.
Original PR description
Forward-Port-Of: odoo/enterprise#126040
--- ## Steps to Reproduce: 1. Create a service product invoiced on delivered quantities with timesheet tracking (`invoice_policy = 'delivery'`, `service_type = 'timesheet'`). 2. Invoice a Sales Order line using this product for 20h in period A. 3. Create a partial credit note refunding 8h of that invoice, also in period A. 4. In a later, unrelated period B, log 10 new hours on the same SO line. 5. Create Invoice again using "Timesheets Period" set to period B only. ## Issue: The new
Original PR description
--- ## Steps to Reproduce: 1. Create a service product invoiced on delivered quantities with timesheet tracking (`invoice_policy = 'delivery'`, `service_type = 'timesheet'`). 2. Invoice a Sales Order…
--- ## Steps to Reproduce: 1. Create a service product invoiced on delivered quantities with timesheet tracking (`invoice_policy = 'delivery'`, `service_type = 'timesheet'`). 2. Invoice a Sales Order line using this product for 20h in period A. 3. Create a partial credit note refunding 8h of that invoice, also in period A. 4. In a later, unrelated period B, log 10 new hours on the same SO line. 5. Create Invoice again using "Timesheets Period" set to period B only. ## Issue: The new invoice for period B (10h) becomes a credit note (`out_refund`) for -2h, instead of a normal invoice, even though the 10h were never billed. Only reproduces if the SO line has any refund from an earlier period. ## Root Cause: `_recompute_qty_to_invoice()` builds `refund_account_moves` from every refund the SO has ever had, with no date filter. This is used to subtract already invoiced quantity from the current period's delivered quantity (`mapping`), which is correctly scoped to period B. Since `refund_account_moves` isn't scoped, period A's 20h invoice and 8h refund (net 12h) get pulled in again and subtracted from period B's 10h: ``` qty_to_invoice = 10 (period B new hours) - 12 (period A net, wrongly reused) = -2 ``` The negative result auto-converts the invoice into a credit note. ## Fix: Added `invoice_date` filtering to `refund_account_moves`, restricting it to the same `start_date`/`end_date` already used for the delivered-quantity domain. Period A's refund, being outside period B, is no longer counted, so `qty_to_invoice` correctly comes out as 10. ## Result: Invoicing a new period after an earlier partial refund now produces a correct invoice for the actual new hours, instead of a wrong credit note. opw : [6373449](https://www.odoo.com/odoo/project/49/tasks/6373449)
Steps to reproduce: ------------------------------------ 1. Install Time off module 2. Create New Fully Flexible Employee 3. Click on the Time off smart button 4. Create time off for multiple days (eg. Mon - Friday) Observation: ------------------------------------ Number of days still shows 1 Days. Issue: ------------------------------------ Issue occurs because `work_time_per_day_mapped` returns one interval per day for standard and flexible schedules in multi-day time off requ
Original PR description
Steps to reproduce: ------------------------------------ 1. Install Time off module 2. Create New Fully Flexible Employee 3. Click on the Time off smart button 4. Create time off for multiple days…
Steps to reproduce: ------------------------------------ 1. Install Time off module 2. Create New Fully Flexible Employee 3. Click on the Time off smart button 4. Create time off for multiple days (eg. Mon - Friday) Observation: ------------------------------------ Number of days still shows 1 Days. Issue: ------------------------------------ Issue occurs because `work_time_per_day_mapped` returns one interval per day for standard and flexible schedules in multi-day time off requests, so the interval count correctly matches the number of leave days. https://github.com/odoo/odoo/blob/d73e5662a0af7c549008661f743ba5d51f765339/addons/hr_holidays/models/hr_leave.py#L460-L461 However, for fully flexible schedules, it returns a single interval containing the total hours across all days, causing the leave duration to always be computed as 1 day regardless of the actual number of days requested. Solution: ------------------------------------ For fully flexible employees, count the actual calendar days and subtract public holidays when applicable. opw-6060552
The getter `getLoadedDataSources` was filtering out datasources that are not 'ready' but they should actually filter out datasources that were already loaded (so ready) but invalid. Task: 6387729 Description of the issue/feature this PR addresses: Current behavior before PR: Desired behavior after PR is merged: --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Original PR description
The getter `getLoadedDataSources` was filtering out datasources that are not 'ready' but they should actually filter out datasources that were already loaded (so ready) but invalid. Task: 6387729 Description of the issue/feature this PR addresses: Current behavior before PR: Desired behavior after PR is merged: --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Routes inherited from product category are not available when on replenish. Steps to reproduce: ------------------- * Create a new route and allow it on product category * Create a product category and add the route * Add the category to a product * use the repenish button -> the category route is not available on the replenishment Observation: ------------- When opening replenishment, it will open the view_product_replenish, in that view, it will use allowed_route_ids to allow us
Original PR description
Routes inherited from product category are not available when on replenish. Steps to reproduce: ------------------- * Create a new route and allow it on product category * Create a product category…
Routes inherited from product category are not available when on replenish. Steps to reproduce: ------------------- * Create a new route and allow it on product category * Create a product category and add the route * Add the category to a product * use the repenish button -> the category route is not available on the replenishment Observation: ------------- When opening replenishment, it will open the view_product_replenish, in that view, it will use allowed_route_ids to allow us to choose a route for this replenishment: https://github.com/odoo/odoo/blob/2f25b2a70eca5b4f3c2e1d6c7003b1760afc0ba0/addons/stock/wizard/product_replenish_views.xml#L37 allowed_route_ids is compute in the mixin with the following domain: https://github.com/odoo/odoo/blob/2f25b2a70eca5b4f3c2e1d6c7003b1760afc0ba0/addons/stock/models/stock_replenish_mixin.py#L18-L21 https://github.com/odoo/odoo/blob/2f25b2a70eca5b4f3c2e1d6c7003b1760afc0ba0/addons/stock/models/stock_replenish_mixin.py#L25-L31 this only take into account route from the product and not the ones from the product category. https://github.com/odoo/odoo/blob/deeecf7cd02e7383b591835b0c6495e3ddead0ff/addons/stock/models/stock_location.py#L511 opw-6297308
### Issue: When importing EDI documents such as Peppol files, Swiss VAT numbers may be formatted or unformatted When `base_vat` is installed, VAT is stored formatted in Odoo (e.g. `CHE-530.781.296 TVA`) If the format differs, the matching fails and a new partner is created at each import ### Cause: `_retrieve_partner` lacked Swiss-specific VAT normalization logic in `_get_country_specific_vat_variants`, causing it to miss formatted variants with language suffixes (`TVA`, `MWST`, `IVA`)
Original PR description
### Issue: When importing EDI documents such as Peppol files, Swiss VAT numbers may be formatted or unformatted When `base_vat` is installed, VAT is stored formatted in Odoo (e.g. `CHE-530.781.296…
### Issue: When importing EDI documents such as Peppol files, Swiss VAT numbers may be formatted or unformatted When `base_vat` is installed, VAT is stored formatted in Odoo (e.g. `CHE-530.781.296 TVA`) If the format differs, the matching fails and a new partner is created at each import ### Cause: `_retrieve_partner` lacked Swiss-specific VAT normalization logic in `_get_country_specific_vat_variants`, causing it to miss formatted variants with language suffixes (`TVA`, `MWST`, `IVA`) Even when the match succeeded, `_import_ubl_create_missing_customer` still compared VAT strings without stripping `-` and lang suffixes for CH partners, causing a false mismatch and triggering partner creation anyway ### Notes: The partner match improvement is backported from 18.3: https://github.com/odoo/odoo/commit/f1a5a3d72a26ee471d2ef1d8034136208b2bcebc The original fix was incomplete — it added `_get_country_specific_vat_variants` but missed the VAT comparison fix in `_import_ubl_create_missing_customer`, allowing the issue to persist after a successful match `base_vat` is required to get the fix fully working ### Steps to reproduce: - Install `account` and `base_vat` - Create a Vendor (Name: Test CH Vendor, Country: Switzerland, Tax ID: CHE-530.781.296 TVA) - Import a [Peppol Bill](https://github.com/user-attachments/files/27202997/CH_bill_to_import.xml) with VAT `CHE530781296TVA` Before the fix, a new partner is created instead of matching the existing one opw-6353387
Steps to reproduce: - Enable the ZUGFeRD (or Factur-X) e-invoicing format on a German customer. - Create a sale order for that customer, confirm it, create an invoice with a down payment. - Confirm and send the invoice to generate the PDF/XML. - Validate the XML (e.g. on portinvoice.com): it is rejected because the invoice line is missing the mandatory ram:Name field, only ram:Description is present. Cause of the issue: a down payment invoice line created from a sale order no l
Original PR description
Steps to reproduce: - Enable the ZUGFeRD (or Factur-X) e-invoicing format on a German customer. - Create a sale order for that customer, confirm it, create an invoice with a down payment. - Confirm and send the invoice to generate the PDF/XML. - Validate the XML (e.g. on portinvoice.com): it is rejected because the invoice line is missing the mandatory ram:Name field, only ram:Description is present. Cause of the issue: a down payment invoice line created from a sale order no longer carries a product_id: it only has a free-text. The Factur-X/CII export template rendered ram:Name directly from line.product_id.name with no fallback. For a line without a product, this produced an empty ram:Name element, which cleanup_xml_node then stripped entirely from the XML, leaving only ram:Description. Solution: Fall back to the line's name when there is no product opw-6391121
When a product's attribute uses the "multi" display type and only a single value is assigned to it, adding the product to the order added it directly, without ever showing the configuration popup. The operator was therefore unable to (de)select that value. opw-6413892 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Original PR description
When a product's attribute uses the "multi" display type and only a single value is assigned to it, adding the product to the order added it directly, without ever showing the configuration popup. The operator was therefore unable to (de)select that value. opw-6413892 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
On Firefox, history.replaceState() enforces a strict size limit (~640KB) on the state object. Customers with a large number of folders in the Documents module cause the serialized history state to exceed this limit, resulting in an uncaught NS_ERROR_ILLEGAL_VALUE that breaks all navigation on Firefox. To fix this, we wrap history.pushState() and history.replaceState() calls in doPush() with a try/catch that catches NS_ERROR_ILLEGAL_VALUE and falls back to storing null state. opw-6182687
Original PR description
On Firefox, history.replaceState() enforces a strict size limit (~640KB) on the state object. Customers with a large number of folders in the Documents module cause the serialized history state to exceed this limit, resulting in an uncaught NS_ERROR_ILLEGAL_VALUE that breaks all navigation on Firefox. To fix this, we wrap history.pushState() and history.replaceState() calls in doPush() with a try/catch that catches NS_ERROR_ILLEGAL_VALUE and falls back to storing null state. opw-6182687
Miscellaneous changes
**Description of the issue/feature this PR addresses:** When some stock.picking.type records have stock.picking.batch records, the count_picking_batch displays the correct value, but when the user clicks the button, it shows a different number of records because it is not filtered by the corresponding stock.picking.type. TODO: We replace the context on the Python side because the same action is used in the menu to go to Inventory / Operations / Jobs / Batch Transfers. I think it is necessary
Original PR description
**Description of the issue/feature this PR addresses:** When some stock.picking.type records have stock.picking.batch records, the count_picking_batch displays the correct value, but when the user…
**Description of the issue/feature this PR addresses:** When some stock.picking.type records have stock.picking.batch records, the count_picking_batch displays the correct value, but when the user clicks the button, it shows a different number of records because it is not filtered by the corresponding stock.picking.type. TODO: We replace the context on the Python side because the same action is used in the menu to go to Inventory / Operations / Jobs / Batch Transfers. I think it is necessary to create a new action to use in the button, different from the one used in the menu, but due to stable policies, we avoid this change. **Current behavior before PR:** The records displayed when the user clicks the button do not match the record count indicated on the button. https://github.com/user-attachments/assets/24f23650-8161-40ec-8177-0a2898004f6f **Desired behavior after PR is merged:** The record count shown on the button matches the records displayed when the user clicks the button and opens the action. https://github.com/user-attachments/assets/3c550575-b4fa-40c9-89fc-eeedf66d069d TT60248 CC: @Tecnativa @pedrobaeza --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr