Tuesday, September 1, 2026
21 changes · 18.0
Enhancements to existing features
Installing the Colombian DIAN e-invoicing module no longer triggers a large one-time recalculation across all existing invoices. This helps large Colombian accounting databases complete setup without timing out, while day-to-day invoice processing remains unchanged.
Original PR description
- Pre-create the stored computed columns `l10n_co_edi_type`, `l10n_co_dian_state`, and `l10n_co_edi_cufe_cude_ref` in `_auto_init()`. - This prevents Odoo from computing and writing these fields for all existing `account.move` records when installing `l10n_co_dian`. - This is particularly important for large databases with a high volume of Colombian accounting moves, where the initial computation can take too long and cause the module installation to hit the time limit. - The compute methods are kept unchanged, so the fields continue to be computed normally for subsequent record creation or dependency changes. **opw-6451331**
Inventory teams using GS1 barcodes can now scan serial or lot numbers together with expiration or best-before dates during stock counts. This automatically applies the dates to newly created lots, reducing manual entry and improving traceability for perishable or date-sensitive products.
Original PR description
When doing an inventory count with GS1 nomenclature activated, if the user scans a barcode that contains the SN/LN info and its expiration dates (or Best before dates), these dates should be set on the LOT. Task 4795187
Resolved issues and error corrections
Fixes an issue where manually increasing the quantity on a timesheet invoice could prevent later timesheets from being invoiced. Businesses can now invoice future work periods correctly while preserving refund-related behavior.
Original PR description
## Issue When a use rmanually increases the invoiced quantity on a timesheet invoice, subsequent timesheets logged in future periods can no longer be invoiced. The system assumes that the most recent…
## Issue
When a use rmanually increases the invoiced quantity on a timesheet invoice, subsequent timesheets logged in future periods can no longer be invoiced. The system assumes that the most recent logged hours are already covered by the previously over-invoiced amount, blocking the billing of the most recent timesheet entries.
## Steps to reproduce
1. Install *Sales Timesheet* (`sale_timesheet`)
2. Create a Product P
- Product Type: Service
- Invoicing Policy: Based on Timesheets
- Create on Order: (Project &) Task
3. Create an SO:
- Customer: Any
- Product: P (any quantity)
- Confirm
4. Record hours on the task created:
- 06/01/2026 (June 1st): 1 hour
- 07/01/2026 (July 1st): 1 hour
5. Create the invoice for the June timesheet entry (by setting a timesheets period when creating the invoice), then **change the quantity to any value strictly greater than 2** and confirm.
6. Create the invoice for July
7. **An "Invalid Operation" error appears, stating that there's nothing to invoice, even though the timesheet entry from July was never invoiced.**
## Cause
Since https://github.com/odoo/odoo/commit/3b86ac3d180993239b63fd305ee0ba1f17f7eb20, the computation of the quantity to invoice changed to include the difference between the quantity delivered and the quantity already invoiced. In the flow described by the steps to reproduce above, the quantity to invoice is larger than the quantity delivered, making the `qty_to_invoice` equal to `0.0`.
https://github.com/odoo/odoo/blob/626d31fa0191bfe7ca8e1fcf177357eeeb60f2c7/addons/sale_timesheet/models/sale_order.py#L331-L334
The reason for the fix above being the partial refunding of timesheet-related invoices, we can keep that solution when working with refunded invoices, and keep the previous behavior for other cases.
This solution solves the issue in the steps to reproduce above, but was also manually tested on the issues from https://github.com/odoo/odoo/commit/3b86ac3d180993239b63fd305ee0ba1f17f7eb20 and https://github.com/odoo/odoo/commit/64cf4afabd0c5ef040cf87fc6f3125fe0bb81bbb.
opw-6485674
Forward-Port-Of: odoo/odoo#284470Code cleanup and technical improvements
This update restructures how Odoo creates and reads Factur-X electronic invoice files, replacing template-based generation with a more standardized internal structure. The change is mainly internal, helping make future updates and country-specific invoice requirements easier to support while preserving existing business behavior.
Original PR description
*= account_edi_ubl_cii_tax_extension, l10n_account_edi_ubl_cii_tests. Refactoring of the export of factur-x. The export now relies on a dict of nodes, and uses the function `dict_to_xml` to generate the xml instead of relying on a qweb template. task-5999127 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
An older Point of Sale cleanup step was removed because a newer, more targeted fix now handles the issue. This reduces the risk of unintended changes while keeping stale loyalty reward lines properly cleaned up.
Original PR description
This fix is no longer required(https://github.com/odoo/odoo/pull/202752) as this one (https://github.com/odoo/odoo/pull/257043) is cleaner and less aggressive (it will only delete stale reward lines). This is the relevant part of the new fix to delete the previous one: https://github.com/odoo/odoo/pull/257043/changes#diff-18cef42f8c39513a82387e9cb81bc732b252304cd15b5b2f7b0b4623ac20cb41R100-R104 opw-6447092
Point of Sale discount rewards now exclude customer tips, so tips keep their full intended amount even when a coupon or loyalty discount is applied. This prevents incorrect restaurant payment totals and ensures tips are handled separately from discounted order items.
Original PR description
Currently, when a discount reward is applied onto an order and a tip is added later, the discount gets also applied on the tip. Steps to reproduce: ------------------- * Make sure all demo data…
Currently, when a discount reward is applied onto an order and a tip is added later, the discount gets also applied on the tip. Steps to reproduce: ------------------- * Make sure all demo data loyalty programs can also apply to the restaurant * Make sure tipping is enabled in the restaurant * Open restaurant * Open a table, add items to the order * Activate coupon code "10pc" * Select payment * On payment page, add a tip (10$) * Go back to product page > Observation: 10% discount is also applied on the tip Why the fix: ------------ A tipping should never have a discount applied on it. During the computation of the discountable amount we have 3 distinct flow (discount applies on the order, on the cheapest line, on some specific products) - On the order: We always exclude tipping lines - Cheapest line: When fetching the cheapest line amongst all lines we don't consider the tipping lines - On specific: With the fix, tipping lines will always be excluded from the discountable lines. Even if the tip product was specified on the loyalty program. Should we still allow discounting tips if it was set in the program specifically? We're using `is_discountable` instead for `is_tip` in case we want to extend the definition later on. opw-6445364
Invoice PDFs for Guatemala and Uruguay now show the customer identification label that matches the selected ID type, rather than a default country VAT label. This prevents confusion for customers and businesses when invoices use alternative local identification numbers.
Original PR description
Currently invoice reports use the label related to the company's country for the partner's vat number, however in the case of latin america countries, a new field l10n_latam_identification_type_id is…
Currently invoice reports use the label related to the company's country for the partner's vat number, however in the case of latin america countries, a new field l10n_latam_identification_type_id is added to partners that allows the use of different identification types. This results in invoice reports showing the identification number next to an incorrect label. This commit fixes the issue for Guatemala and Uruguay by overriding the vat label in their respective invoice template to match the selected identification type. Steps to reproduce: - Install a latam localisation (Uruguay or Guatemala) - Change company to that country's Company - Create a partner located in said country, ensure the identification number is set to something else than the default one - Create an invoice for the partner, confirm it, and then Send it - In the pdf generated for the invoice you will see that under the partner's address the "vat number" has the wrong label opw-6087359 related to: https://github.com/odoo/odoo/pull/260159
Rental orders can now be created successfully when adding products that include optional rental items, even if the website rental feature is not installed. The fix prevents an error in rental pricing calculations by handling rental dates consistently before prices are computed.
Original PR description
Problem: When the website_sale_renting module is uninstalled and you try to create a rental order with a product that has an optional rental product, the system crashes with a traceback. This happens…
Problem: When the website_sale_renting module is uninstalled and you try to create a rental order with a product that has an optional rental product, the system crashes with a traceback. This happens because the rental start and end dates are passed to the product pricing calculations as plain text strings instead of proper date formats, which breaks the timezone math. Solution: This commit ensures that the rental start and end dates are converted into proper datetime formats before any duration or pricing calculations occur, preventing the error and allowing the products to be added to the rental order smoothly. Steps to reproduce(runbot v18): 1. Install the Rental (sale_renting) and Website modules. 2. Uninstall the website_sale_renting module. 3. Create a rental product and configure another rental product as its optional product. 4. Open the Rental application and try to add the created product to a rental order. 5. A traceback is raised while calculating the rental price. opw-6485776
The Helpdesk team card now aligns the email alias with the team name by removing extra spacing. This provides a cleaner, more consistent layout for users viewing or managing Helpdesk teams.
Original PR description
In this commit, we remove the margin before the mail alias, ensuring aligment within the helpdesk team card. task-6416578
This fixes an internal accounting test so it no longer changes Hungary's currency rounding rules during validation. The change helps prevent false test failures in localized accounting setups without affecting normal business workflows.
Original PR description
The test forced HUF rounding to 1.0, which fails under l10n_hu once HUF has accounting entries. This commit Uses JPY, which already has coarse rounding, instead. [error-946580](https://runbot.odoo.com/odoo/error/946580)
Image upload fields now apply the Android camera workaround only for Android Chromium-based browsers that need it. This prevents other browsers and the mobile app from showing inappropriate file picker options while keeping camera uploads available where Android would otherwise hide them.
Original PR description
Since Android 14, Chromium sends a file input accepting only images straight to the photo picker, which has no "Camera" entry. The image fields work around it by appending `dummy/allowAndroidCamera`…
Since Android 14, Chromium sends a file input accepting only images straight to the photo picker, which has no "Camera" entry. The image fields work around it by appending `dummy/allowAndroidCamera` to their accept attribute: a mimetype which is not an image is enough to get the generic chooser, and its camera, back. https://issues.chromium.org/issues/40937303 That invalid mimetype was appended for everyone, while only the browsers based on Chromium on Android need it: - the issue is an Android one, the desktop file dialogs are not concerned - the native app builds its own file chooser out of the accept attribute, and the invalid mimetype makes it offer the document picker on a field which only accepts images - Firefox and Safari are not based on Chromium and are not affected The workaround is now limited to the browsers needing it, and the expression moved from the template to a getter, since it is no longer a simple concatenation. Code made by Claude Supervised by RFR --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#285643
Responses to invoices received through the French PDP system are now sent through that same PDP channel only, instead of also being sent through the classic Peppol route. This avoids duplicate messages and reduces unnecessary chatter while keeping the invoice response process clearer for users.
Original PR description
When responding to an invoice received through PDP, 2 responses were actually sent: one through PDP and the other through the 'classic' Peppol. This was done to allow more reliability, in case one channel is down when trying to respond, but this mainly creates cluttah' in the chattah' (call me busta rhymes yo) task-6522652 Forward-Port-Of: odoo/odoo#285596
This update corrects how long touch-and-drag actions are handled in Odoo's web testing tools. It prevents an unintended click after a held drag, reducing the risk of tests masking or triggering incorrect interface behavior.
Original PR description
Before this commit, a tap held past `LONG_TAP_DELAY` and released with `drop()` got a click, where the same tap released with `pointerUp()` got none. No test on 18.0 holds a drag that long, so nothing reads that click here. Note that saas-18.3 has one, `selection can be enabled by long touch with drag & drop enabled`, where that click unselects the kanban record the long press just selected. This happens because `drop()` calls the internal `_pointerUp()` without `canBeLongTap`, the flag "[FIX] web: hoot - keep the click of a slow tap on touch" added so that the delay counts only a press the test itself holds. This commit passes that flag from `drop()` too, as a drop is the test releasing the pointer itself.
This fixes a loophole that allowed portal users to change a customer name by creating a different invoice contact. The sales portal now applies the intended restriction consistently, helping preserve accurate customer records linked to sales orders.
Original PR description
Before this commit it was possible to update name by creating an other invoice partner. The AND condition was wrong here.
This fix prevents UPS shipment confirmations from being rejected when a customer's invoice address has no name. The system now uses a fallback partner name for UPS invoicing details, improving reliability for deliveries linked to existing customer billing addresses.
Original PR description
Issue ----- By default, invoicing addresses of existing partners are created without a name. This leads to the deliveries being rejected by UPS. Steps to reproduce ----- - Set Up UPS - Create a Customer - Create an invoicing address with no name - Create a SO for the partner & confirm - UPS delivery - Open the picking and confirm it > UPS rejects the shipment /!\ I could not reproduce in testing environment, so this is based off user steps in their production DB. /!\ Cause ----- The partner being used in `_set_invoice` was changed in #119747 but this use case was missed due to the error not occuring in test mode. ----- Ticket: opw-6485164
Fixed an issue where Planning Gantt rows for the same employee could appear greyed out when their shifts were split across projects. All rows for that employee now correctly display working hours, making schedules easier to read and preventing confusion.
Original PR description
Issue: ---------------------------------------- In the Planning Gantt view, when an employee's slots are split across several rows, all of them are greyed out except the last one, which correctly…
Issue: ---------------------------------------- In the Planning Gantt view, when an employee's slots are split across several rows, all of them are greyed out except the last one, which correctly displays the employee's working hours. Steps to reproduce: ---------------------------------------- - Have an employee with a running contract. - Create two shifts for that employee on two different projects. - Open Planning, switch to the Gantt view, and group by Resource, then by Project. - Only one of the two rows for that employee shows the working hours; the other is greyed out. Cause: ---------------------------------------- The grey background is applied by the Gantt view to any cell that has no matching working period, the same way it greys out days off. The working periods, based on the employee's work intervals are computed in `gantt_resource_employees_working_periods()` and indexed by employee id in a plain dict (`row_per_employee_id`). When the same employee appeared in several rows, each new row overwrote the previous one in the dict, so only the last row kept a reference and got its working periods filled in. Solution: ---------------------------------------- Group the rows per employee into a list instead of a single dict entry, and append the computed `working_periods` to every row belonging to that employee. opw-6449130
Fixed an issue where dismissed mention suggestions could reappear and intercept the Escape key, preventing users from closing replies or popups as expected. This makes the mail composer behavior more predictable, especially when suggestions load slowly or the screen refreshes.
Original PR description
Two independent causes made "reply: discard on pressing escape" red, one commit each. "[FIX] mail: wait for the mention suggestions before Escape" is the one that fixes the reported failure, and it holds on every branch: the test presses Escape while the mention fetch is in flight, and the suggestions arriving from the server re-open the list that Escape closed, so the re-opened list takes the second Escape and the reply is never discarded. The test now waits for the fetched suggestions before pressing Escape. "[FIX] mail: keep the suggestion list closed on a re-render" backports "[FIX] mail: keep composer suggestion list closed on unrelated re-render", which entered at 19.0 and never came down. Here NavigableList is re-opened on every patch, so opening the emoji picker after Escape brings the dismissed list back, and it then steals the Escape meant for the picker. https://runbot.odoo.com/odoo/error/946314
Fixed an issue where invoicing multiple orders together could cause timesheets from one order to be excluded because another order's billing period was incorrectly reused. This helps ensure invoices include the correct delivered timesheet work and reduces silent billing omissions.
Original PR description
### Steps to reproduce: - Install `sale_subscription_timesheet` module - Create a subscription (Order A) with delivered-timesheet products and log timesheets within its current period (e.g., January)…
### Steps to reproduce: - Install `sale_subscription_timesheet` module - Create a subscription (Order A) with delivered-timesheet products and log timesheets within its current period (e.g., January) - Create a standard order (Order B) with delivered-timesheet products and log timesheets outside of Order A's period (e.g., March) - Select both orders and invoice them simultaneously in a single batch (uncheck 'Consolidated Billing') > Result: Order B's invoice is created but silently fails to link its March timesheets, because they fall outside Order A's January window. ### Cause of Issue: In `_link_timesheets_to_invoice`, the `start_date` and `end_date` parameters are overwritten inside the invoice line loop when `_get_range_dates` is called. Because these variables are never reset per iteration, the first order that yields a date range locks in that window for every subsequent invoice line in the batch. Timesheets outside this leaked window are silently ignored. Additionally, the `_get_range_dates` hook is invoked before verifying if the invoice line actually contains timesheet products, causing it to execute on empty recordsets (e.g. invoice line notes) ### Fix: Prevent the date range leak by preserving the original parameter state of the dates and safely ensure the extension hook is never invoked with an empty recordset. opw-6507110 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Adds a test to ensure batch invoicing for multiple subscription orders keeps each order's own date range separate. This helps prevent invoices from including timesheet entries from the wrong period, improving billing accuracy for subscription services.
Original PR description
A test case to guarantee that batch invoicing multiple orders respects their individual time frames. This test is made to assert the fix in this [PR](https://github.com/odoo/odoo/pull/284781) opw-6507110
This fix prevents website pages from failing when a visitor's cookie consent data is malformed or empty. Instead of showing a server error across the site, Odoo clears the invalid cookie and lets the visitor choose cookie preferences again.
Original PR description
Steps to reproduce: =================== 1. Enable the cookies bar on a website. 2. Set a malformed `website_cookies_bar` cookie in the browser (e.g. an empty value). 3. Open any frontend page. =>…
Steps to reproduce:
===================
1. Enable the cookies bar on a website.
2. Set a malformed `website_cookies_bar` cookie in the browser (e.g. an empty value).
3. Open any frontend page.
=> Every frontend page returns a bare 500; the error page cannot
render either, since it goes through the same code path.
Root cause:
===========
`_is_allowed_cookie('optional')` parses the `website_cookies_bar` cookie with `json_scriptsafe.loads` without guarding against invalid JSON. An empty or unparsable value raises `JSONDecodeError`.
The unguarded `loads` has existed since 16.0, but it was only reached in a few situational spots, so a bad cookie was harmless. It became reachable on every page render when [1] added an unconditional `_is_allowed_cookie('optional')` call to
`IrQWeb._prepare_frontend_environment` (run for every frontend QWeb render) and put its result in `_get_template_cache_keys`, so the value must be computed on every render. This is why 17.0 is unaffected and 18.0+ crash.
Fix:
====
Wrap the `loads` in a try/except on `ValueError` (superclass of `JSONDecodeError`) and fall back to `None`. A `None`/non-dict value already flows into the existing pre-16.0 compatibility branch, which resets the cookie (`max_age=0`) and lets the visitor choose again.
[1]: https://github.com/odoo/odoo/commit/958b41c4acec7e1700ca4d6e0b25ee0ad2aac9f1
opw-6380116
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-prThis fix prevents French e-invoicing status updates from failing when the external PDP service returns a response without a tracking identifier. By ignoring untrackable responses, scheduled processing can continue normally and avoid blocking invoice status updates.
Original PR description
Steps to reproduce: - Install `l10n_fr_pdp` and `l10n_be` module - Activate `French e-invoicing` (you need to put your DB in test mode) (i.e: `account_peppol.edi.mode = 'test'` in system parameter)…
Steps to reproduce:
- Install `l10n_fr_pdp` and `l10n_be` module
- Activate `French e-invoicing` (you need to put your DB in test mode)
(i.e: `account_peppol.edi.mode = 'test'` in system parameter) Refer this Documentation https://www.odoo.com/documentation/19.0/applications/finance/fiscal_localizations/france.html?highlight=e%20invoicing#localizations-france-e-invoicing-fac-elec-config
- Create a Invoice and Sent with `E-invoicing`
- Run `PEPPOL: retrieve new documents` Cron
- In Invoice Other Info > `E-Invoicing Status` should be in `Done` state
- Now Vendor Bill is created for that invoice > Cancel that bill with reason > Check `E-Invoicing Status` response for bill(one response will be without UUID)
- Go to schedule actions > `PEPPOL: update message status`
- Run it and get the traceback: `KeyError: 'false'`
Issue:
When sending a lifecycle response to the French PDP, `_pdp_send_response` calls the `/api/pdp/1/send_response` endpoint and expects IAP to return a `message_uuid` for each response.
However, IAP return a response without a message UUID, for example: ` {'messages': [{'message_uuid': False}]}` `_pdp_send_response` currently assumes that every returned message has a valid UUID and creates an `account.peppol.response` with:
```
'peppol_message_uuid': False
'peppol_state': 'processing'
```
This leaves an `account.peppol.response` in `processing` state without a UUID that can be used to track it.
During the next execution of `_cron_peppol_get_message_status`, `_peppol_get_message_status` retrieves this response through `_peppol_get_documents_for_status` and builds `uuid_to_record` using `peppol_message_uuid` as the key. The resulting mapping contains the Python value `False` as a key.
The cron then calls the PDP `1/get_document` endpoint with this invalid UUID. IAP returns it as the string `'false'`, which is passed to `_peppol_process_messages_status`. The French PDP implementation tries to retrieve the corresponding record with: `uuid_to_record[uid]`
Since the mapping contains `False` while `uid` is `'false'`, this raises: `KeyError: 'false'`
This failure prevents the status cron from completing the processing of the messages.
Solution:
Avoid creating the `account.peppol.response` when IAP does not return a `message_uuid`. A response without a UUID cannot be tracked through the PDP status flow, so creating it in `processing` state only leaves an
invalid record that will later cause the status processing to fail.
opw-6453133
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr