Daily updates from Odoo
Thursday, August 6, 2026
40 changes
7 changes
Enhancements to existing features
The signing workflow now avoids loading unnecessary role data when preparing a signing template. This reduces background work and can make the Sign app more responsive, especially for databases with many signing roles.
Original PR description
This `search_read` is done - without `domain` which means it fetches the entire table - without `fields` which means it fetches all fields And the business code only actually needs the id of the very first record which is returned. See `SignTemplateIframe`'s constuctor: `this.props.signRoles[0].id;` AFAICS, that's the only place where it's used. This will be better refactored in master. Forward-Port-Of: odoo/enterprise#126835
Bank connection synchronization now recognizes warnings that should not stop the connection from working. This helps avoid unnecessary error states for users when the issue does not require blocking the online banking link.
Original PR description
Odoofin now sends a 'non_blocking_error' error response to indicate that the state on account.online.link shouldn't be set to error. In this commit, we start using it. Task ID: 6358809 Forward-Port-Of: odoo/enterprise#126737 Forward-Port-Of: odoo/enterprise#123287
Comply with new provincial fiscal transparency regulations (CABA, Entre Ríos, Chubut, and other provinces still not defined) that mandate detailing Perception IIBB taxes on B2C invoices (Factura/Invoice B): - Some jurisdictions mandate a new description for IIBB perception taxes on the invoice, - We need to put the IIBB perception taxes in the special "Fiscal Transparency Regime" table, and exclude them from the standard tax table, task-6367087 Forward-Port-Of: odoo/odoo#278729 Forwar
Original PR description
Comply with new provincial fiscal transparency regulations (CABA, Entre Ríos, Chubut, and other provinces still not defined) that mandate detailing Perception IIBB taxes on B2C invoices (Factura/Invoice B): - Some jurisdictions mandate a new description for IIBB perception taxes on the invoice, - We need to put the IIBB perception taxes in the special "Fiscal Transparency Regime" table, and exclude them from the standard tax table, task-6367087 Forward-Port-Of: odoo/odoo#278729 Forward-Port-Of: odoo/odoo#277673
The decorator `no_retry` can be used on any objects and just sets a flag. When checking if we should retry a test, let's also check the value on the class. This was broken by 2423b460526e9e7dd9ea8a7b3f7e40c93ab708f5. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280641
Original PR description
The decorator `no_retry` can be used on any objects and just sets a flag. When checking if we should retry a test, let's also check the value on the class. This was broken by 2423b460526e9e7dd9ea8a7b3f7e40c93ab708f5. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280641
The feature was never merged on IAP/Internal's side and we now have a new task to move the setting up of the auto-refill from the local db to the IAP server, so the feature is no longer relevant on the client side. Task-6397951 Forward-Port-Of: odoo/odoo#280745 Forward-Port-Of: odoo/odoo#277512
Original PR description
The feature was never merged on IAP/Internal's side and we now have a new task to move the setting up of the auto-refill from the local db to the IAP server, so the feature is no longer relevant on the client side. Task-6397951 Forward-Port-Of: odoo/odoo#280745 Forward-Port-Of: odoo/odoo#277512
This commit adds 3 new `Tax Exemption Reason Code`: - VATEX-FR-F - VATEX-FR-I - VATEX-FR-J task-6333649 Forward-Port-Of: odoo/odoo#280537 Forward-Port-Of: odoo/odoo#278086
Original PR description
This commit adds 3 new `Tax Exemption Reason Code`: - VATEX-FR-F - VATEX-FR-I - VATEX-FR-J task-6333649 Forward-Port-Of: odoo/odoo#280537 Forward-Port-Of: odoo/odoo#278086
Some more improvements to refine the production process: - Allow computation of workorders' expected duration for a full production. - Remove writing into `qty_produced` while setting workorders states, unless its being marked as done. Task: 6421044 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Original PR description
Some more improvements to refine the production process: - Allow computation of workorders' expected duration for a full production. - Remove writing into `qty_produced` while setting workorders states, unless its being marked as done. Task: 6421044 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
6 changes
Enhancements to existing features
Bank synchronization now recognizes a new type of temporary error from Odoofin without marking the bank connection as failed. This helps avoid unnecessary disruption for users when an issue does not require stopping the connection.
Original PR description
Odoofin now sends a 'non_blocking_error' error response to indicate that the state on account.online.link shouldn't be set to error. In this commit, we start using it. Task ID: 6358809 Forward-Port-Of: odoo/enterprise#126737 Forward-Port-Of: odoo/enterprise#123287
Motivation ---------- Each database served keeps a full registry in a process-wide LRU. The LRU is bounded by a count, so on a server hosting thousands of databases the number of retained registries follows traffic rather than memory pressure. Their combined footprint can push a worker past its virtual-memory soft limit, at which point it is killed and restarted. On a server with ~2500 databases, the soft limit is reached at ~180 resident databases while the LRU could still hold ~210, so HT
Original PR description
Motivation ---------- Each database served keeps a full registry in a process-wide LRU. The LRU is bounded by a count, so on a server hosting thousands of databases the number of retained registries…
Motivation ---------- Each database served keeps a full registry in a process-wide LRU. The LRU is bounded by a count, so on a server hosting thousands of databases the number of retained registries follows traffic rather than memory pressure. Their combined footprint can push a worker past its virtual-memory soft limit, at which point it is killed and restarted. On a server with ~2500 databases, the soft limit is reached at ~180 resident databases while the LRU could still hold ~210, so HTTP workers were being recycled under normal load. Tracking usage -------------- Every request for a registry goes through the single lookup in the registry constructor, which stamps it with a monotonic timestamp; the stamp is also set when a registry is first built. Collecting idle registries -------------------------- A collection pass drops every registry whose last use is older than the configured idle timeout. It runs at the end of registry loading, so it fires periodically as databases come and go. Registries that are still loading are skipped, so a concurrent build is never collected. Dropping a registry only detaches it from the LRU: a request still holding a reference keeps working, and the next lookup rebuilds it. The timeout is read from ODOO_REGISTRY_MAX_IDLE_TIMEOUT, in seconds; a value of zero, the default, disables the mechanism so behaviour is unchanged unless it is opted into. Results ------- With a five-minute timeout on the same ~2500-database server, the HTTP workers settle at around 40 resident registries instead of saturating memory on the long run. The gevent worker, which sees every web client reconnect at startup and briefly fills the LRU with ~150 databases, releases most of them on the first pass, reclaiming the memory. On a real-life SaaS server with 64GB of RAM, that frees up to ~10GB which were previously taken by unused registries in the LRU. It comes at the expense of extra registry recomputes, but on the other hand workers do not reach their virtual memory limit anymore. closes odoo/odoo#276581 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 Forward-Port-Of: odoo/odoo#278236
Comply with new provincial fiscal transparency regulations (CABA, Entre Ríos, Chubut, and other provinces still not defined) that mandate detailing Perception IIBB taxes on B2C invoices (Factura/Invoice B): - Some jurisdictions mandate a new description for IIBB perception taxes on the invoice, - We need to put the IIBB perception taxes in the special "Fiscal Transparency Regime" table, and exclude them from the standard tax table, task-6367087 Forward-Port-Of: odoo/odoo#278729 Forwar
Original PR description
Comply with new provincial fiscal transparency regulations (CABA, Entre Ríos, Chubut, and other provinces still not defined) that mandate detailing Perception IIBB taxes on B2C invoices (Factura/Invoice B): - Some jurisdictions mandate a new description for IIBB perception taxes on the invoice, - We need to put the IIBB perception taxes in the special "Fiscal Transparency Regime" table, and exclude them from the standard tax table, task-6367087 Forward-Port-Of: odoo/odoo#278729 Forward-Port-Of: odoo/odoo#277673
The decorator `no_retry` can be used on any objects and just sets a flag. When checking if we should retry a test, let's also check the value on the class. This was broken by 2423b460526e9e7dd9ea8a7b3f7e40c93ab708f5. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280641
Original PR description
The decorator `no_retry` can be used on any objects and just sets a flag. When checking if we should retry a test, let's also check the value on the class. This was broken by 2423b460526e9e7dd9ea8a7b3f7e40c93ab708f5. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280641
The feature was never merged on IAP/Internal's side and we now have a new task to move the setting up of the auto-refill from the local db to the IAP server, so the feature is no longer relevant on the client side. Task-6397951 Forward-Port-Of: odoo/odoo#277512
Original PR description
The feature was never merged on IAP/Internal's side and we now have a new task to move the setting up of the auto-refill from the local db to the IAP server, so the feature is no longer relevant on the client side. Task-6397951 Forward-Port-Of: odoo/odoo#277512
This commit adds 3 new `Tax Exemption Reason Code`: - VATEX-FR-F - VATEX-FR-I - VATEX-FR-J task-6333649 Forward-Port-Of: odoo/odoo#280537 Forward-Port-Of: odoo/odoo#278086
Original PR description
This commit adds 3 new `Tax Exemption Reason Code`: - VATEX-FR-F - VATEX-FR-I - VATEX-FR-J task-6333649 Forward-Port-Of: odoo/odoo#280537 Forward-Port-Of: odoo/odoo#278086
6 changes
Enhancements to existing features
Bank synchronization now recognizes a new type of warning from Odoo’s financial connection service that should not stop the connection. This helps avoid unnecessary error states, keeping bank links active when an issue is informational or temporary rather than blocking.
Original PR description
Odoofin now sends a 'non_blocking_error' error response to indicate that the state on account.online.link shouldn't be set to error. In this commit, we start using it. Task ID: 6358809 Forward-Port-Of: odoo/enterprise#126737 Forward-Port-Of: odoo/enterprise#123287
Odoo now checks whether a payment or batch payment exceeds the maximum amount allowed by the connected financial institution before attempting to send it. This helps prevent failed payment submissions and gives businesses earlier visibility when a bank-imposed limit blocks a payment.
Original PR description
Before trying to initiate payments through Odoo/Odoofin, we should check that the total amount for the (batch) payment does not exceed the maximum payment amount allowed by the institution (some Powens institutions introduced that limit). task-6310729 Forward-Port-Of: odoo/enterprise#126699 Forward-Port-Of: odoo/enterprise#121513
Before this commit, contains() and its variants gave the client 3 seconds, and the bus helpers 2 seconds. The problem is that the first wait after openDiscuss pays for the whole mount, /mail/data and /discuss/channel/messages. Measured from openDiscuss resolving to the message being in the DOM: - 250 to 460ms on an idle machine; - 867 to 5258ms over 10 runs with the CPU throttled 4x, which is what a busy runbot looks like, 3 of the 10 over 2 seconds; - 1474 to 6912ms with the CPU throttl
Original PR description
Before this commit, contains() and its variants gave the client 3 seconds, and the bus helpers 2 seconds. The problem is that the first wait after openDiscuss pays for the whole mount, /mail/data and…
Before this commit, contains() and its variants gave the client 3 seconds, and the bus helpers 2 seconds. The problem is that the first wait after openDiscuss pays for the whole mount, /mail/data and /discuss/channel/messages. Measured from openDiscuss resolving to the message being in the DOM:
- 250 to 460ms on an idle machine;
- 867 to 5258ms over 10 runs with the CPU throttled 4x, which is what a
busy runbot looks like, 3 of the 10 over 2 seconds;
- 1474 to 6912ms with the CPU throttled 6x, 5 of 6 over 3 seconds.
"Reactions are ordered by id" fails 1 run in 60 at 4x for that reason.
Note that a longer timeout costs nothing on a green build: the timer is cleared as soon as the element is there, so it only delays the report of a test that was going to fail anyway.
This commit raises both to 10 seconds, the delay a tour step already gets in macro.js. test_js.py runs the presets with timeout=15000, so hoot fails the test itself at 15 seconds and 10 leaves room for the rest of the test.
This should also close most of the open runbot errors shaped like:
Failed to find x of "..." (Timeout of 3 seconds). Found 0 instead.
The element does show up in those, just after the wait gave up.
https://runbot.odoo.com/odoo/error/944188
web companion https://github.com/odoo/odoo/pull/279984
Forward-Port-Of: odoo/odoo#280586
Forward-Port-Of: odoo/odoo#279983The feature was never merged on IAP/Internal's side and we now have a new task to move the setting up of the auto-refill from the local db to the IAP server, so the feature is no longer relevant on the client side. Task-6397951 Forward-Port-Of: odoo/odoo#277512
Original PR description
The feature was never merged on IAP/Internal's side and we now have a new task to move the setting up of the auto-refill from the local db to the IAP server, so the feature is no longer relevant on the client side. Task-6397951 Forward-Port-Of: odoo/odoo#277512
Comply with new provincial fiscal transparency regulations (CABA, Entre Ríos, Chubut, and other provinces still not defined) that mandate detailing Perception IIBB taxes on B2C invoices (Factura/Invoice B): - Some jurisdictions mandate a new description for IIBB perception taxes on the invoice, - We need to put the IIBB perception taxes in the special "Fiscal Transparency Regime" table, and exclude them from the standard tax table, task-6367087 Forward-Port-Of: odoo/odoo#278729 Forwar
Original PR description
Comply with new provincial fiscal transparency regulations (CABA, Entre Ríos, Chubut, and other provinces still not defined) that mandate detailing Perception IIBB taxes on B2C invoices (Factura/Invoice B): - Some jurisdictions mandate a new description for IIBB perception taxes on the invoice, - We need to put the IIBB perception taxes in the special "Fiscal Transparency Regime" table, and exclude them from the standard tax table, task-6367087 Forward-Port-Of: odoo/odoo#278729 Forward-Port-Of: odoo/odoo#277673
The decorator `no_retry` can be used on any objects and just sets a flag. When checking if we should retry a test, let's also check the value on the class. This was broken by 2423b460526e9e7dd9ea8a7b3f7e40c93ab708f5. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280641
Original PR description
The decorator `no_retry` can be used on any objects and just sets a flag. When checking if we should retry a test, let's also check the value on the class. This was broken by 2423b460526e9e7dd9ea8a7b3f7e40c93ab708f5. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280641
5 changes
Enhancements to existing features
Bank synchronization now recognizes advisory errors from Odoofin that should not interrupt the connection. This helps avoid unnecessarily marking bank links as failed when the issue does not block synchronization.
Original PR description
Odoofin now sends a 'non_blocking_error' error response to indicate that the state on account.online.link shouldn't be set to error. In this commit, we start using it. Task ID: 6358809 Forward-Port-Of: odoo/enterprise#126737 Forward-Port-Of: odoo/enterprise#123287
Odoo now checks whether a payment or batch payment exceeds the maximum amount allowed by the connected financial institution before trying to process it. This helps prevent failed payment attempts and gives businesses earlier warning when a bank-imposed limit applies.
Original PR description
Before trying to initiate payments through Odoo/Odoofin, we should check that the total amount for the (batch) payment does not exceed the maximum payment amount allowed by the institution (some Powens institutions introduced that limit). task-6310729 Forward-Port-Of: odoo/enterprise#126699 Forward-Port-Of: odoo/enterprise#121513
The decorator `no_retry` can be used on any objects and just sets a flag. When checking if we should retry a test, let's also check the value on the class. This was broken by 2423b460526e9e7dd9ea8a7b3f7e40c93ab708f5. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280641
Original PR description
The decorator `no_retry` can be used on any objects and just sets a flag. When checking if we should retry a test, let's also check the value on the class. This was broken by 2423b460526e9e7dd9ea8a7b3f7e40c93ab708f5. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280641
This commit adds 3 new `Tax Exemption Reason Code`: - VATEX-FR-F - VATEX-FR-I - VATEX-FR-J task-6333649 Forward-Port-Of: odoo/odoo#280537 Forward-Port-Of: odoo/odoo#278086
Original PR description
This commit adds 3 new `Tax Exemption Reason Code`: - VATEX-FR-F - VATEX-FR-I - VATEX-FR-J task-6333649 Forward-Port-Of: odoo/odoo#280537 Forward-Port-Of: odoo/odoo#278086
Doing an euclidean division on floats with the native operators is unreliable: because of IEEE-754 representation errors, `value1 % value2` can return a spurious remainder (e.g. `50.4 % 16.8 == 16.799999999999997` instead of 0.0) and `int(value1 / value2)` can truncate the quotient one step too low (e.g. `int(0.3 / 0.1) == 2` instead of 3). `float_div` returns the `(quotient, remainder)` pair free of those errors. The key is to never run a lossy `%` or `//` on the raw floats. Instead both ope
Original PR description
Doing an euclidean division on floats with the native operators is unreliable: because of IEEE-754 representation errors, `value1 % value2` can return a spurious remainder (e.g. `50.4 % 16.8 ==…
Doing an euclidean division on floats with the native operators is unreliable: because of IEEE-754 representation errors, `value1 % value2` can return a spurious remainder (e.g. `50.4 % 16.8 == 16.799999999999997` instead of 0.0) and `int(value1 / value2)` can truncate the quotient one step too low (e.g. `int(0.3 / 0.1) == 2` instead of 3). `float_div` returns the `(quotient, remainder)` pair free of those errors. The key is to never run a lossy `%` or `//` on the raw floats. Instead both operands are first snapped onto the precision grid with `float_round` and then scaled to integers: since a grid-snapped value is a multiple of `rounding`, dividing it by `rounding` counts how many grid steps it spans. That division is still noisy (`4.35 / 0.05 == 86.99999999999999`), so the result is passed through `builtins.round` to coerce it to the exact integer step count. The euclidean division itself is then a plain integer `divmod`, which is exact, and the remainder is scaled back to real units. This is why the correction is applied to the inputs and not to the output: rounding the result of a native `%` would only round an already-corrupt value, and would still misreport the quotient in the corner cases the util exists to handle. Dividing by `rounding` is meaningful for any precision, not only powers of ten: the grid step can be `0.05`, `0.25`, `0.5`, `0.03`, ... and `value / step` counts the steps in every case. This mirrors the normalize/denormalize scheme `float_round` already uses internally. The util shares `float_round`'s inherent limitation: the scaled step count must stay representable as an exact `float` integer, so exactness is lost past ~2**53 grid steps (extreme magnitudes at a fine precision). This is the IEEE-754 double-precision ceiling and is well outside any realistic quantity or price range. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280456 Forward-Port-Of: odoo/odoo#277160
2 changes
Enhancements to existing features
Bank synchronization now recognizes a new type of warning from Odoo’s financial connection service that should not stop the connection. This helps avoid unnecessary error states, keeping online bank links active when the issue is not blocking.
Original PR description
Odoofin now sends a 'non_blocking_error' error response to indicate that the state on account.online.link shouldn't be set to error. In this commit, we start using it. Task ID: 6358809 Forward-Port-Of: odoo/enterprise#123287
Odoo now checks whether a payment or batch payment exceeds the maximum amount allowed by the connected financial institution before trying to send it. This helps prevent failed payment attempts and gives businesses earlier feedback when a bank-imposed limit applies.
Original PR description
Before trying to initiate payments through Odoo/Odoofin, we should check that the total amount for the (batch) payment does not exceed the maximum payment amount allowed by the institution (some Powens institutions introduced that limit). task-6310729 Forward-Port-Of: odoo/enterprise#126699 Forward-Port-Of: odoo/enterprise#121513
2 changes
Enhancements to existing features
Appointment pages now calculate bookable capacities more efficiently, especially when resources can be combined. This reduces slow page loads from minutes to seconds in affected cases and also corrects some capacity values shown during resource selection.
Original PR description
Before --- - When opening the appointment page, the possible capacities that a user can book needs to be calculated to show the capacity dropdown. - This is currently done by getting all the possible…
Before
---
- When opening the appointment page, the possible capacities that a user can book needs to be calculated to show the capacity dropdown.
- This is currently done by getting all the possible combinations that could arise from each resource along with its linked resources (Linked resources are resources that are used to combine with the main resource to allow a bigger capacity)
- The complexity of this approach blows up with the increase in linked resources. For each resource we end up with O(2^m) where m is the number of linked resources it has. If they have the same number of linked resources, and n is the number of resources, we end up with O(n*2^m)
Solution:
- Flipping the algorithm, we create a structure for possible capacities and only keep the best combination for a found capacity.
- We first store all available capacity for relevant resources.
Algorithm
---
For a resource:
- We initialize the solution S to {0: []}
- Greedy dynamic programming is then used to get all the possible combinations by building the result dict, adding one resource at a time. We add its capacity to all entries of S -> we use the combination if a new capacity is reached OR update the combination if the number of elements of combination is less than the existing one.
Then, for the global solution:
- We go through S of resources (in the order of self), and add entries to G (the general dict of solutions). Again, on collision, we only keep the lower-cardinal combination.
The complexity of this algorithm is O(n*u) where u is the dynamic programming complexity. u would be quadratic O(m^2) if the sub-sums of capacities overlap heavily, for instance when resources have the same capacity, like tables of a restaurant, but could reach (2^m) in the worst case scenario.
Related changes / side effects
---
1. Fixing an issue
2. Cleaning strange logic
3. Cleaning max capacity computation
Benchmark:
- opw-5177932 goes down from 6 mins to less than 2 seconds using the new algorithm.
Also include
---
Fix the max capacity on resources when skipping resource selection. It computed the max capa based on values that were not yet emptied in the website_appointment controller.
Task-6233563
Forward-Port-Of: odoo/enterprise#118981Online bank synchronization now recognizes a new type of warning from Odoo’s financial connection service that should not stop the connection. This helps avoid unnecessary error statuses and keeps bank links active when the issue is not blocking.
Original PR description
Odoofin now sends a 'non_blocking_error' error response to indicate that the state on account.online.link shouldn't be set to error. In this commit, we start using it. Task ID: 6358809 Forward-Port-Of: odoo/enterprise#123287
8 changes
Enhancements to existing features
GSTR-1 export generation for Indian GST reporting has been optimized to handle large datasets with much lower memory use and faster processing. This helps businesses complete large tax report exports more reliably without hitting time or memory limits.
Original PR description
Current Implementation: ======================= The current implementation of _get_l10n_in_gstr1_json relies on multiple iterations over ORM recordsets. Since the ORM loads multiple fields rather…
Current Implementation: ======================= The current implementation of _get_l10n_in_gstr1_json relies on multiple iterations over ORM recordsets. Since the ORM loads multiple fields rather than only the required fields, memory consumption grows significantly for large datasets (around 700 MB for 150K account move lines). Additionally, the method builds a single large dictionary from _get_tax_details that is tailored for the Indian GST reporting logic. Constructing and holding this intermediate data structure further increases memory usage. The combination of repeated Python loops, ORM overhead, and the large intermediate dictionary results in high execution time and memory consumption, causing the process to exceed the available time and memory limits for large exports. Solution: ========= Instead of processing tax details through the ORM, create a temporary table containing the GST tax details and query this subset directly for each GSTR-1 subsection. This approach bypasses the ORM, fetching only the required columns instead of entire records. Eliminates the need to build the large _get_tax_details dictionary. Reduces the number of Python-side iterations and intermediate data structures. Pushes the data aggregation and filtering to SQL, where it is more efficient. Restricts Python's responsibility to formatting the final JSON output. This significantly reduces memory usage, improves execution speed, and makes the implementation simpler and easier to maintain. task-3941950
Customer portal counters are now calculated with only the level of detail each portal item needs. Alerts still show exact counts, while other portal entries use a faster existence check to improve performance without changing what users see.
Original PR description
Make sure the search_count performed on the portal entries to get the customer portal counters are using the correct limit depending on what is actually needed. Portal entries of type "alert" need the accurate count to be displayed on the alert, no limit should be used. Other portal entries only need to know whether there is at least one matching record in order to display the entry or not, limiting the search_count to 1 to gain some performance. Task-5387495
This update adds more automated checks around marketing automation participant synchronization, especially when scheduled processing fails. The goal is to catch regressions earlier and improve confidence in future performance and reliability work for large databases.
Original PR description
Backport various tests added in Odoo 19.4+ in order to better spot potential regressions. Add new tests for synchronization cron behavior, notably in case of failure. Forward-Port-Of: odoo/enterprise#126810 Forward-Port-Of: odoo/enterprise#126334
The AI-powered website sales module was updated to stay compatible with recent changes in the main Odoo platform. This helps keep the online sales experience reliable and ready for continued improvements.
Original PR description
Community PR: - https://github.com/odoo/odoo/pull/242995 task-5405584
Test helpers now freeze all Odoo time sources consistently, so automated tests no longer compare a frozen application time with a live database timestamp. This reduces flaky test results across affected apps without changing production behavior.
Original PR description
*= marketing_automations, whatsapp_events
**Odoo has two clocks**
When you write a test and “freeze time,” you usually mean everything should behave as if it’s that moment. In Odoo, that’s not one clock it’s two:
| Source | What it drives |
|:------------|-------------:|
| datetime.now() / fields.Datetime.now() | Domains, defaults, Python logic, “is this overdue?” checks |
| env.cr.now() / cursor.now() | ORM create_date, write_date, and other DB-timestamp fields |
Freezegun only freezes the first one.
So a test can end up in this broken state:
```py
with freeze_time("2024-01-01"):
record = self.env['res.partner'].create({'name': 'Test'})
# Datetime.now() says: 2024-01-01
# record.create_date says: 2025-06-05 (real DB time)
```
The test thinks it’s January 1st, but the record was stamped with today. Any logic comparing “now” vs create_date will be wrong or flaky.
com: https://github.com/odoo/odoo/pull/268541
task-6124007Spreadsheet backend URLs now include the document access token, so copied links work outside the current user session. This makes spreadsheet sharing consistent with regular Documents and avoids needing to open the sharing dialog just to get a usable link.
Original PR description
Regular documents can be shared by copying the URL from the backend. This was not true for spreadsheets: the backend URL only contained the spreadsheet id, so opening it outside the current user session did not carry the document access token. Users had to open the sharing dialog and copy the dedicated link instead. Use the document access token in backend spreadsheet URLs, as `/odoo/.../spreadsheet/<access_token>`, following the same format as other Documents share URLs. The token already embeds the document id, so the id does not need to appear in the path anymore. Spreadsheet actions can be reached from different apps, not only Documents (for example inserted lists/pivots or survey results), so the existing action path is preserved and only the spreadsheet segment is rewritten. Task: [6123064](https://www.odoo.com/odoo/project/2328/tasks/6123064)
Bank synchronization now recognizes a new type of temporary, non-blocking error from Odoofin. This helps avoid marking a bank connection as failed when the issue does not require stopping the synchronization flow.
Original PR description
Odoofin now sends a 'non_blocking_error' error response to indicate that the state on account.online.link shouldn't be set to error. In this commit, we start using it. Task ID: 6358809 Forward-Port-Of: odoo/enterprise#126737 Forward-Port-Of: odoo/enterprise#123287
In the payroll run wizard, clicking an employee row now opens that employee's record instead of changing the selection. This makes it faster and clearer for payroll users to review employee details during pay run preparation.
Original PR description
Currently, clicking a row on the first screen of the payrun wizard toggles the employee selection instead of opening the employee record. This change ensures that clicking a row directly opens the employee record. task-6372551
3 changes
Enhancements to existing features
The cash flow report now handles large payment datasets in a way that lets the database choose more reliable execution plans. This helps prevent the report from hanging or slowing dramatically after database maintenance or restores, while keeping normal performance about the same.
Original PR description
The payment_move_ids CTE of the cash flow statement collapsed all the liquidity move ids into a single array with array_agg(DISTINCT move_id), and the consumer queries filtered with move_id IN…
The payment_move_ids CTE of the cash flow statement collapsed all the
liquidity move ids into a single array with array_agg(DISTINCT move_id),
and the consumer queries filtered with
move_id IN (SELECT unnest(payment_move_ids.move_id)).
PostgreSQL cannot estimate the cardinality of unnest() over a
non-constant array: the ProjectSet node is always planned with a fixed
guess (rows=10) regardless of how many moves the period contains. On the
affected database the CTE returns 30,307 moves for a single month, a
3,000x planner misestimation that is visible in the EXPLAIN below even
when the query happens to be fast. Whenever the surrounding statistics
degrade (e.g. right after a pg_restore, before any ANALYZE runs), that
guess collapses every downstream join into nested loops over
account_move_line (39M rows) and account_partial_reconcile (9.2M rows)
and the report never finishes (>5 minutes, killed). The unnest() call
was also repeated 7 times (3 in _get_liquidity_moves, 4 in
_get_reconciled_moves).
Make the CTE return a plain row set (SELECT DISTINCT move_id) and filter
with regular IN/NOT IN subqueries so the planner works with real row
estimates and can choose hash semi/anti joins or index nested loops
based on actual costs.
With healthy statistics both forms now perform the same (~0.6s for the
liquidity moves query below); the difference is that the new form
degrades gracefully when estimates drift instead of falling off a
cliff. Full report render on the 39M-line database: 6.4s.
Related operational findings on the affected database (not part of this
patch, applied at the DB level):
- The database had been restored without ANALYZE: pg_stats had 0 rows
for account_move_line, which is what made the report hang for >5
minutes regardless of this patch. Fixed with:
vacuumdb --analyze -t account_move_line -t account_move
-t account_partial_reconcile ... (6.5s)
- move_id n_distinct was estimated at 169,957 vs 9,844,904 real (58x
off), pushing the planner away from the efficient move_id index
probes. Fixed with:
ALTER TABLE account_move_line
ALTER COLUMN move_id SET (n_distinct = -0.25);
ANALYZE account_move_line;
- account_partial_reconcile has no index on max_date; the intermediate
plans seq-scanned 9.2M rows per UNION branch. Added:
CREATE INDEX account_partial_reconcile__max_date_index
ON account_partial_reconcile (max_date);
<details>
<summary>Problematic query BEFORE the change</summary>
```sql
(WITH payment_move_ids AS (
SELECT
array_agg(DISTINCT account_move_line.move_id) AS move_id
FROM "account_move_line"
WHERE ("account_move_line"."account_id" IN (1274, 15, 1941, 1942, 672, 673, 674, 675, 676, 37, 38, 677, 678, 679, 680, 681, 1079, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 1346, 1353, 1236, 1111, 1252, 1254, 1385, 1898, 1899, 1900, 1264, 1908, 1909, 1018, 1275) AND "account_move_line"."company_id" IN (1) AND "account_move_line"."date" <= '2026-07-31'::date AND "account_move_line"."date" >= '2026-07-01'::date AND "account_move_line"."display_type" NOT IN ('line_section', 'line_subsection', 'line_note') AND "account_move_line"."journal_id" IN (244, 245, 247, 249, 251, 252, 255, 377, 637, 256, 258, 259, 261, 651, 433, 511, 30, 8, 349, 11, 10, 370, 43, 679, 12, 13, 15, 14, 434, 598, 599, 648, 263, 248, 228, 229, 231, 234, 236, 238, 239, 241, 243, 22, 24, 25, 7, 405, 374, 28, 20, 23, 375, 27, 371, 19, 26, 425, 559, 373, 21, 607, 372, 542, 680, 5, 42, 36, 32, 522, 530, 529, 527, 516, 520, 521, 526, 519, 528, 531, 517, 29, 214, 215, 216, 217, 409, 218, 220, 191, 336, 6, 681, 38, 4, 45, 262, 264, 266, 268, 233, 237, 240, 246, 254, 260, 265, 270, 272, 273, 235, 242, 257, 267, 271, 274, 277, 278, 232, 253, 269, 276, 279, 281, 250, 275, 337, 338, 407, 280, 282, 230, 283, 284, 285, 219, 222, 224, 17, 16, 435, 286, 289, 290, 291, 297, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 426, 611, 31, 35, 3, 33, 41, 37, 312, 313, 315, 316, 317, 318, 320, 311, 321, 323, 324, 326, 327, 328, 305, 306, 606, 376, 40, 34, 602, 294, 302, 287, 295, 296, 298, 303, 288, 299, 304, 225, 221, 226, 227, 307, 308, 309, 310, 314, 319, 322, 325, 329, 223, 403, 404, 676, 39, 18, 9, 292, 300, 293, 301, 406, 330, 331, 332, 333, 334, 195, 196, 197, 335, 198, 203, 208, 192, 204, 193, 205, 209, 199, 194, 200, 210, 211, 212, 206, 213, 201, 202, 207, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 390, 392, 393, 395, 397, 399, 401, 402, 400, 378, 389, 391, 394, 396, 398, 682, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369) AND "account_move_line"."parent_state" IN ('posted'))
)
-- Credit amount of each account
SELECT
'((''forced_options'', ((''date'', ((''currency_table_period_key'', ''2026-07-01_2026-07-31''), (''date_from'', ''2026-07-01''), (''date_to'', ''2026-07-31''), (''filter'', ''custom''), (''mode'', ''range''), (''period_type'', ''month''), (''string'', ''Jul 2026''))),)), (''horizontal_groupby_element'', ()))' AS column_group_key,
account_move_line.account_id,
(COALESCE("account_move_line__account_id"."code_store"->'1',to_jsonb(NULL::VARCHAR))->>0)::VARCHAR AS account_code,
"account_move_line__account_id"."name"->>'en_US' AS account_name,
"account_move_line__account_id"."account_type" AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
SUM((account_partial_reconcile.amount) * COALESCE(account_currency_table.rate, 1)) AS balance
FROM "account_move_line" JOIN "account_account" AS "account_move_line__account_id" ON ("account_move_line"."account_id" = "account_move_line__account_id"."id")
JOIN (VALUES (1, CAST(NULL AS VARCHAR), CAST(NULL AS DATE), CAST(NULL AS DATE), 'current', 1)) AS account_currency_table(company_id, period_key, date_from, date_next, rate_type, rate)
ON account_move_line.company_id = account_currency_table.company_id
AND (account_currency_table.period_key = '2026-07-01_2026-07-31' OR account_currency_table.period_key IS NULL)
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.credit_move_id = account_move_line.id
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN (1, 3, 2)
WHERE account_move_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_move_line.account_id NOT IN (1274, 15, 1941, 1942, 672, 673, 674, 675, 676, 37, 38, 677, 678, 679, 680, 681, 1079, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 1346, 1353, 1236, 1111, 1252, 1254, 1385, 1898, 1899, 1900, 1264, 1908, 1909, 1018, 1275)
AND account_partial_reconcile.max_date BETWEEN '2026-07-01' AND '2026-07-31'
GROUP BY account_move_line.company_id, account_move_line.account_id, (COALESCE("account_move_line__account_id"."code_store"->'1',to_jsonb(NULL::VARCHAR))->>0)::VARCHAR, "account_move_line__account_id"."name"->>'en_US', account_account_type, account_account_account_tag.account_account_tag_id
UNION ALL
-- Debit amount of each account
SELECT
'((''forced_options'', ((''date'', ((''currency_table_period_key'', ''2026-07-01_2026-07-31''), (''date_from'', ''2026-07-01''), (''date_to'', ''2026-07-31''), (''filter'', ''custom''), (''mode'', ''range''), (''period_type'', ''month''), (''string'', ''Jul 2026''))),)), (''horizontal_groupby_element'', ()))' AS column_group_key,
account_move_line.account_id,
(COALESCE("account_move_line__account_id"."code_store"->'1',to_jsonb(NULL::VARCHAR))->>0)::VARCHAR AS account_code,
"account_move_line__account_id"."name"->>'en_US' AS account_name,
"account_move_line__account_id"."account_type" AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
-SUM((account_partial_reconcile.amount) * COALESCE(account_currency_table.rate, 1)) AS balance
FROM "account_move_line" JOIN "account_account" AS "account_move_line__account_id" ON ("account_move_line"."account_id" = "account_move_line__account_id"."id")
JOIN (VALUES (1, CAST(NULL AS VARCHAR), CAST(NULL AS DATE), CAST(NULL AS DATE), 'current', 1)) AS account_currency_table(company_id, period_key, date_from, date_next, rate_type, rate)
ON account_move_line.company_id = account_currency_table.company_id
AND (account_currency_table.period_key = '2026-07-01_2026-07-31' OR account_currency_table.period_key IS NULL)
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.debit_move_id = account_move_line.id
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN (1, 3, 2)
WHERE account_move_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_move_line.account_id NOT IN (1274, 15, 1941, 1942, 672, 673, 674, 675, 676, 37, 38, 677, 678, 679, 680, 681, 1079, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 1346, 1353, 1236, 1111, 1252, 1254, 1385, 1898, 1899, 1900, 1264, 1908, 1909, 1018, 1275)
AND account_partial_reconcile.max_date BETWEEN '2026-07-01' AND '2026-07-31'
GROUP BY account_move_line.company_id, account_move_line.account_id, (COALESCE("account_move_line__account_id"."code_store"->'1',to_jsonb(NULL::VARCHAR))->>0)::VARCHAR, "account_move_line__account_id"."name"->>'en_US', account_account_type, account_account_account_tag.account_account_tag_id
UNION ALL
-- Total amount of each account
SELECT
'((''forced_options'', ((''date'', ((''currency_table_period_key'', ''2026-07-01_2026-07-31''), (''date_from'', ''2026-07-01''), (''date_to'', ''2026-07-31''), (''filter'', ''custom''), (''mode'', ''range''), (''period_type'', ''month''), (''string'', ''Jul 2026''))),)), (''horizontal_groupby_element'', ()))' AS column_group_key,
account_move_line.account_id AS account_id,
(COALESCE("account_move_line__account_id"."code_store"->'1',to_jsonb(NULL::VARCHAR))->>0)::VARCHAR AS account_code,
"account_move_line__account_id"."name"->>'en_US' AS account_name,
"account_move_line__account_id"."account_type" AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
SUM((account_move_line.balance) * COALESCE(account_currency_table.rate, 1)) AS balance
FROM "account_move_line" JOIN "account_account" AS "account_move_line__account_id" ON ("account_move_line"."account_id" = "account_move_line__account_id"."id")
JOIN (VALUES (1, CAST(NULL AS VARCHAR), CAST(NULL AS DATE), CAST(NULL AS DATE), 'current', 1)) AS account_currency_table(company_id, period_key, date_from, date_next, rate_type, rate)
ON account_move_line.company_id = account_currency_table.company_id
AND (account_currency_table.period_key = '2026-07-01_2026-07-31' OR account_currency_table.period_key IS NULL)
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN (1, 3, 2)
WHERE account_move_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_move_line.account_id NOT IN (1274, 15, 1941, 1942, 672, 673, 674, 675, 676, 37, 38, 677, 678, 679, 680, 681, 1079, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 1346, 1353, 1236, 1111, 1252, 1254, 1385, 1898, 1899, 1900, 1264, 1908, 1909, 1018, 1275)
GROUP BY account_move_line.account_id, (COALESCE("account_move_line__account_id"."code_store"->'1',to_jsonb(NULL::VARCHAR))->>0)::VARCHAR, "account_move_line__account_id"."name"->>'en_US', account_account_type, account_account_account_tag.account_account_tag_id)
```
</details>
<details>
<summary>EXPLAIN (ANALYZE, BUFFERS) BEFORE the change</summary>
note the ProjectSet rows=10 estimate vs 30,307 actual rows coming out of unnest
```txt
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Append (cost=26694.10..26849.66 rows=40 width=148) (actual time=307.108..638.422 rows=62 loops=1)
Buffers: shared hit=789523
CTE payment_move_ids
-> Aggregate (cost=26613.84..26613.85 rows=1 width=32) (actual time=96.170..96.172 rows=1 loops=1)
Buffers: shared hit=18968
-> Sort (cost=26552.25..26583.04 rows=12317 width=4) (actual time=92.110..93.591 rows=37545 loops=1)
Sort Key: account_move_line_3.move_id
Sort Method: quicksort Memory: 1537kB
Buffers: shared hit=18968
-> Index Scan using account_move_line_account_id_date_idx on account_move_line account_move_line_3 (cost=1.23..25715.41 rows=12317 width=4) (actual time=0.101..86.829 rows=37545 loops=1)
Index Cond: ((account_id = ANY ('{1274,15,1941,1942,672,673,674,675,676,37,38,677,678,679,680,681,1079,63,64,65,66,67,68,69,70,71,72,73,1346,1353,1236,1111,1252,1254,1385,1898,1899,1900,1264,1908,1909,1018,1275}'::integer[])) AND (date <= '2026-07-31'::date) AND (date >= '2026-07-01'::date))
Filter: ((company_id = 1) AND ((parent_state)::text = 'posted'::text) AND ((display_type)::text <> ALL ('{line_section,line_subsection,line_note}'::text[])) AND (journal_id = ANY ('{244,245,247,249,251,252,255,377,637,256,258,259,261,651,433,511,30,8,349,11,10,370,43,679,12,13,15,14,434,598,599,648,263,248,228,229,231,234,236,238,239,241,243,22,24,25,7,405,374,28,20,23,375,27,371,19,26,425,559,373,21,607,372,542,680,5,42,36,32,522,530,529,527,516,520,521,526,519,528,531,517,29,214,215,216,217,409,218,220,191,336,6,681,38,4,45,262,264,266,268,233,237,240,246,254,260,265,270,272,273,235,242,257,267,271,274,277,278,232,253,269,276,279,281,250,275,337,338,407,280,282,230,283,284,285,219,222,224,17,16,435,286,289,290,291,297,339,340,341,342,343,344,345,346,347,348,426,611,31,35,3,33,41,37,312,313,315,316,317,318,320,311,321,323,324,326,327,328,305,306,606,376,40,34,602,294,302,287,295,296,298,303,288,299,304,225,221,226,227,307,308,309,310,314,319,322,325,329,223,403,404,676,39,18,9,292,300,293,301,406,330,331,332,333,334,195,196,197,335,198,203,208,192,204,193,205,209,199,194,200,210,211,212,206,213,201,202,207,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,390,392,393,395,397,399,401,402,400,378,389,391,394,396,398,682,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369}'::integer[])))
Rows Removed by Filter: 117
Buffers: shared hit=18968
-> Subquery Scan on "*SELECT* 1_1" (cost=80.25..80.30 rows=1 width=148) (actual time=307.108..309.550 rows=2 loops=1)
Buffers: shared hit=320873
-> GroupAggregate (cost=80.25..80.29 rows=1 width=152) (actual time=307.106..309.547 rows=2 loops=1)
Group Key: account_move_line.account_id, (((COALESCE((account_move_line__account_id.code_store -> '1'::text)) ->> 0))::character varying), ((account_move_line__account_id.name ->> 'en_US'::text)), account_move_line__account_id.account_type, account_account_account_tag.account_account_tag_id
Buffers: shared hit=320873
-> Sort (cost=80.25..80.26 rows=1 width=95) (actual time=305.265..305.862 rows=14982 loops=1)
Sort Key: account_move_line.account_id, (((COALESCE((account_move_line__account_id.code_store -> '1'::text)) ->> 0))::character varying), ((account_move_line__account_id.name ->> 'en_US'::text)), account_move_line__account_id.account_type, account_account_account_tag.account_account_tag_id
Sort Method: quicksort Memory: 1488kB
Buffers: shared hit=320873
-> Nested Loop Left Join (cost=1.63..80.24 rows=1 width=95) (actual time=105.455..300.078 rows=14982 loops=1)
Buffers: shared hit=320867
-> Nested Loop (cost=1.36..79.64 rows=1 width=100) (actual time=105.434..279.121 rows=14982 loops=1)
Buffers: shared hit=290903
-> Nested Loop (cost=1.08..79.35 rows=1 width=15) (actual time=105.423..265.444 rows=14982 loops=1)
Buffers: shared hit=245957
-> Nested Loop (cost=0.65..58.84 rows=38 width=12) (actual time=105.398..220.996 rows=23165 loops=1)
Buffers: shared hit=162104
-> HashAggregate (cost=0.10..0.20 rows=10 width=4) (actual time=105.370..111.578 rows=30307 loops=1)
Group Key: unnest(payment_move_ids.move_id)
Batches: 1 Memory Usage: 3625kB
Buffers: shared hit=18968
-> ProjectSet (cost=0.00..0.08 rows=10 width=4) (actual time=96.186..97.942 rows=30307 loops=1)
Buffers: shared hit=18968
-> CTE Scan on payment_move_ids (cost=0.00..0.02 rows=1 width=32) (actual time=96.183..96.184 rows=1 loops=1)
Buffers: shared hit=18968
-> Index Scan using account_move_line__move_id_index on account_move_line (cost=0.55..5.82 rows=4 width=16) (actual time=0.003..0.003 rows=1 loops=30307)
Index Cond: (move_id = (unnest(payment_move_ids.move_id)))
Filter: ((company_id = 1) AND (account_id <> ALL ('{1274,15,1941,1942,672,673,674,675,676,37,38,677,678,679,680,681,1079,63,64,65,66,67,68,69,70,71,72,73,1346,1353,1236,1111,1252,1254,1385,1898,1899,1900,1264,1908,1909,1018,1275}'::integer[])))
Rows Removed by Filter: 1
Buffers: shared hit=143136
-> Index Scan using account_partial_reconcile__credit_move_id_index on account_partial_reconcile (cost=0.43..0.53 rows=1 width=11) (actual time=0.002..0.002 rows=1 loops=23165)
Index Cond: (credit_move_id = account_move_line.id)
Filter: ((max_date >= '2026-07-01'::date) AND (max_date <= '2026-07-31'::date))
Buffers: shared hit=83853
-> Index Scan using account_account_pkey on account_account account_move_line__account_id (cost=0.28..0.29 rows=1 width=89) (actual time=0.001..0.001 rows=1 loops=14982)
Index Cond: (id = account_move_line.account_id)
Buffers: shared hit=44946
-> Index Only Scan using account_account_account_tag_sh_auto_pk on account_account_account_tag (cost=0.28..0.58 rows=1 width=8) (actual time=0.001..0.001 rows=0 loops=14982)
Index Cond: ((account_account_id = account_move_line.account_id) AND (account_account_tag_id = ANY ('{1,3,2}'::integer[])))
Heap Fetches: 0
Buffers: shared hit=29964
-> Subquery Scan on "*SELECT* 2" (cost=80.25..80.31 rows=1 width=148) (actual time=172.103..172.363 rows=10 loops=1)
Buffers: shared hit=256017
-> GroupAggregate (cost=80.25..80.30 rows=1 width=152) (actual time=172.102..172.359 rows=10 loops=1)
Group Key: account_move_line_1.account_id, (((COALESCE((account_move_line__account_id_1.code_store -> '1'::text)) ->> 0))::character varying), ((account_move_line__account_id_1.name ->> 'en_US'::text)), account_move_line__account_id_1.account_type, account_account_account_tag_1.account_account_tag_id
Buffers: shared hit=256017
-> Sort (cost=80.25..80.26 rows=1 width=95) (actual time=170.281..170.572 rows=7235 loops=1)
Sort Key: account_move_line_1.account_id, (((COALESCE((account_move_line__account_id_1.code_store -> '1'::text)) ->> 0))::character varying), ((account_move_line__account_id_1.name ->> 'en_US'::text)), account_move_line__account_id_1.account_type, account_account_account_tag_1.account_account_tag_id
Sort Method: quicksort Memory: 764kB
Buffers: shared hit=256017
-> Nested Loop Left Join (cost=1.63..80.24 rows=1 width=95) (actual time=8.816..167.962 rows=7235 loops=1)
Buffers: shared hit=256017
-> Nested Loop (cost=1.36..79.64 rows=1 width=100) (actual time=8.805..157.802 rows=7235 loops=1)
Buffers: shared hit=241547
-> Nested Loop (cost=1.08..79.35 rows=1 width=15) (actual time=8.801..151.024 rows=7235 loops=1)
Buffers: shared hit=219842
-> Nested Loop (cost=0.65..58.84 rows=38 width=12) (actual time=8.791..112.486 rows=23165 loops=1)
Buffers: shared hit=143136
-> HashAggregate (cost=0.10..0.20 rows=10 width=4) (actual time=8.774..14.897 rows=30307 loops=1)
Group Key: unnest(payment_move_ids_1.move_id)
Batches: 1 Memory Usage: 3625kB
-> ProjectSet (cost=0.00..0.08 rows=10 width=4) (actual time=0.011..1.752 rows=30307 loops=1)
-> CTE Scan on payment_move_ids payment_move_ids_1 (cost=0.00..0.02 rows=1 width=32) (actual time=0.008..0.008 rows=1 loops=1)
-> Index Scan using account_move_line__move_id_index on account_move_line account_move_line_1 (cost=0.55..5.82 rows=4 width=16) (actual time=0.003..0.003 rows=1 loops=30307)
Index Cond: (move_id = (unnest(payment_move_ids_1.move_id)))
Filter: ((company_id = 1) AND (account_id <> ALL ('{1274,15,1941,1942,672,673,674,675,676,37,38,677,678,679,680,681,1079,63,64,65,66,67,68,69,70,71,72,73,1346,1353,1236,1111,1252,1254,1385,1898,1899,1900,1264,1908,1909,1018,1275}'::integer[])))
Rows Removed by Filter: 1
Buffers: shared hit=143136
-> Index Scan using account_partial_reconcile__debit_move_id_index on account_partial_reconcile account_partial_reconcile_1 (cost=0.43..0.53 rows=1 width=11) (actual time=0.001..0.001 rows=0 loops=23165)
Index Cond: (debit_move_id = account_move_line_1.id)
Filter: ((max_date >= '2026-07-01'::date) AND (max_date <= '2026-07-31'::date))
Buffers: shared hit=76706
-> Index Scan using account_account_pkey on account_account account_move_line__account_id_1 (cost=0.28..0.29 rows=1 width=89) (actual time=0.001..0.001 rows=1 loops=7235)
Index Cond: (id = account_move_line_1.account_id)
Buffers: shared hit=21705
-> Index Only Scan using account_account_account_tag_sh_auto_pk on account_account_account_tag account_account_account_tag_1 (cost=0.28..0.58 rows=1 width=8) (actual time=0.001..0.001 rows=0 loops=7235)
Index Cond: ((account_account_id = account_move_line_1.account_id) AND (account_account_tag_id = ANY ('{1,3,2}'::integer[])))
Heap Fetches: 0
Buffers: shared hit=14470
-> HashAggregate (cost=74.25..75.01 rows=38 width=148) (actual time=156.479..156.498 rows=50 loops=1)
Group Key: account_move_line_2.account_id, ((COALESCE((account_move_line__account_id_2.code_store -> '1'::text)) ->> 0))::character varying, (account_move_line__account_id_2.name ->> 'en_US'::text), account_move_line__account_id_2.account_type, account_account_account_tag_2.account_account_tag_id
Batches: 1 Memory Usage: 48kB
Buffers: shared hit=212633
-> Nested Loop Left Join (cost=1.20..73.58 rows=38 width=90) (actual time=9.037..145.306 rows=23165 loops=1)
Join Filter: (account_account_account_tag_2.account_account_id = account_move_line_2.account_id)
Buffers: shared hit=212633
-> Nested Loop (cost=0.93..69.95 rows=38 width=95) (actual time=9.027..135.207 rows=23165 loops=1)
Buffers: shared hit=212631
-> Nested Loop (cost=0.65..58.84 rows=38 width=10) (actual time=9.022..113.369 rows=23165 loops=1)
Buffers: shared hit=143136
-> HashAggregate (cost=0.10..0.20 rows=10 width=4) (actual time=9.005..14.381 rows=30307 loops=1)
Group Key: unnest(payment_move_ids_2.move_id)
Batches: 1 Memory Usage: 3625kB
-> ProjectSet (cost=0.00..0.08 rows=10 width=4) (actual time=0.080..1.829 rows=30307 loops=1)
-> CTE Scan on payment_move_ids payment_move_ids_2 (cost=0.00..0.02 rows=1 width=32) (actual time=0.077..0.077 rows=1 loops=1)
-> Index Scan using account_move_line__move_id_index on account_move_line account_move_line_2 (cost=0.55..5.82 rows=4 width=14) (actual time=0.003..0.003 rows=1 loops=30307)
Index Cond: (move_id = (unnest(payment_move_ids_2.move_id)))
Filter: ((company_id = 1) AND (account_id <> ALL ('{1274,15,1941,1942,672,673,674,675,676,37,38,677,678,679,680,681,1079,63,64,65,66,67,68,69,70,71,72,73,1346,1353,1236,1111,1252,1254,1385,1898,1899,1900,1264,1908,1909,1018,1275}'::integer[])))
Rows Removed by Filter: 1
Buffers: shared hit=143136
-> Index Scan using account_account_pkey on account_account account_move_line__account_id_2 (cost=0.28..0.29 rows=1 width=89) (actual time=0.001..0.001 rows=1 loops=23165)
Index Cond: (id = account_move_line_2.account_id)
Buffers: shared hit=69495
-> Materialize (cost=0.28..2.78 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=23165)
Buffers: shared hit=2
-> Index Only Scan using account_account_account_tag_account_account_tag_id_account__idx on account_account_account_tag account_account_account_tag_2 (cost=0.28..2.77 rows=1 width=8) (actual time=0.007..0.007 rows=0 loops=1)
Index Cond: (account_account_tag_id = ANY ('{1,3,2}'::integer[]))
Heap Fetches: 0
Buffers: shared hit=2
Planning:
Buffers: shared hit=1089
Planning Time: 11.934 ms
Execution Time: 639.826 ms
```
</details>
<details>
<summary>Query AFTER the change</summary>
```sql
(WITH payment_move_ids AS (
SELECT DISTINCT
account_move_line.move_id AS move_id
FROM "account_move_line"
WHERE ("account_move_line"."account_id" IN (1274, 15, 1941, 1942, 672, 673, 674, 675, 676, 37, 38, 677, 678, 679, 680, 681, 1079, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 1346, 1353, 1236, 1111, 1252, 1254, 1385, 1898, 1899, 1900, 1264, 1908, 1909, 1018, 1275) AND "account_move_line"."company_id" IN (1) AND "account_move_line"."date" <= '2026-07-31'::date AND "account_move_line"."date" >= '2026-07-01'::date AND "account_move_line"."display_type" NOT IN ('line_section', 'line_subsection', 'line_note') AND "account_move_line"."journal_id" IN (244, 245, 247, 249, 251, 252, 255, 377, 637, 256, 258, 259, 261, 651, 433, 511, 30, 8, 349, 11, 10, 370, 43, 679, 12, 13, 15, 14, 434, 598, 599, 648, 263, 248, 228, 229, 231, 234, 236, 238, 239, 241, 243, 22, 24, 25, 7, 405, 374, 28, 20, 23, 375, 27, 371, 19, 26, 425, 559, 373, 21, 607, 372, 542, 680, 5, 42, 36, 32, 522, 530, 529, 527, 516, 520, 521, 526, 519, 528, 531, 517, 29, 214, 215, 216, 217, 409, 218, 220, 191, 336, 6, 681, 38, 4, 45, 262, 264, 266, 268, 233, 237, 240, 246, 254, 260, 265, 270, 272, 273, 235, 242, 257, 267, 271, 274, 277, 278, 232, 253, 269, 276, 279, 281, 250, 275, 337, 338, 407, 280, 282, 230, 283, 284, 285, 219, 222, 224, 17, 16, 435, 286, 289, 290, 291, 297, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 426, 611, 31, 35, 3, 33, 41, 37, 312, 313, 315, 316, 317, 318, 320, 311, 321, 323, 324, 326, 327, 328, 305, 306, 606, 376, 40, 34, 602, 294, 302, 287, 295, 296, 298, 303, 288, 299, 304, 225, 221, 226, 227, 307, 308, 309, 310, 314, 319, 322, 325, 329, 223, 403, 404, 676, 39, 18, 9, 292, 300, 293, 301, 406, 330, 331, 332, 333, 334, 195, 196, 197, 335, 198, 203, 208, 192, 204, 193, 205, 209, 199, 194, 200, 210, 211, 212, 206, 213, 201, 202, 207, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 390, 392, 393, 395, 397, 399, 401, 402, 400, 378, 389, 391, 394, 396, 398, 682, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369) AND "account_move_line"."parent_state" IN ('posted'))
)
-- Credit amount of each account
SELECT
'((''forced_options'', ((''date'', ((''currency_table_period_key'', ''2026-07-01_2026-07-31''), (''date_from'', ''2026-07-01''), (''date_to'', ''2026-07-31''), (''filter'', ''custom''), (''mode'', ''range''), (''period_type'', ''month''), (''string'', ''Jul 2026''))),)), (''horizontal_groupby_element'', ()))' AS column_group_key,
account_move_line.account_id,
(COALESCE("account_move_line__account_id"."code_store"->'1',to_jsonb(NULL::VARCHAR))->>0)::VARCHAR AS account_code,
"account_move_line__account_id"."name"->>'en_US' AS account_name,
"account_move_line__account_id"."account_type" AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
SUM((account_partial_reconcile.amount) * COALESCE(account_currency_table.rate, 1)) AS balance
FROM "account_move_line" JOIN "account_account" AS "account_move_line__account_id" ON ("account_move_line"."account_id" = "account_move_line__account_id"."id")
JOIN (VALUES (1, CAST(NULL AS VARCHAR), CAST(NULL AS DATE), CAST(NULL AS DATE), 'current', 1)) AS account_currency_table(company_id, period_key, date_from, date_next, rate_type, rate)
ON account_move_line.company_id = account_currency_table.company_id
AND (account_currency_table.period_key = '2026-07-01_2026-07-31' OR account_currency_table.period_key IS NULL)
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.credit_move_id = account_move_line.id
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN (1, 3, 2)
WHERE account_move_line.move_id IN (SELECT payment_move_ids.move_id FROM payment_move_ids)
AND account_move_line.account_id NOT IN (1274, 15, 1941, 1942, 672, 673, 674, 675, 676, 37, 38, 677, 678, 679, 680, 681, 1079, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 1346, 1353, 1236, 1111, 1252, 1254, 1385, 1898, 1899, 1900, 1264, 1908, 1909, 1018, 1275)
AND account_partial_reconcile.max_date BETWEEN '2026-07-01' AND '2026-07-31'
GROUP BY account_move_line.company_id, account_move_line.account_id, (COALESCE("account_move_line__account_id"."code_store"->'1',to_jsonb(NULL::VARCHAR))->>0)::VARCHAR, "account_move_line__account_id"."name"->>'en_US', account_account_type, account_account_account_tag.account_account_tag_id
UNION ALL
-- Debit amount of each account
SELECT
'((''forced_options'', ((''date'', ((''currency_table_period_key'', ''2026-07-01_2026-07-31''), (''date_from'', ''2026-07-01''), (''date_to'', ''2026-07-31''), (''filter'', ''custom''), (''mode'', ''range''), (''period_type'', ''month''), (''string'', ''Jul 2026''))),)), (''horizontal_groupby_element'', ()))' AS column_group_key,
account_move_line.account_id,
(COALESCE("account_move_line__account_id"."code_store"->'1',to_jsonb(NULL::VARCHAR))->>0)::VARCHAR AS account_code,
"account_move_line__account_id"."name"->>'en_US' AS account_name,
"account_move_line__account_id"."account_type" AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
-SUM((account_partial_reconcile.amount) * COALESCE(account_currency_table.rate, 1)) AS balance
FROM "account_move_line" JOIN "account_account" AS "account_move_line__account_id" ON ("account_move_line"."account_id" = "account_move_line__account_id"."id")
JOIN (VALUES (1, CAST(NULL AS VARCHAR), CAST(NULL AS DATE), CAST(NULL AS DATE), 'current', 1)) AS account_currency_table(company_id, period_key, date_from, date_next, rate_type, rate)
ON account_move_line.company_id = account_currency_table.company_id
AND (account_currency_table.period_key = '2026-07-01_2026-07-31' OR account_currency_table.period_key IS NULL)
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.debit_move_id = account_move_line.id
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN (1, 3, 2)
WHERE account_move_line.move_id IN (SELECT payment_move_ids.move_id FROM payment_move_ids)
AND account_move_line.account_id NOT IN (1274, 15, 1941, 1942, 672, 673, 674, 675, 676, 37, 38, 677, 678, 679, 680, 681, 1079, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 1346, 1353, 1236, 1111, 1252, 1254, 1385, 1898, 1899, 1900, 1264, 1908, 1909, 1018, 1275)
AND account_partial_reconcile.max_date BETWEEN '2026-07-01' AND '2026-07-31'
GROUP BY account_move_line.company_id, account_move_line.account_id, (COALESCE("account_move_line__account_id"."code_store"->'1',to_jsonb(NULL::VARCHAR))->>0)::VARCHAR, "account_move_line__account_id"."name"->>'en_US', account_account_type, account_account_account_tag.account_account_tag_id
UNION ALL
-- Total amount of each account
SELECT
'((''forced_options'', ((''date'', ((''currency_table_period_key'', ''2026-07-01_2026-07-31''), (''date_from'', ''2026-07-01''), (''date_to'', ''2026-07-31''), (''filter'', ''custom''), (''mode'', ''range''), (''period_type'', ''month''), (''string'', ''Jul 2026''))),)), (''horizontal_groupby_element'', ()))' AS column_group_key,
account_move_line.account_id AS account_id,
(COALESCE("account_move_line__account_id"."code_store"->'1',to_jsonb(NULL::VARCHAR))->>0)::VARCHAR AS account_code,
"account_move_line__account_id"."name"->>'en_US' AS account_name,
"account_move_line__account_id"."account_type" AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
SUM((account_move_line.balance) * COALESCE(account_currency_table.rate, 1)) AS balance
FROM "account_move_line" JOIN "account_account" AS "account_move_line__account_id" ON ("account_move_line"."account_id" = "account_move_line__account_id"."id")
JOIN (VALUES (1, CAST(NULL AS VARCHAR), CAST(NULL AS DATE), CAST(NULL AS DATE), 'current', 1)) AS account_currency_table(company_id, period_key, date_from, date_next, rate_type, rate)
ON account_move_line.company_id = account_currency_table.company_id
AND (account_currency_table.period_key = '2026-07-01_2026-07-31' OR account_currency_table.period_key IS NULL)
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN (1, 3, 2)
WHERE account_move_line.move_id IN (SELECT payment_move_ids.move_id FROM payment_move_ids)
AND account_move_line.account_id NOT IN (1274, 15, 1941, 1942, 672, 673, 674, 675, 676, 37, 38, 677, 678, 679, 680, 681, 1079, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 1346, 1353, 1236, 1111, 1252, 1254, 1385, 1898, 1899, 1900, 1264, 1908, 1909, 1018, 1275)
GROUP BY account_move_line.account_id, (COALESCE("account_move_line__account_id"."code_store"->'1',to_jsonb(NULL::VARCHAR))->>0)::VARCHAR, "account_move_line__account_id"."name"->>'en_US', account_account_type, account_account_account_tag.account_account_tag_id)
```
</details>
<details>
<summary>EXPLAIN (ANALYZE, BUFFERS) AFTER the change</summary>
the CTE scan is now estimated from real statistics
```txt
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Append (cost=122015.01..291526.05 rows=47439 width=148) (actual time=346.625..638.772 rows=62 loops=1)
Buffers: shared hit=675626
CTE payment_move_ids
-> HashAggregate (cost=25746.20..25869.31 rows=12311 width=4) (actual time=149.934..153.654 rows=30307 loops=1)
Group Key: account_move_line_3.move_id
Batches: 1 Memory Usage: 3601kB
Buffers: shared hit=18968
-> Index Scan using account_move_line_account_id_date_idx on account_move_line account_move_line_3 (cost=1.23..25715.41 rows=12317 width=4) (actual time=53.024..140.718 rows=37545 loops=1)
Index Cond: ((account_id = ANY ('{1274,15,1941,1942,672,673,674,675,676,37,38,677,678,679,680,681,1079,63,64,65,66,67,68,69,70,71,72,73,1346,1353,1236,1111,1252,1254,1385,1898,1899,1900,1264,1908,1909,1018,1275}'::integer[])) AND (date <= '2026-07-31'::date) AND (date >= '2026-07-01'::date))
Filter: ((company_id = 1) AND ((parent_state)::text = 'posted'::text) AND ((display_type)::text <> ALL ('{line_section,line_subsection,line_note}'::text[])) AND (journal_id = ANY ('{244,245,247,249,251,252,255,377,637,256,258,259,261,651,433,511,30,8,349,11,10,370,43,679,12,13,15,14,434,598,599,648,263,248,228,229,231,234,236,238,239,241,243,22,24,25,7,405,374,28,20,23,375,27,371,19,26,425,559,373,21,607,372,542,680,5,42,36,32,522,530,529,527,516,520,521,526,519,528,531,517,29,214,215,216,217,409,218,220,191,336,6,681,38,4,45,262,264,266,268,233,237,240,246,254,260,265,270,272,273,235,242,257,267,271,274,277,278,232,253,269,276,279,281,250,275,337,338,407,280,282,230,283,284,285,219,222,224,17,16,435,286,289,290,291,297,339,340,341,342,343,344,345,346,347,348,426,611,31,35,3,33,41,37,312,313,315,316,317,318,320,311,321,323,324,326,327,328,305,306,606,376,40,34,602,294,302,287,295,296,298,303,288,299,304,225,221,226,227,307,308,309,310,314,319,322,325,329,223,403,404,676,39,18,9,292,300,293,301,406,330,331,332,333,334,195,196,197,335,198,203,208,192,204,193,205,209,199,194,200,210,211,212,206,213,201,202,207,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,390,392,393,395,397,399,401,402,400,378,389,391,394,396,398,682,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369}'::integer[])))
Rows Removed by Filter: 117
Buffers: shared hit=18968
-> Subquery Scan on "*SELECT* 1_1" (cost=96145.70..96153.45 rows=155 width=148) (actual time=346.624..348.928 rows=2 loops=1)
Buffers: shared hit=290911
-> GroupAggregate (cost=96145.70..96151.90 rows=155 width=152) (actual time=346.621..348.924 rows=2 loops=1)
Group Key: account_move_line.account_id, (((COALESCE((account_move_line__account_id.code_store -> '1'::text)) ->> 0))::character varying), ((account_move_line__account_id.name ->> 'en_US'::text)), account_move_line__account_id.account_type, account_account_account_tag.account_account_tag_id
Buffers: shared hit=290911
-> Sort (cost=96145.70..96146.09 rows=155 width=95) (actual time=344.851..345.445 rows=14982 loops=1)
Sort Key: account_move_line.account_id, (((COALESCE((account_move_line__account_id.code_store -> '1'::text)) ->> 0))::character varying), ((account_move_line__account_id.name ->> 'en_US'::text)), account_move_line__account_id.account_type, account_account_account_tag.account_account_tag_id
Sort Method: quicksort Memory: 1488kB
Buffers: shared hit=290911
-> Hash Left Join (cost=281.04..96140.06 rows=155 width=95) (actual time=163.798..339.652 rows=14982 loops=1)
Hash Cond: (account_move_line.account_id = account_account_account_tag.account_account_id)
Buffers: shared hit=290905
-> Nested Loop (cost=278.25..96135.52 rows=155 width=100) (actual time=163.762..333.768 rows=14982 loops=1)
Buffers: shared hit=290903
-> Nested Loop (cost=277.98..96090.18 rows=155 width=15) (actual time=163.745..319.832 rows=14982 loops=1)
Buffers: shared hit=245957
-> Nested Loop (cost=277.55..70646.77 rows=47129 width=12) (actual time=163.712..275.793 rows=23165 loops=1)
Buffers: shared hit=162104
-> HashAggregate (cost=277.00..400.11 rows=12311 width=4) (actual time=163.675..168.382 rows=30307 loops=1)
Group Key: payment_move_ids.move_id
Batches: 1 Memory Usage: 3601kB
Buffers: shared hit=18968
-> CTE Scan on payment_move_ids (cost=0.00..246.22 rows=12311 width=4) (actual time=149.936..157.948 rows=30307 loops=1)
Buffers: shared hit=18968
-> Index Scan using account_move_line__move_id_index on account_move_line (cost=0.55..5.67 rows=4 width=16) (actual time=0.003..0.003 rows=1 loops=30307)
Index Cond: (move_id = payment_move_ids.move_id)
Filter: ((company_id = 1) AND (account_id <> ALL ('{1274,15,1941,1942,672,673,674,675,676,37,38,677,678,679,680,681,1079,63,64,65,66,67,68,69,70,71,72,73,1346,1353,1236,1111,1252,1254,1385,1898,1899,1900,1264,1908,1909,1018,1275}'::integer[])))
Rows Removed by Filter: 1
Buffers: shared hit=143136
-> Index Scan using account_partial_reconcile__credit_move_id_index on account_partial_reconcile (cost=0.43..0.53 rows=1 width=11) (actual time=0.002..0.002 rows=1 loops=23165)
Index Cond: (credit_move_id = account_move_line.id)
Filter: ((max_date >= '2026-07-01'::date) AND (max_date <= '2026-07-31'::date))
Buffers: shared hit=83853
-> Index Scan using account_account_pkey on account_account account_move_line__account_id (cost=0.28..0.29 rows=1 width=89) (actual time=0.001..0.001 rows=1 loops=14982)
Index Cond: (id = account_move_line.account_id)
Buffers: shared hit=44946
-> Hash (cost=2.77..2.77 rows=1 width=8) (actual time=0.011..0.012 rows=0 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 8kB
Buffers: shared hit=2
-> Index Only Scan using account_account_account_tag_account_account_tag_id_account__idx on account_account_account_tag (cost=0.28..2.77 rows=1 width=8) (actual time=0.011..0.011 rows=0 loops=1)
Index Cond: (account_account_tag_id = ANY ('{1,3,2}'::integer[]))
Heap Fetches: 0
Buffers: shared hit=2
-> Subquery Scan on "*SELECT* 2" (cost=96145.50..96153.64 rows=155 width=148) (actual time=159.664..159.903 rows=10 loops=1)
Buffers: shared hit=241549
-> GroupAggregate (cost=96145.50..96152.09 rows=155 width=152) (actual time=159.661..159.898 rows=10 loops=1)
Group Key: account_move_line_1.account_id, (((COALESCE((account_move_line__account_id_1.code_store -> '1'::text)) ->> 0))::character varying), ((account_move_line__account_id_1.name ->> 'en_US'::text)), account_move_line__account_id_1.account_type, account_account_account_tag_1.account_account_tag_id
Buffers: shared hit=241549
-> Sort (cost=96145.50..96145.89 rows=155 width=95) (actual time=157.870..158.167 rows=7235 loops=1)
Sort Key: account_move_line_1.account_id, (((COALESCE((account_move_line__account_id_1.code_store -> '1'::text)) ->> 0))::character varying), ((account_move_line__account_id_1.name ->> 'en_US'::text)), account_move_line__account_id_1.account_type, account_account_account_tag_1.account_account_tag_id
Sort Method: quicksort Memory: 764kB
Buffers: shared hit=241549
-> Hash Left Join (cost=281.04..96139.86 rows=155 width=95) (actual time=7.401..155.573 rows=7235 loops=1)
Hash Cond: (account_move_line_1.account_id = account_account_account_tag_1.account_account_id)
Buffers: shared hit=241549
-> Nested Loop (cost=278.25..96135.32 rows=155 width=100) (actual time=7.380..152.835 rows=7235 loops=1)
Buffers: shared hit=241547
-> Nested Loop (cost=277.98..96089.98 rows=155 width=15) (actual time=7.365..145.962 rows=7235 loops=1)
Buffers: shared hit=219842
-> Nested Loop (cost=277.55..70646.77 rows=47129 width=12) (actual time=7.349..107.795 rows=23165 loops=1)
Buffers: shared hit=143136
-> HashAggregate (cost=277.00..400.11 rows=12311 width=4) (actual time=7.329..11.783 rows=30307 loops=1)
Group Key: payment_move_ids_1.move_id
Batches: 1 Memory Usage: 3601kB
-> CTE Scan on payment_move_ids payment_move_ids_1 (cost=0.00..246.22 rows=12311 width=4) (actual time=0.001..1.685 rows=30307 loops=1)
-> Index Scan using account_move_line__move_id_index on account_move_line account_move_line_1 (cost=0.55..5.67 rows=4 width=16) (actual time=0.003..0.003 rows=1 loops=30307)
Index Cond: (move_id = payment_move_ids_1.move_id)
Filter: ((company_id = 1) AND (account_id <> ALL ('{1274,15,1941,1942,672,673,674,675,676,37,38,677,678,679,680,681,1079,63,64,65,66,67,68,69,70,71,72,73,1346,1353,1236,1111,1252,1254,1385,1898,1899,1900,1264,1908,1909,1018,1275}'::integer[])))
Rows Removed by Filter: 1
Buffers: shared hit=143136
-> Index Scan using account_partial_reconcile__debit_move_id_index on account_partial_reconcile account_partial_reconcile_1 (cost=0.43..0.53 rows=1 width=11) (actual time=0.001..0.001 rows=0 loops=23165)
Index Cond: (debit_move_id = account_move_line_1.id)
Filter: ((max_date >= '2026-07-01'::date) AND (max_date <= '2026-07-31'::date))
Buffers: shared hit=76706
-> Index Scan using account_account_pkey on account_account account_move_line__account_id_1 (cost=0.28..0.29 rows=1 width=89) (actual time=0.001..0.001 rows=1 loops=7235)
Index Cond: (id = account_move_line_1.account_id)
Buffers: shared hit=21705
-> Hash (cost=2.77..2.77 rows=1 width=8) (actual time=0.008..0.008 rows=0 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 8kB
Buffers: shared hit=2
-> Index Only Scan using account_account_account_tag_account_account_tag_id_account__idx on account_account_account_tag account_account_account_tag_1 (cost=0.28..2.77 rows=1 width=8) (actual time=0.007..0.008 rows=0 loops=1)
Index Cond: (account_account_tag_id = ANY ('{1,3,2}'::integer[]))
Heap Fetches: 0
Buffers: shared hit=2
-> HashAggregate (cost=72169.88..73112.46 rows=47129 width=148) (actual time=129.819..129.928 rows=50 loops=1)
Group Key: account_move_line_2.account_id, ((COALESCE((account_move_line__account_id_2.code_store -> '1'::text)) ->> 0))::character varying, (account_move_line__account_id_2.name ->> 'en_US'::text), account_move_line__account_id_2.account_type, account_account_account_tag_2.account_account_tag_id
Batches: 1 Memory Usage: 1585kB
Buffers: shared hit=143166
-> Hash Left Join (cost=320.08..71345.12 rows=47129 width=90) (actual time=7.557..119.457 rows=23165 loops=1)
Hash Cond: (account_move_line_2.account_id = account_account_account_tag_2.account_account_id)
Buffers: shared hit=143166
-> Hash Join (cost=317.29..70811.23 rows=47129 width=95) (actual time=7.539..111.472 rows=23165 loops=1)
Hash Cond: (account_move_line_2.account_id = account_move_line__account_id_2.id)
Buffers: shared hit=143164
-> Nested Loop (cost=277.55..70646.77 rows=47129 width=10) (actual time=7.314..106.772 rows=23165 loops=1)
Buffers: shared hit=143136
-> HashAggregate (cost=277.00..400.11 rows=12311 width=4) (actual time=7.291..11.487 rows=30307 loops=1)
Group Key: payment_move_ids_2.move_id
Batches: 1 Memory Usage: 3601kB
-> CTE Scan on payment_move_ids payment_move_ids_2 (cost=0.00..246.22 rows=12311 width=4) (actual time=0.001..1.610 rows=30307 loops=1)
-> Index Scan using account_move_line__move_id_index on account_move_line account_move_line_2 (cost=0.55..5.67 rows=4 width=14) (actual time=0.003..0.003 rows=1 loops=30307)
Index Cond: (move_id = payment_move_ids_2.move_id)
Filter: ((company_id = 1) AND (account_id <> ALL ('{1274,15,1941,1942,672,673,674,675,676,37,38,677,678,679,680,681,1079,63,64,65,66,67,68,69,70,71,72,73,1346,1353,1236,1111,1252,1254,1385,1898,1899,1900,1264,1908,1909,1018,1275}'::integer[])))
Rows Removed by Filter: 1
Buffers: shared hit=143136
-> Hash (cost=33.22..33.22 rows=522 width=89) (actual time=0.210..0.210 rows=522 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 72kB
Buffers: shared hit=28
-> Seq Scan on account_account account_move_line__account_id_2 (cost=0.00..33.22 rows=522 width=89) (actual time=0.024..0.130 rows=522 loops=1)
Buffers: shared hit=28
-> Hash (cost=2.77..2.77 rows=1 width=8) (actual time=0.008..0.008 rows=0 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 8kB
Buffers: shared hit=2
-> Index Only Scan using account_account_account_tag_account_account_tag_id_account__idx on account_account_account_tag account_account_account_tag_2 (cost=0.28..2.77 rows=1 width=8) (actual time=0.008..0.008 rows=0 loops=1)
Index Cond: (account_account_tag_id = ANY ('{1,3,2}'::integer[]))
Heap Fetches: 0
Buffers: shared hit=2
Planning:
Buffers: shared hit=1083
Planning Time: 12.195 ms
JIT:
Functions: 114
Options: Inlining false, Optimization false, Expressions true, Deforming true
Timing: Generation 6.501 ms (Deform 3.916 ms), Inlining 0.000 ms, Optimization 3.079 ms, Emission 50.027 ms, Total 59.607 ms
Execution Time: 668.775 ms
(137 rows)
```
</details>
UPDATED: In staging instance I have reproduced using the following code
<details>
<summary>code for odoo-bin shell</summary>
```python
report = env.ref("account_reports.cash_flow_report")
options = report.get_options({})
options["date"].update({"date_from": "2026-07-01", "date_to": "2026-07-31", "mode": "range", "filter": "custom"})
options = report.get_options(options)
handler = env[report.custom_handler_model_name] # -> account.cash.flow.report.handler
payment_account_ids = handler._get_account_ids(report, options)
print("PAYMENT_ACCOUNT_IDS=", sorted(payment_account_ids))
import logging
logging.getLogger("odoo.sql_db").setLevel(logging.DEBUG)
logging.basicConfig()
report._get_lines(options)
```
</details>
UPDATED2: The real solution was `vacuumdb --analyze`Odoo now checks whether an online or batch payment exceeds the maximum amount allowed by the connected financial institution before trying to start the payment. This helps prevent failed payment attempts and gives users earlier feedback when a bank-imposed limit applies.
Original PR description
Before trying to initiate payments through Odoo/Odoofin, we should check that the total amount for the (batch) payment does not exceed the maximum payment amount allowed by the institution (some Powens institutions introduced that limit). task-6310729 Forward-Port-Of: odoo/enterprise#126699 Forward-Port-Of: odoo/enterprise#121513
Bank account synchronization now recognizes a new type of provider error that should not stop the connection. This helps keep online banking links active when the issue is temporary or non-critical, reducing unnecessary disruption for users.
Original PR description
Odoofin now sends a 'non_blocking_error' error response to indicate that the state on account.online.link shouldn't be set to error. In this commit, we start using it. Task ID: 6358809 Forward-Port-Of: odoo/enterprise#123287
1 change
Enhancements to existing features
Receipt printer selection now uses the same broader matching rules already applied to preparation printers. This helps businesses connect supported printers more consistently in Point of Sale setups without unnecessary restrictions.
Original PR description
In odoo/enterprise#124306, we removed the subtype from the printer domain, but only for preparation printers. We also update it for receipt printers.