Tuesday, September 8, 2026
6 changes · saas-18.3
Enhancements to existing features
Opening a Point of Sale register for a newly created company is now much faster when the database contains very large accounting histories from other companies. The change avoids slow database searches, reducing delays from seconds to milliseconds in large multi-company environments.
Original PR description
When you create a new company in a database that has a lot of existing account move lines and you attempt to open a PoS register from the list view, `_compute_company_has_template` checks…
When you create a new company in a database that has a lot of existing
account move lines and you attempt to open a PoS register from the list
view, `_compute_company_has_template` checks `_existing_accounting` for
the new company and wil run a sequential scan on the entire account_move_line
table followed by a nested loop as the query planner assumes AMLs company_ids
will be roughly evenly distributed.
This is not the case in a new company that has no/very few AMLs.
This is because the query ran is:
`SELECT COUNT(*) FROM
(SELECT FROM "account_move_line"
WHERE (
"account_move_line"."company_id" IN
(SELECT "res_company"."id" FROM
"res_company" WHERE
("res_company"."parent_path" LIKE '3/%')
))
LIMIT 1)`
and the values of company_id being searched for aren't known until the
subquery runs.
Running a query more like
`SELECT COUNT(*) FROM
account_move_line
WHERE company_id IN (%s)`
is much faster
Since res_company will always be a smaller table, we can do the inexpensive
search first and then pass in the values so Postgres can do a cheaper
search and return faster.
Benchmark time of `_existing_accounting`:
| Company 1 AML count | Company 2 AML count | Pre-fix | Post-fix | Multiplier |
|---|---|---|---|---|
| 10,000,000 | 0 | 700 Milliseconds | 500 Microseconds | 1,400x |
| 20,000,000 | 0 | 1.35 Seconds | 1 Millisecond | 1,350x |
| 20,000,000 | 20,000,000 | 2 Milliseconds |1.3 Milliseconds | 1.5x |
| 50,000,000 | 0 | 3.25 Seconds | 1 Millisecond | 3,250x |
| 100,000,000 | 0 | 5.3 Seconds | 1.3 Milliseconds | 4,075x |
Query Plan Before:
```
"Aggregate (cost=0.08..0.09 rows=1 width=8) (actual time=5573.001..5573.002 rows=1.00 loops=1)"
" Buffers: shared read=571435"
" -> Limit (cost=0.00..0.08 rows=1 width=0) (actual time=5572.995..5572.997 rows=0.00 loops=1)"
" Buffers: shared read=571435"
" -> Nested Loop (cost=0.00..https://github.com/odoo/odoo/commit/1571439c0c70c1f1dc3229421e696b97ce1678f8.33 rows=20000086 width=0) (actual time=5572.988..5572.989 rows=0.00 loops=1)"
" Join Filter: (account_move_line.company_id = res_company.id)"
" Buffers: shared read=571435"
" -> Seq Scan on account_move_line (cost=0.00..971432.72 rows=40000172 width=4) (actual time=0.432..1913.934 rows=40000000.00 loops=1)"
" Buffers: shared read=571431"
" -> Materialize (cost=0.00..4.03 rows=1 width=4) (actual time=0.000..0.000 rows=0.00 loops=40000000)"
" Storage: Memory Maximum Storage: 17kB"
" Buffers: shared read=4"
" -> Seq Scan on res_company (cost=0.00..4.03 rows=1 width=4) (actual time=0.785..0.785 rows=0.00 loops=1)"
" Filter: ((parent_path)::text ~~ '3/%'::text)"
" Rows Removed by Filter: 2"
" Buffers: shared read=4"
"Planning:"
" Buffers: shared hit=574 read=77"
"Planning Time: 15.323 ms"
"Execution Time: 5573.060 ms"
```
Query Plan After:
```
"Aggregate (cost=4.46..4.47 rows=1 width=8) (actual time=1.972..1.973 rows=1.00 loops=1)"
" Buffers: shared read=3"
" -> Limit (cost=0.44..4.46 rows=1 width=0) (actual time=1.967..1.968 rows=0.00 loops=1)"
" Buffers: shared read=3"
" -> Index Only Scan using account_move_line__company_id_index on account_move_line (cost=0.44..4.46 rows=1 width=0) (actual time=1.965..1.966 rows=0.00 loops=1)"
" Index Cond: (company_id = 3)"
" Heap Fetches: 0"
" Index Searches: 1"
" Buffers: shared read=3"
"Planning:"
" Buffers: shared hit=3"
"Planning Time: 0.219 ms"
"Execution Time: 1.998 ms"
```
opw-6513885
Forward-Port-Of: odoo/odoo#285096Resolved issues and error corrections
This fixes an issue where loyalty or coupon discounts in the Point of Sale could incorrectly reduce customer tips added during restaurant payments. Tips are now excluded from discount calculations, helping ensure staff gratuities remain accurate and unaffected by promotions.
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 Forward-Port-Of: odoo/odoo#281400
Paid point-of-sale orders now reopen with their order lines and customer details intact, even when the related product or customer was not preloaded. This helps staff process refunds and invoices correctly without reselecting customers or losing order information.
Original PR description
Steps to reproduce: - More products/customers than the loading limits (or a product made unavailable in PoS after its order was placed) - Sell such a product to such a customer, close the order -…
Steps to reproduce:
- More products/customers than the loading limits (or a product made unavailable in PoS after its order was placed)
- Sell such a product to such a customer, close the order
- Reopen the ticket screen in a fresh session and open the paid order
Issue:
The order shows without its orderlines (silently deleted), so it cannot be refunded correctly. Its customer is blank: a refund loses the partner and invoicing asks to pick a customer again.
Cause:
Since 64ca0e5e924a the ticket screen fetches paid orders through `callRelated("pos.order", "get_ticket_screen_order_data")` instead of `data.read("pos.order", ids)`. `read` is part of
`autoLoadedOrmMethods`, so `missingRecursive()` fetched the products and partners referenced by the orders; `callRelated` loads the payload as-is, and `read_pos_data()` includes neither `product.product` nor
`res.partner` records. An orderline whose product is not in the store is deleted by its `setup()`, and an unknown `partner_id` stays unresolved.
Fix:
Send the orderlines' products and the orders' partners along in `get_ticket_screen_order_data()`: `loadData()` links the records before the orderlines' `setup()` runs, so the frontend keeps them. Server-side only, no API change; 19.0 already loads missing records through `read_pos_orders` + `missingRecursive` (f12407c2cf31), so the forward-port is a no-op.
opw-6524807
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#285912This fix prevents users from opening additional shop floor menus while a manufacturing order is loading. It avoids confusing error messages and keeps the Manufacturing shop floor workflow stable, especially on slower connections.
Original PR description
**Steps to reproduce:** * Install the **Manufacturing** module with **Work Orders** enabled. * Create and confirm a Manufacturing Order with at least one Work Order. * Open the **Shop Floor** view. *…
**Steps to reproduce:** * Install the **Manufacturing** module with **Work Orders** enabled. * Create and confirm a Manufacturing Order with at least one Work Order. * Open the **Shop Floor** view. * On a work order card, click the **gear** icon to open the menu dialog. * Click **Open Manufacturing Order** on a slow network connection. * Before the MO form view finishes loading, quickly click the **gear** icon again and open another dialog (e.g. Log Note). * The MO form view loads, destroying the shop floor component. * Close the Log Note dialog. **Observed behavior:** * An `UncaughtPromiseError: Component is destroyed` error is thrown because the dialog tries to interact with the shop floor component that has already been destroyed by the navigation to the MO form view. **Cause:** * When the user clicks "Open Manufacturing Order", `doAction` is called to navigate to the MO form view, and `props.close()` immediately closes the menu dialog. However, the shop floor component is still visible while the new view is loading. * During this gap, the gear button remains clickable. If the user opens another dialog (e.g. Log Note), that dialog holds a reference to the shop floor component. When the MO form view finishes mounting, the shop floor is destroyed, and closing the stale dialog triggers operations on the destroyed component. **Fix:** * Add an `actionPending` state flag to `MrpDisplayRecord`. When the user selects "Open Manufacturing Order" from the menu dialog, an `onSelect` callback sets `actionPending` to `true`, which disables the gear button and prevents any new dialog from being opened. * The flag is only set for `openMO` (which navigates away and destroys the component), not for other menu actions like Scrap, Add Component, or Log Note which open wizard dialogs and return to the shop floor. opw-6107579 Forward-Port-Of: odoo/enterprise#123490
This fix checks for an existing proxy user before requesting new credentials from Odoo's IAP service. It prevents rare concurrent registrations from leaving a database with stale credentials that break later electronic invoicing proxy communication.
Original PR description
In the current flow, _register_proxy_user first calls IAP create_user, then inserts the returned credentials in account_edi_proxy_client.user. At the same time, IAP create_user_2 may replace an existing user with a new one with different credentials before local persistence settles. So Tx A calls IAP and gets credentials for remote user U1. Tx B calls IAP and gets credentials for remote user U2 and unlinks U1. Tx A persists U1 locally. Tx B fails local insert due to a unique constraint. Client DB keeps U1 credentials, but IAP now expects U2. Subsequent proxy calls from the client fail. The DB is left with stale, unusable credentials and cannot recover without re-registration. IAP: https://github.com/odoo/iap-apps/pull/1816 task-6520501 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#285648
Fixed an issue where one task that could not be scheduled correctly caused other tasks assigned to the same person to be pushed far into the future. This keeps dependent project work scheduled in the next available slot, improving planning accuracy in Gantt rescheduling.
Original PR description
Steps to reproduce: 1. create three tasks A,B & C with the same assignee 2. make tasks B and C depend on A and start on the `date_deadline` of task A 3. set the duration of task B (processed before C) to be longer than the 53-week search window 4. reschedule task A to end after task C starts Problem: A candidate that couldn't be scheduled was putting its assignee in conflict, subtracting the inspected availability while failing from the shared pool. Consequently, every later candidate sharing that assignee got force-placed into the same forward reschedule fallback that lands near the tail of the 53-week search window instead of the next available slot. Solution: Each candidate should be evaluated on its own ability to fit, and the time it occupies should count against the shared pool. opw-6380163 --- Forward-Port-Of: odoo/enterprise#129326