Daily updates from Odoo
Thursday, August 6, 2026
316 changes
39 changes
Enhancements to existing features
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
Resolved issues and error corrections
The payroll system now prevents refund payslips from being recalculated when users click compute, preserving their existing refund details. Users can still clear and reset the payslip lines when needed, reducing the risk of accidental changes to refund payroll records.
Original PR description
When a payslip is a refund payslip, we don't want to compute it if we click on "compute". The lines can still be reset when clicking on "reset" Forward-Port-Of: odoo/enterprise#126845
Appointment cancellation emails are now sent in the language of the customer who booked the appointment, matching the behavior of confirmation emails. This avoids confusing customers with cancellation notices in the staff member's language while keeping the old behavior for non-appointment events.
Original PR description
**Problem:** When an appointment is cancelled, the cancellation email is always sent in the organizer's (staff member's) language, ignoring the booking customer's language. The booking confirmation,…
**Problem:**
When an appointment is cancelled, the cancellation email is always sent in the organizer's (staff member's) language, ignoring the booking customer's language. The booking confirmation, by contrast, is correctly localized.
**Steps to reproduce:**
1. Set a contact's language to a non-default one (e.g. Romanian).
2. Book an appointment for that contact (they are the booker/attendee).
3. Cancel the appointment.
4. The customer received the confirmation in Romanian but the cancellation email arrives in English.
**Current behavior:**
The cancellation email is rendered in the organizer's language.
**Expected behavior:**
The cancellation email is rendered in the booking customer's language, like the confirmation/invitation email.
**Cause of the issue:**
The cancellation uses `appointment_canceled_mail_template`, whose `lang` is `{{ object.partner_id.lang }}`. On `calendar.event`, `partner_id` is `related='user_id.partner_id'`, i.e. the organizer, not the customer. The template is posted once per event (via `_track_template`), so its single rendering language applies to every recipient, including attendees whose own language differs. The confirmation email is unaffected because it is the per-attendee `attendee_invitation_mail_template` (model `calendar.attendee`), rendered once per attendee in that attendee's language.
**Fix:**
Deriving the language from `appointment_booker_id` makes the cancellation consistent with the other appointment mails, which are meant for the person who booked the meeting. It falls back to `partner_id` when there is no booker (e.g. an event not created through the appointment flow), preserving the previous behavior in that case.
opw-6323179
Forward-Port-Of: odoo/enterprise#125927
Forward-Port-Of: odoo/enterprise#124116This fix prevents users from accidentally expanding the same financial report line multiple times when clicking quickly or using a slow connection. Reports now fold and unfold reliably, avoiding confusing duplicate entries and improving day-to-day report navigation.
Original PR description
Steps to reproduce:- - Open any report. - Click on a particular line to unfold more than once very fast(or throttle network to 3G) - Once line is unfolded click again to fold that line. - Line is not…
Steps to reproduce:- - Open any report. - Click on a particular line to unfold more than once very fast(or throttle network to 3G) - Once line is unfolded click again to fold that line. - Line is not folding. Cause:- - When we clicked multiple times to unfold line, duplicate child lines were created(as many times as many times we clicked). - Because when first promise was not resolved so `unfolded = false` and we clicked again so new promise also tries to unfold the same line, resulting in unfolding the same line multiple times. - In version 17.0 these duplicate child lines are created but somehow not visible but it breaks `foldLine`. From version 18.0 onwards these duplicate child lines are visible. Solution: In `unfoldLine` set the flag `unfolding`. So in all clicks other than first, we get `unfolding = true` and don't proceed further, preventing unfolding the same line multiple times. task-6260425 Forward-Port-Of: odoo/enterprise#126765 Forward-Port-Of: odoo/enterprise#120392
The Vietnamese financial reports now classify short-term held-to-maturity loan balances under the correct balance sheet category required by Circular 99/2025. This helps businesses produce compliant balance sheets without manual adjustments.
Original PR description
### Expected behavior: As per circular 99/2025, short-term loan (12831) balance is required to fall under Held to Maturity Investment (Code 123) instead of 112, translated: ``` Short-term held-to-maturity investments (Code 123): includes held-to-maturity investments with a remaining term of 12 months or less from the end of the accounting period, such as term deposits, bonds, commercial paper, loans, and other debt securities. This item does not include held-to-maturity investments that have been presented in the item “Cash equivalents” ``` ### Steps to reproduce: Install `l10n_vn_reports` module ### Fix: PO validated: Update the Balance Sheet code formula for the 12381 account opw-6413120 Forward-Port-Of: odoo/enterprise#126763
Portal users editing Knowledge articles can now view and edit existing voice transcription text without seeing voice recording controls. This keeps recording features limited to internal users while preserving access to article content.
Original PR description
Portal users can edit Knowledge articles but should not have access to the voice recording feature, which is reserved for internal users. The readonly component is already used in read-only views, so reusing it in the editor for portal users is safe, it still renders the existing transcription content correctly without exposing any recording controls. Portal users can still edit the text content inside the component thanks to the editable descendants concept, which allows the editor to manage specific editable areas even within a readonly component. Task-6320461 Forward-Port-Of: odoo/enterprise#126606
Appointment bookings now open correctly when the appointment type name contains non-ASCII characters, such as Arabic text. This prevents customers from getting stuck in repeated redirects and ensures they can complete the booking form.
Original PR description
Clicking a slot on an appointment type with a non-ASCII name (for example an Arabic title) puts the browser in an endless 301 loop, so the info form never opens. ### Steps to reproduce - Create an…
Clicking a slot on an appointment type with a non-ASCII name (for example an Arabic title) puts the browser in an endless 301 loop, so the info form never opens. ### Steps to reproduce - Create an appointment type with an Arabic name, e.g. `عنوان`. - Open its page and pick a time slot. - The browser keeps redirecting on `/appointment/<slug>/info` and fails with "too many redirections". ### Cause The info URL is built from the slug `<name>-<id>`, here `عنوان-1`. We build a `URL` with `encodeURIComponent(slug)`, so `url.href` is already encoded once (`عنوان` becomes `%D8%B9...`). But we then navigate with `encodeURI(url.href)`, and `encodeURI` escapes the `%` signs a second time, so `%D8%B9...` becomes `%25D8%25B9...`. The slug is now encoded twice. To canonicalize the URL, the server decodes the request path and the path it rebuilds from the route, once each, and redirects if they differ. For a normal URL they are equal. For ours they are not, because one side is decoded one step less than the other, so the server keeps answering 301 with the same double-encoded URL. An ASCII slug has no `%` for `encodeURI` to escape, so only non-ASCII names hit this. ### Fix Navigate to `url.href` directly. It is already encoded, so the extra `encodeURI` only broke it. Same fix on the manual resource confirmation path. opw-6409641 Forward-Port-Of: odoo/enterprise#125497
This fix ensures Tyro payment surcharge fees are reliably added to point-of-sale orders before the order is validated. It prevents occasional missing surcharge lines caused by timing issues during payment completion, improving billing accuracy for merchants using Tyro.
Original PR description
Currently when completing a Tyro payment with a surcharge fee in some cases there is a race condition preventing the surcharge line to be added to the pos order before its validation This PR fixes that issue opw-6402191 Forward-Port-Of: odoo/enterprise#126035 Forward-Port-Of: odoo/enterprise#125852
Australian payroll data updates now include salary rule category information. This prevents scheduled payroll updates from failing when new payroll rule categories are introduced, helping payroll maintenance run reliably.
Original PR description
ir_cron_update_payroll_data updates the payroll data including rules but fails if a new rule category is introduced. This commit adds the hr_salary_rule_category_data file to the list of data files to update. task-6351929 Forward-Port-Of: odoo/enterprise#122392
Uploading a document from a contact now places it in the intended default workspace instead of reusing the last folder selected in Documents. This prevents files from being misfiled, such as ending up in Finance when they should go to My Drive or the contact-related document area.
Original PR description
Steps to reproduce ================== 1. Open Documents. 2. Select Finance. 3. Return to the home page and open the Contacts app. 4. Open any contact. 5. Click the Documents stat button. 6. Upload a document. Issue ===== The document is uploaded to the Finance folder instead of My Drive. Reason ====== When uploading a document using the upload button, we use `currentFolderAccessToken` to determine the destination folder. When opening the Documents view from a contact, `searchpanel_default_folder_id` is set to `False` so that documents are uploaded to the `All` workspace. However, when the search model is loaded, we do not reset `currentFolderAccessToken` when `folder_id` is `False`, causing the previously selected folder (Finance) to be reused. Task-6352242 Forward-Port-Of: odoo/enterprise#123285
Timesheet assistant rules were updated so suggestions no longer use empty captured text that could appear as confusing phrases such as "Discussing with undefined". This improves the clarity and reliability of automatically generated timesheet suggestions for users.
Original PR description
Several aw.rule regexes use (.*) for the capture groups feeding the suggestion name/description, allowing an empty match and producing incorrect suggestions (e.g. "Discussing with undefined") Task-6377168 Forward-Port-Of: odoo/enterprise#126751 Forward-Port-Of: odoo/enterprise#125771
Automatic bank reconciliation rules now use simpler text matching and also consider whether transaction amounts are incoming or outgoing. This helps show and apply the right reconciliation suggestions for each journal, reducing incorrect matches and manual cleanup.
Original PR description
Reconcile models automatically created now use contains instead of match regex and take the amount into consideration when creating the rule as well as checking for existing rules, it's checked whether all of the lines are positive or negative. Added an extra filter on the reconcile models so that it only shows rules that would be applied on the journal, and did some optimizations in the substring matching. task-6140372 Forward-Port-Of: odoo/enterprise#126187 Forward-Port-Of: odoo/enterprise#117256
Installing the website subscription feature no longer fails if the default monthly subscription plan was previously deleted. The setup now skips that optional plan link when it is unavailable, allowing businesses to complete installation without manual recovery steps.
Original PR description
Currently, an error occurs when user tries to install `website_sale_subscription` after deleting the monthly subscription plan. Steps to replicate: - Install `sale_subscription`. - Open Subscription…
Currently, an error occurs when user tries to install `website_sale_subscription` after deleting the monthly subscription plan.
Steps to replicate:
- Install `sale_subscription`.
- Open Subscription > Configuration > Recurring Plans.
- Delete the plan named `monthly`.
- Install `website_sale_subscription`.
Error:
```
ValueError: External ID not found in the system: sale_subscription.subscription_plan_month
odoo.tools.convert.ParseError: while parsing /home/odoo/odoo19/enterprise/website_sale_subscription/data/donation_data.xml:16, somewhere inside
<record id="product_recurring_donation_pricing_monthly" model="product.pricelist.item">
<field name="plan_id" ref="sale_subscription.subscription_plan_month"/>
<field name="product_tmpl_id" ref="product_recurring_donation"/>
<field name="fixed_price">1.0</field>
<field name="pricelist_id" eval="False"/>
</record>
```
Cause:
- Error is raised when assigning the `Monthly` subscription plan in the `website_sale_subscription` master data because the user has deleted the `Monthly` subscription plan.
Solution:
- As the `plan_id` is not a required field, we can skip assigning the `plan_id` if it the record is not found.
sentry-7441124574The cash basis report test now uses the company’s configured outstanding receipts account instead of assuming a fixed account code. This prevents false test failures when account codes differ across database setups, improving release reliability without changing user-facing accounting behavior.
Original PR description
Description of the issue this commit addresses: Commit 63f5646cfd75 made the tests use the default outstanding account but hard-coded code 101403. In an all-module database, generated account codes depend on existing accounts, so Outstanding Receipts may use code 101404 and make otherwise correct report assertions fail. --- Desired behavior after this commit is merged: This commit derives the expected report line name from the configured outstanding receipts account, making the assertions independent of its generated code. --- runbot-[231581](https://runbot.odoo.com/odoo/error/231581) Forward-Port-Of: odoo/enterprise#126743 Forward-Port-Of: odoo/enterprise#125652
This fixes missing styling for spreadsheet side panels in the backend. Users should see these panels with the intended layout and appearance after a previous cleanup accidentally removed the shared styling from the right place.
Original PR description
During the reorganisation of the css in #114180, the generic sidepanel.css file was removed from every assets. It belongs to the bundle assets at is impacts sidepanels that are only available in the backend (i.e. not in public spreadsheets) Task-6448906
This fix makes a product merge test consistently choose the intended main product before merging. It prevents intermittent automated build failures caused by unpredictable record ordering, improving release validation reliability.
Original PR description
Version: - saas-19.4 Steps to reproduce: - Run the test_merge_success_single_variant test case multiple times with different products created each time. Issue: The master record is selected based on its creation date, but that ordering is not always reliable. As a result, the wrong product may be chosen as the master,causing the incorrect product to be archived and the runbot test to fail. Fix: Before merging, we now manually set product 1 as the master. This makes the test predictable and stops the failures. Build error - 941476
Fixes an issue where online rental orders using click & collect could incorrectly show no availability because reservations from other warehouses were counted. Availability is now checked only against the selected pickup warehouse, helping customers complete valid rental orders and reducing checkout errors.
Original PR description
When using the click & collect option, the available qty for renting was not taking the selected warehouse into account. Steps to reproduce: ------------------- * Create 2 different warehouses with 2…
When using the click & collect option, the available qty for renting was not taking the selected warehouse into account. Steps to reproduce: ------------------- * Create 2 different warehouses with 2 different adresses * Create a product available for renting * Setup the product to use serial numbers * Create 2 serial number, 1 in each warehouse * Activate the click & collect option on the website * Create a first sale order to collect in warehouse 1 * In the backend, confirm the order and pick it up * Go back to the website and make a second order for the second warehouse > Observation: When clicking on the "Add to cart" you get an error saying that there is no quantity available Why the fix: ------------ When computing the `product_rented_quantities` it would look for `sale.order.line` in all the warehouse. So it would find the line from the first order even if it's not linked to the selected warehouse. So we just add a new element to the domain to filter out the incorrect warehouses. opw-6328475 Forward-Port-Of: odoo/enterprise#126817 Forward-Port-Of: odoo/enterprise#124969
User-facing messages and warnings now show translated labels for selection fields instead of untranslated internal values. This improves clarity for users working in different languages across accounting, payroll, recruitment, IoT, localization, appointments, documents, and reporting features.
Original PR description
The `selection` attribute of `fields.Selection` is not generally translated (unless it is a function instead of a list). For user facing strings, we generally need to translate the value displayed. Forward-Port-Of: odoo/enterprise#126741 Forward-Port-Of: odoo/enterprise#126538
Shop floor users can now finish the final work order in continuous production after entering a produced quantity. The update also ensures produced quantities display correctly when assigning serial numbers, reducing confusion and blocked manufacturing flows.
Original PR description
Previously there was a limitation for continuous production in shopfloor, that blocked the user from marking a workorder as done after registering a quantity. This commit fixes it by assigning the production's `quantity_producing` to the work order's `qty_produced` if it is the final work order. This unblocks the user and allows them to complete the work order.
Belgian CodaBox users can now revoke their connection using either the fiduciary password or a valid IAP token. This fixes a client-side gap so the existing server-side token option works as intended, making disconnection easier when the password is not used.
Original PR description
The user should be able to revoke the CodaBox connection by either entering the fidu password or by using a valid iap_token. This was implemented in the iap server but not in the client side, after this commit the user should be able to either revoke by using the fidu password or by using the iap_token. task-6348433 Forward-Port-Of: odoo/enterprise#126698
This fix prevents Approval records from trying to send notifications while temporary form data is being recalculated. Users working with Studio-created fields linked to Approval Requests can now update forms without crashes, including when requests are approved or refused.
Original PR description
**Before this change** We have the potential to attempt to send a notification email from virtual records created by our `BaseModel.new()` method. This can happen during an `onchange` request, given…
**Before this change** We have the potential to attempt to send a notification email from virtual records created by our `BaseModel.new()` method. This can happen during an `onchange` request, given that we'll be working with a virtual "snapshot" record to recompute potentially changed values for our origin record after a change to one or more values. This issue manifests when using Studio to attach a many2many field to a form view, where the related model is "Approval Request". If an approval request record in either the "Approved" or "Refused" state is attached to our record via this new Studio field, any `onchange` requests will trigger this bug. This is because we can't send messages on a virtual record. **After this change** Prevent the creation and sending of a message if we are computing the request status of a virtual `approval.request()` record. The field `request_status` on this model is computed and stored, but the fact that it is a computed field means that it must be recomputed for a virtual record, even if the origin record already has a stored `request_status`. Thus, we may need to compute a `request_status` value for an ephemeral "snapshot" record. Though the issue only manifests for the "Approved" and "Refused" states, this PR expands on a test that covers every approval request state. opw-6390453 Forward-Port-Of: odoo/enterprise#125374
Customer balances shown in Point of Sale are now calculated consistently when the company and PoS use different currencies. This prevents pay-later orders from being converted twice, so staff see the correct amount owed by the customer.
Original PR description
Steps to reproduce: - set the company currency to XCG - set the PoS sales journal currency and the PoS pricelist currency to USD - configure a XCG <-> USD rate - create a customer without any…
Steps to reproduce: - set the company currency to XCG - set the PoS sales journal currency and the PoS pricelist currency to USD - configure a XCG <-> USD rate - create a customer without any outstanding balance - open the PoS, create an order of USD 100 and validate it with the Customer Account (Pay Later) payment method - open the Customers screen and look at the Total Due of that customer Issue: The Total Due shows about USD 55.56, i.e. the amount converted once too many, instead of the expected USD 100. Cause: get_total_due() sums two amounts that are not expressed in the same currency before converting them. partner.total_due comes from the accounting entries, it is the sum of account.move.line.amount_residual and is therefore in company currency, while total_settled is the sum of pos.payment.amount of the still open sessions, which is in the currency of the order, so the PoS one. The addition is done first and the result is then converted from the company currency to the PoS one, so the pay later payments end up converted a second time. opw-6403320 Forward-Port-Of: odoo/enterprise#126402 Forward-Port-Of: odoo/enterprise#125798
Payslip reports now correctly treat notes that contain only empty formatting as blank. This prevents unnecessary empty note sections from appearing on payroll documents, keeping payslips and related reports cleaner for employees and payroll teams.
Original PR description
The condition was only checking if the note field was truthy. Actually, this can be a problem if the field is with only empty tags. Instead, we should use is_html_empty. Forward-Port-Of: odoo/enterprise#127008
Before this commit, test_chatbot_stop_when_agent_joins failed at random in nightly builds: FAILED: [8/13] Tour chatbot_stop_when_agent_joins_tour -> Step .o-livechat-root:shadow button:contains(Try again). Element (.o-livechat-root:shadow button:contains(Try again)) has not been found. TIMEOUT step failed to complete within 10000 ms. This happens because the test monkeypatches the chatbot_trigger_step route to make the email step fail once, while the routing map bak
Original PR description
Before this commit, test_chatbot_stop_when_agent_joins failed at random in nightly builds: FAILED: [8/13] Tour chatbot_stop_when_agent_joins_tour -> Step .o-livechat-root:shadow button:contains(Try…
Before this commit, test_chatbot_stop_when_agent_joins failed at random in nightly builds:
FAILED: [8/13] Tour chatbot_stop_when_agent_joins_tour -> Step
.o-livechat-root:shadow button:contains(Try again).
Element (.o-livechat-root:shadow button:contains(Try again)) has
not been found.
TIMEOUT step failed to complete within 10000 ms.
This happens because the test monkeypatches the chatbot_trigger_step route to make the email step fail once, while the routing map bakes the controller endpoints in when it is built and keeps them in the "routing" ormcache. Every later request reuses that map, so when it was already built with the original method the patch is ignored: the step never fails, the chatbot posts its last step and the retry button the tour clicks never appears.
This commit fixes the issue by invalidating the "routing" ormcache inside the patch, as done for meeting_view_tour, so the request reaches the patched route.
https://runbot.odoo.com/odoo/error/945351Followup of odoo/odoo@41fe2ebdb9cc. Before this commit, when trying to submit a track proposal with a speaker image, the request failed with: ``` TypeError: event.track.image: use BinaryValue instead of bytes ``` --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#274366
Original PR description
Followup of odoo/odoo@41fe2ebdb9cc. Before this commit, when trying to submit a track proposal with a speaker image, the request failed with: ``` TypeError: event.track.image: use BinaryValue instead of bytes ``` --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#274366
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#280396
Original PR description
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#280396
From versions 18.3 to 19.3, company_id and siret refer to the same field. This caused an issue when connecting to the PDP using siret or siren could overwrite company_id. task-6327304 Forward-Port-Of: odoo/odoo#277446 Forward-Port-Of: odoo/odoo#275276
Original PR description
From versions 18.3 to 19.3, company_id and siret refer to the same field. This caused an issue when connecting to the PDP using siret or siren could overwrite company_id. task-6327304 Forward-Port-Of: odoo/odoo#277446 Forward-Port-Of: odoo/odoo#275276
Steps to reproduce: 1) Install hr_holidays & hr_attendance. 2) Enable "Display Extra Hours" & "Absence Management" in the Attendance app settings. 3) Make a new employee, in the form view click the "+" button to make a new hr version record for this employee. 4) Set the date to be the first of the last month. 5) In the Attendance app -> New -> Select the new employee. 6) Select the check in and out dates to be from last month, edit the times such that the time worked is between 7 and 8 h
Original PR description
Steps to reproduce: 1) Install hr_holidays & hr_attendance. 2) Enable "Display Extra Hours" & "Absence Management" in the Attendance app settings. 3) Make a new employee, in the form view click the…
Steps to reproduce: 1) Install hr_holidays & hr_attendance. 2) Enable "Display Extra Hours" & "Absence Management" in the Attendance app settings. 3) Make a new employee, in the form view click the "+" button to make a new hr version record for this employee. 4) Set the date to be the first of the last month. 5) In the Attendance app -> New -> Select the new employee. 6) Select the check in and out dates to be from last month, edit the times such that the time worked is between 7 and 8 hours (ex 9:00am to 4:55pm). 7) In the Attendance app -> Reporting -> Attendances -> the test employee should have a negative value for "Worked Extra Hours" 8) Create a new time off type, enable "Deduct Extra Hours" & disable "Requires Allocation" use hours as the Unit of measure. 9) Open the time off smart button menu from the test employee's form view. Issue) The value seen in the report from step 7 is not the same as what the user sees in the dashboard. Notes) This issue can occur when an employee scheduled to work for 8 hours a day only clocks in for 7:55 hours leading to a negative extra time. The back-end has the correct value stored and sends it to the browser. The issue occurs because the JavaScript function that converts the decimal number of hours into a string (9.5 -> "9:30") does not work with negative input. This PR resolves that issue. opw-6417543 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280491
Before this commit: Deleting a record that uses a filterable selection field with `whitelist_fname` raises a traceback because the record field data becomes undefined during deletion. Steps to reproduce: 1. Install Belgium Accounting (l10n_be). 2. Create a contact and set a Peppol scheme and endpoint. 3. Delete the contact -> a traceback is raised. After this commit: The selection field safely handles undefined record field data during record deletion without causing an error. no-t
Original PR description
Before this commit: Deleting a record that uses a filterable selection field with `whitelist_fname` raises a traceback because the record field data becomes undefined during deletion. Steps to reproduce: 1. Install Belgium Accounting (l10n_be). 2. Create a contact and set a Peppol scheme and endpoint. 3. Delete the contact -> a traceback is raised. After this commit: The selection field safely handles undefined record field data during record deletion without causing an error. no-task Forward-Port-Of: odoo/odoo#280153 Forward-Port-Of: odoo/odoo#278808
### Issue: A user who can modify `tax_exigibility` but does not have `account.group_account_readonly` is blocked from switching to `on_payment` There is no way to complete the operation as the account field is not visible and cannot be filled before saving ### Cause: When `tax_exigibility` is set to `on_payment`, the view expects `cash_basis_transition_account_id` to be filled before saving`_constrains_cash_basis_transition_account` then validates that the account is reconcilable `cas
Original PR description
### Issue: A user who can modify `tax_exigibility` but does not have `account.group_account_readonly` is blocked from switching to `on_payment` There is no way to complete the operation as the…
### Issue: A user who can modify `tax_exigibility` but does not have `account.group_account_readonly` is blocked from switching to `on_payment` There is no way to complete the operation as the account field is not visible and cannot be filled before saving ### Cause: When `tax_exigibility` is set to `on_payment`, the view expects `cash_basis_transition_account_id` to be filled before saving`_constrains_cash_basis_transition_account` then validates that the account is reconcilable `cash_basis_transition_account_id` was restricted to `account.group_account_readonly`, hiding it from other users The field is never rendered, so it cannot be filled The `required` constraint is never evaluated client-side and `_constrains_cash_basis_transition_account` raises a `ValidationError` on save because the account is empty There is no reason to restrict `cash_basis_transition_account_id` independently — if a user can modify `tax_exigibility`, they must also be able to set the linked account ### Steps to reproduce: - Install `account` - Enable Cash Basis in Settings (On RunBot ensure no default account is set) - Enable Developer Mode in Settings - Go to Settings > Users & Companies > Users - Open the current user and disable both: `Show Accounting Features - Readonly` and `Show Full Accounting Features` (if set) - Go to Invoicing > Configuration > Taxes - Open any tax and go to Advanced Options - Change Tax Exigibility to Based on Payment Before the fix, the account selector is not displayed and saving raises an error opw-6359173 Forward-Port-Of: odoo/odoo#274728
Before this commit, the `empty a many2one field in list view` test sometimes failed, because the many2one value wasn't correctly unset (`first record` was selected). This happened because we cleared the input and automatically validated (typically with tab). However, it could happen that the validation occurred after the dropdown was opened, so the first value of the dropdown was selected. As a matter of fact, adding `await runAllTimers()` after clearing the input is a way to make the test fail
Original PR description
Before this commit, the `empty a many2one field in list view` test sometimes failed, because the many2one value wasn't correctly unset (`first record` was selected). This happened because we cleared…
Before this commit, the `empty a many2one field in list view` test sometimes failed, because the many2one value wasn't correctly unset (`first record` was selected). This happened because we cleared the input and automatically validated (typically with tab). However, it could happen that the validation occurred after the dropdown was opened, so the first value of the dropdown was selected. As a matter of fact, adding `await runAllTimers()` after clearing the input is a way to make the test fail deterministically. This commit avoids the issue by emptying the many2one without validation, so it basically only set the input value to the empty string, but doesn't tab/enter or anything else, hence it never selects an unwanted value. runbot error-941430 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#280351
If the badge selection widget value is false, you get an error as it cannot includes in false. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280678
Original PR description
If the badge selection widget value is false, you get an error as it cannot includes in false. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280678
The tracking field was removed from point_of_sale in https://github.com/odoo/odoo/pull/241368 However, forward ports may still keep the field in fixes prior to 19.3 like here in https://github.com/odoo/odoo/pull/279952 This breaks silently because issues only occur when stock is not installed so the forward-port build won't fail but some nightly builds will. runbot-error-944823 Forward-Port-Of: odoo/odoo#280716
Original PR description
The tracking field was removed from point_of_sale in https://github.com/odoo/odoo/pull/241368 However, forward ports may still keep the field in fixes prior to 19.3 like here in https://github.com/odoo/odoo/pull/279952 This breaks silently because issues only occur when stock is not installed so the forward-port build won't fail but some nightly builds will. runbot-error-944823 Forward-Port-Of: odoo/odoo#280716
This is rather an attempt of fix, as we couldn't reproduce the error locally or in a multi build., so we have no guarantee that this strenghtens the test. The test sometimes fails as `.o_crop_icon` can't be found within 200ms. Before that, we wait for the video element to be ready (we rely on a patch of the `isVideoReady` method of the component to know that the video is ready). Once it is, the isReady flag in the state is set to true and the CropOverlay component renders its `o_crop_icon` el
Original PR description
This is rather an attempt of fix, as we couldn't reproduce the error locally or in a multi build., so we have no guarantee that this strenghtens the test. The test sometimes fails as `.o_crop_icon`…
This is rather an attempt of fix, as we couldn't reproduce the error locally or in a multi build., so we have no guarantee that this strenghtens the test. The test sometimes fails as `.o_crop_icon` can't be found within 200ms. Before that, we wait for the video element to be ready (we rely on a patch of the `isVideoReady` method of the component to know that the video is ready). Once it is, the isReady flag in the state is set to true and the CropOverlay component renders its `o_crop_icon` element. Our guess is that we may sometimes early return in `isVideoReady`, because the component has been destroyed (a new rendering might be on the way). To ensure that we don't take that as the ready signal in the test, we now only consider that we're ready if isVideoReady returned true (i.e. no early return). runbot error-241798 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#280329
When a customer invoice is digitized through OCR, the salesperson may be set to the "Public User" instead of the internal user who uploaded the document. Steps to reproduce: - Upload a PDF invoice of an existing customer - Send the PDF to OCR - Reload the page Issue: Observe the Salesperson field: it is set to the Public User. Analysis: This occurs because when the partner is filled in, the compute of the salesperson will trigger. On SaaS this happens through the extraction c
Original PR description
When a customer invoice is digitized through OCR, the salesperson may be set to the "Public User" instead of the internal user who uploaded the document. Steps to reproduce: - Upload a PDF invoice of an existing customer - Send the PDF to OCR - Reload the page Issue: Observe the Salesperson field: it is set to the Public User. Analysis: This occurs because when the partner is filled in, the compute of the salesperson will trigger. On SaaS this happens through the extraction completion webhook a public route processed in sudo. that does not change the current user (public user). As self.env.user is the fallback of the compute, it may be set as salesperson. opw-6296330 Forward-Port-Of: odoo/odoo#279724
**PROBLEM** Before this PR, there was no way to handle invoices sent to clients depending from a JST/LGU (local government unit). This PR add support for it. opw-6095581 Forward-Port-Of: odoo/odoo#280377 Forward-Port-Of: odoo/odoo#260367
Original PR description
**PROBLEM** Before this PR, there was no way to handle invoices sent to clients depending from a JST/LGU (local government unit). This PR add support for it. opw-6095581 Forward-Port-Of: odoo/odoo#280377 Forward-Port-Of: odoo/odoo#260367
Miscellaneous changes
**Steps to reproduce:** - Install Survey app - Create a survey - Share as e-mail - Add some text in the body/subject - Modify recipients - The added text is automatically reset **Issue:** Behavior was previously introduced to match the content of the composer body/subject to the recipient language. If there was only one language among the recipients it automatically adapted the template and changed the rendered language (which also refreshed the content). This logic was trigge
Original PR description
**Steps to reproduce:** - Install Survey app - Create a survey - Share as e-mail - Add some text in the body/subject - Modify recipients - The added text is automatically reset **Issue:** Behavior…
**Steps to reproduce:** - Install Survey app - Create a survey - Share as e-mail - Add some text in the body/subject - Modify recipients - The added text is automatically reset **Issue:** Behavior was previously introduced to match the content of the composer body/subject to the recipient language. If there was only one language among the recipients it automatically adapted the template and changed the rendered language (which also refreshed the content). This logic was triggered by a depends on `partner_ids` and triggered the compute on every recipient changes which led to the subject/body reset. **Fix:** Revert commit: https://github.com/odoo/odoo/commit/b7bbb7b21f4848323666230b518cad9459726f67 in 18.0+ Also adapt commit: https://github.com/odoo/odoo/commit/c6f19e89cb6019e7dbaadbc7427fbb6ddd5661ed to avoid mixed language in resulting mail when the composer was modified We could also try to prevent the compute when the subject or body is already modified instead of removing its logic. opw-6020245 Forward-Port-Of: odoo/odoo#280162 Forward-Port-Of: odoo/odoo#254090
12 changes
Enhancements to existing features
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
Resolved issues and error corrections
Steps to reproduce: 1) Install hr_holidays & hr_attendance. 2) Enable "Display Extra Hours" & "Absence Management" in the Attendance app settings. 3) Make a new employee, in the form view click the "+" button to make a new hr version record for this employee. 4) Set the date to be the first of the last month. 5) In the Attendance app -> New -> Select the new employee. 6) Select the check in and out dates to be from last month, edit the times such that the time worked is between 7 and 8 h
Original PR description
Steps to reproduce: 1) Install hr_holidays & hr_attendance. 2) Enable "Display Extra Hours" & "Absence Management" in the Attendance app settings. 3) Make a new employee, in the form view click the…
Steps to reproduce: 1) Install hr_holidays & hr_attendance. 2) Enable "Display Extra Hours" & "Absence Management" in the Attendance app settings. 3) Make a new employee, in the form view click the "+" button to make a new hr version record for this employee. 4) Set the date to be the first of the last month. 5) In the Attendance app -> New -> Select the new employee. 6) Select the check in and out dates to be from last month, edit the times such that the time worked is between 7 and 8 hours (ex 9:00am to 4:55pm). 7) In the Attendance app -> Reporting -> Attendances -> the test employee should have a negative value for "Worked Extra Hours" 8) Create a new time off type, enable "Deduct Extra Hours" & disable "Requires Allocation" use hours as the Unit of measure. 9) Open the time off smart button menu from the test employee's form view. Issue) The value seen in the report from step 7 is not the same as what the user sees in the dashboard. Notes) This issue can occur when an employee scheduled to work for 8 hours a day only clocks in for 7:55 hours leading to a negative extra time. The back-end has the correct value stored and sends it to the browser. The issue occurs because the JavaScript function that converts the decimal number of hours into a string (9.5 -> "9:30") does not work with negative input. This PR resolves that issue. opw-6417543 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280491
Before this commit: Deleting a record that uses a filterable selection field with `whitelist_fname` raises a traceback because the record field data becomes undefined during deletion. Steps to reproduce: 1. Install Belgium Accounting (l10n_be). 2. Create a contact and set a Peppol scheme and endpoint. 3. Delete the contact -> a traceback is raised. After this commit: The selection field safely handles undefined record field data during record deletion without causing an error. no-t
Original PR description
Before this commit: Deleting a record that uses a filterable selection field with `whitelist_fname` raises a traceback because the record field data becomes undefined during deletion. Steps to reproduce: 1. Install Belgium Accounting (l10n_be). 2. Create a contact and set a Peppol scheme and endpoint. 3. Delete the contact -> a traceback is raised. After this commit: The selection field safely handles undefined record field data during record deletion without causing an error. no-task Forward-Port-Of: odoo/odoo#280153 Forward-Port-Of: odoo/odoo#278808
### Issue: A user who can modify `tax_exigibility` but does not have `account.group_account_readonly` is blocked from switching to `on_payment` There is no way to complete the operation as the account field is not visible and cannot be filled before saving ### Cause: When `tax_exigibility` is set to `on_payment`, the view expects `cash_basis_transition_account_id` to be filled before saving`_constrains_cash_basis_transition_account` then validates that the account is reconcilable `cas
Original PR description
### Issue: A user who can modify `tax_exigibility` but does not have `account.group_account_readonly` is blocked from switching to `on_payment` There is no way to complete the operation as the…
### Issue: A user who can modify `tax_exigibility` but does not have `account.group_account_readonly` is blocked from switching to `on_payment` There is no way to complete the operation as the account field is not visible and cannot be filled before saving ### Cause: When `tax_exigibility` is set to `on_payment`, the view expects `cash_basis_transition_account_id` to be filled before saving`_constrains_cash_basis_transition_account` then validates that the account is reconcilable `cash_basis_transition_account_id` was restricted to `account.group_account_readonly`, hiding it from other users The field is never rendered, so it cannot be filled The `required` constraint is never evaluated client-side and `_constrains_cash_basis_transition_account` raises a `ValidationError` on save because the account is empty There is no reason to restrict `cash_basis_transition_account_id` independently — if a user can modify `tax_exigibility`, they must also be able to set the linked account ### Steps to reproduce: - Install `account` - Enable Cash Basis in Settings (On RunBot ensure no default account is set) - Enable Developer Mode in Settings - Go to Settings > Users & Companies > Users - Open the current user and disable both: `Show Accounting Features - Readonly` and `Show Full Accounting Features` (if set) - Go to Invoicing > Configuration > Taxes - Open any tax and go to Advanced Options - Change Tax Exigibility to Based on Payment Before the fix, the account selector is not displayed and saving raises an error opw-6359173 Forward-Port-Of: odoo/odoo#274728
Before this commit, the `empty a many2one field in list view` test sometimes failed, because the many2one value wasn't correctly unset (`first record` was selected). This happened because we cleared the input and automatically validated (typically with tab). However, it could happen that the validation occurred after the dropdown was opened, so the first value of the dropdown was selected. As a matter of fact, adding `await runAllTimers()` after clearing the input is a way to make the test fail
Original PR description
Before this commit, the `empty a many2one field in list view` test sometimes failed, because the many2one value wasn't correctly unset (`first record` was selected). This happened because we cleared…
Before this commit, the `empty a many2one field in list view` test sometimes failed, because the many2one value wasn't correctly unset (`first record` was selected). This happened because we cleared the input and automatically validated (typically with tab). However, it could happen that the validation occurred after the dropdown was opened, so the first value of the dropdown was selected. As a matter of fact, adding `await runAllTimers()` after clearing the input is a way to make the test fail deterministically. This commit avoids the issue by emptying the many2one without validation, so it basically only set the input value to the empty string, but doesn't tab/enter or anything else, hence it never selects an unwanted value. runbot error-941430 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#280351
This is rather an attempt of fix, as we couldn't reproduce the error locally or in a multi build., so we have no guarantee that this strenghtens the test. The test sometimes fails as `.o_crop_icon` can't be found within 200ms. Before that, we wait for the video element to be ready (we rely on a patch of the `isVideoReady` method of the component to know that the video is ready). Once it is, the isReady flag in the state is set to true and the CropOverlay component renders its `o_crop_icon` el
Original PR description
This is rather an attempt of fix, as we couldn't reproduce the error locally or in a multi build., so we have no guarantee that this strenghtens the test. The test sometimes fails as `.o_crop_icon`…
This is rather an attempt of fix, as we couldn't reproduce the error locally or in a multi build., so we have no guarantee that this strenghtens the test. The test sometimes fails as `.o_crop_icon` can't be found within 200ms. Before that, we wait for the video element to be ready (we rely on a patch of the `isVideoReady` method of the component to know that the video is ready). Once it is, the isReady flag in the state is set to true and the CropOverlay component renders its `o_crop_icon` element. Our guess is that we may sometimes early return in `isVideoReady`, because the component has been destroyed (a new rendering might be on the way). To ensure that we don't take that as the ready signal in the test, we now only consider that we're ready if isVideoReady returned true (i.e. no early return). runbot error-241798 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#280329
When a customer invoice is digitized through OCR, the salesperson may be set to the "Public User" instead of the internal user who uploaded the document. Steps to reproduce: - Upload a PDF invoice of an existing customer - Send the PDF to OCR - Reload the page Issue: Observe the Salesperson field: it is set to the Public User. Analysis: This occurs because when the partner is filled in, the compute of the salesperson will trigger. On SaaS this happens through the extraction c
Original PR description
When a customer invoice is digitized through OCR, the salesperson may be set to the "Public User" instead of the internal user who uploaded the document. Steps to reproduce: - Upload a PDF invoice of an existing customer - Send the PDF to OCR - Reload the page Issue: Observe the Salesperson field: it is set to the Public User. Analysis: This occurs because when the partner is filled in, the compute of the salesperson will trigger. On SaaS this happens through the extraction completion webhook a public route processed in sudo. that does not change the current user (public user). As self.env.user is the fallback of the compute, it may be set as salesperson. opw-6296330 Forward-Port-Of: odoo/odoo#279724
### Steps to reproduce: - In the settings enable: Multi-Steps Routes - Inventory > Configuration > Warehouse Management > Warehouses - Set your warehouse deliveries in two steps - Inventory > Operations > Tranfers > Internal > New - Set the operation type as Pick, set a partner and add Partner: Bob - In the sales & Purchase tab of the partner form set a customer location to be a child of the Customers location: Customers/Bob'Stock - Confirm and validate the Pick for 1 unit of a product
Original PR description
### Steps to reproduce: - In the settings enable: Multi-Steps Routes - Inventory > Configuration > Warehouse Management > Warehouses - Set your warehouse deliveries in two steps - Inventory >…
### Steps to reproduce: - In the settings enable: Multi-Steps Routes - Inventory > Configuration > Warehouse Management > Warehouses - Set your warehouse deliveries in two steps - Inventory > Operations > Tranfers > Internal > New - Set the operation type as Pick, set a partner and add Partner: Bob - In the sales & Purchase tab of the partner form set a customer location to be a child of the Customers location: Customers/Bob'Stock - Confirm and validate the Pick for 1 unit of a product P #### > A ship picking is created but the destination of the related move is still set to the default customer location. ### Note: If the flow is performed by a sale order, the `property_stock_customer` location will appropriately be used as `location_final_id`: https://github.com/odoo/odoo/blob/7609b5805c3704034b4d7813e2f356381ed18771/addons/sale_stock/models/sale_order_line.py#L297 https://github.com/odoo/odoo/blob/7609b5805c3704034b4d7813e2f356381ed18771/addons/sale_stock/models/sale_order_line.py#L306-L309 https://github.com/odoo/odoo/blob/720598d0315dbb91628441078febfd43ffefb431/addons/stock/models/stock_rule.py#L263-L264 So that the bug does not occur in that case. By contrast if the pick move is created manually, we do not set its `location_final_id` and hence do not propagate the info. Even though it looks expected to be set set as location_dest_id of the ship move sas suggested by the `stock.picking.location_dest_id` compute method : https://github.com/odoo/odoo/blob/fe3aea07a1964cd24f4c8ebf2bc93e483eca6b0b/addons/stock/models/stock_picking.py#L990-L1002 opw-6402483 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#279616 Forward-Port-Of: odoo/odoo#278838
**PROBLEM** Before this PR, there was no way to handle invoices sent to clients depending from a JST/LGU (local government unit). This PR add support for it. opw-6095581 Forward-Port-Of: odoo/odoo#280377 Forward-Port-Of: odoo/odoo#260367
Original PR description
**PROBLEM** Before this PR, there was no way to handle invoices sent to clients depending from a JST/LGU (local government unit). This PR add support for it. opw-6095581 Forward-Port-Of: odoo/odoo#280377 Forward-Port-Of: odoo/odoo#260367
Miscellaneous changes
**Steps to reproduce:** - Install Survey app - Create a survey - Share as e-mail - Add some text in the body/subject - Modify recipients - The added text is automatically reset **Issue:** Behavior was previously introduced to match the content of the composer body/subject to the recipient language. If there was only one language among the recipients it automatically adapted the template and changed the rendered language (which also refreshed the content). This logic was trigge
Original PR description
**Steps to reproduce:** - Install Survey app - Create a survey - Share as e-mail - Add some text in the body/subject - Modify recipients - The added text is automatically reset **Issue:** Behavior…
**Steps to reproduce:** - Install Survey app - Create a survey - Share as e-mail - Add some text in the body/subject - Modify recipients - The added text is automatically reset **Issue:** Behavior was previously introduced to match the content of the composer body/subject to the recipient language. If there was only one language among the recipients it automatically adapted the template and changed the rendered language (which also refreshed the content). This logic was triggered by a depends on `partner_ids` and triggered the compute on every recipient changes which led to the subject/body reset. **Fix:** Revert commit: https://github.com/odoo/odoo/commit/b7bbb7b21f4848323666230b518cad9459726f67 in 18.0+ Also adapt commit: https://github.com/odoo/odoo/commit/c6f19e89cb6019e7dbaadbc7427fbb6ddd5661ed to avoid mixed language in resulting mail when the composer was modified We could also try to prevent the compute when the subject or body is already modified instead of removing its logic. opw-6020245 Forward-Port-Of: odoo/odoo#280162 Forward-Port-Of: odoo/odoo#254090
8 changes
Enhancements to existing features
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
Resolved issues and error corrections
Before this commit, turning the camera on during a call could leave the camera off in every member's UI, including the local user's own tile, while the video was already being sent. This happens because toggleVideo awaits network.updateUpload before updateAndBroadcast, and updateUpload waits on the ready promise of every peer. A single member whose handshake never completes holds isCameraOn and isScreenSharingOn for everyone. This commit fixes the issue by broadcasting the state first and
Original PR description
Before this commit, turning the camera on during a call could leave the camera off in every member's UI, including the local user's own tile, while the video was already being sent. This happens because toggleVideo awaits network.updateUpload before updateAndBroadcast, and updateUpload waits on the ready promise of every peer. A single member whose handshake never completes holds isCameraOn and isScreenSharingOn for everyone. This commit fixes the issue by broadcasting the state first and awaiting the upload after. Note that updateUpload sends its info snapshot to the peers synchronously, so they still learn the new track. Back-port of https://github.com/odoo/odoo/pull/279106 Forward-Port-Of: odoo/odoo#280492 Forward-Port-Of: odoo/odoo#280014
Steps to reproduce: 1) Install hr_holidays & hr_attendance. 2) Enable "Display Extra Hours" & "Absence Management" in the Attendance app settings. 3) Make a new employee, in the form view click the "+" button to make a new hr version record for this employee. 4) Set the date to be the first of the last month. 5) In the Attendance app -> New -> Select the new employee. 6) Select the check in and out dates to be from last month, edit the times such that the time worked is between 7 and 8 h
Original PR description
Steps to reproduce: 1) Install hr_holidays & hr_attendance. 2) Enable "Display Extra Hours" & "Absence Management" in the Attendance app settings. 3) Make a new employee, in the form view click the…
Steps to reproduce: 1) Install hr_holidays & hr_attendance. 2) Enable "Display Extra Hours" & "Absence Management" in the Attendance app settings. 3) Make a new employee, in the form view click the "+" button to make a new hr version record for this employee. 4) Set the date to be the first of the last month. 5) In the Attendance app -> New -> Select the new employee. 6) Select the check in and out dates to be from last month, edit the times such that the time worked is between 7 and 8 hours (ex 9:00am to 4:55pm). 7) In the Attendance app -> Reporting -> Attendances -> the test employee should have a negative value for "Worked Extra Hours" 8) Create a new time off type, enable "Deduct Extra Hours" & disable "Requires Allocation" use hours as the Unit of measure. 9) Open the time off smart button menu from the test employee's form view. Issue) The value seen in the report from step 7 is not the same as what the user sees in the dashboard. Notes) This issue can occur when an employee scheduled to work for 8 hours a day only clocks in for 7:55 hours leading to a negative extra time. The back-end has the correct value stored and sends it to the browser. The issue occurs because the JavaScript function that converts the decimal number of hours into a string (9.5 -> "9:30") does not work with negative input. This PR resolves that issue. opw-6417543 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280491
Before this commit: Deleting a record that uses a filterable selection field with `whitelist_fname` raises a traceback because the record field data becomes undefined during deletion. Steps to reproduce: 1. Install Belgium Accounting (l10n_be). 2. Create a contact and set a Peppol scheme and endpoint. 3. Delete the contact -> a traceback is raised. After this commit: The selection field safely handles undefined record field data during record deletion without causing an error. no-t
Original PR description
Before this commit: Deleting a record that uses a filterable selection field with `whitelist_fname` raises a traceback because the record field data becomes undefined during deletion. Steps to reproduce: 1. Install Belgium Accounting (l10n_be). 2. Create a contact and set a Peppol scheme and endpoint. 3. Delete the contact -> a traceback is raised. After this commit: The selection field safely handles undefined record field data during record deletion without causing an error. no-task Forward-Port-Of: odoo/odoo#280153 Forward-Port-Of: odoo/odoo#278808
Bug === Since a4efb745e9f4ba24dfa01f1e17e65f1a7cac90a5 , we cannot insert properties. The reason is that we try to get the field definition from the inserted QWeb expression (eg `object.properties.get('property_product', env['product']).name`) which crash. Task-6311655
Original PR description
Bug
===
Since a4efb745e9f4ba24dfa01f1e17e65f1a7cac90a5 , we cannot insert properties. The reason is that we try to get the field definition from the inserted QWeb expression (eg `object.properties.get('property_product', env['product']).name`) which crash.
Task-63116559 changes
Enhancements to existing features
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
Resolved issues and error corrections
Version: -------- - 19.0+ Steps to reproduce: ------------------- - Install `purchase` and `stock` - Create a Purchase Order for a storable or consumable product - Set the ordered quantity to a negative value (e.g. `-10`) - Confirm the Purchase Order. - Open the Deliveries and search using the Purchase Order name Issue: ------ An outgoing return picking is correctly created for 10 units, but an additional empty incoming receipt is also generated and linked to the Purchase Order
Original PR description
Version: -------- - 19.0+ Steps to reproduce: ------------------- - Install `purchase` and `stock` - Create a Purchase Order for a storable or consumable product - Set the ordered quantity to a…
Version: -------- - 19.0+ Steps to reproduce: ------------------- - Install `purchase` and `stock` - Create a Purchase Order for a storable or consumable product - Set the ordered quantity to a negative value (e.g. `-10`) - Confirm the Purchase Order. - Open the Deliveries and search using the Purchase Order name Issue: ------ An outgoing return picking is correctly created for 10 units, but an additional empty incoming receipt is also generated and linked to the Purchase Order. Cause: ------ When a Purchase Order is confirmed, `purchase.order.button_approve()` calls `purchase.order._create_picking()`: https://github.com/odoo/odoo/blob/ed0d1bd21131920cb9c7b2ae69d7baedc2818e85/addons/purchase_stock/models/purchase_order.py#L179 Inside `_create_picking()`, if no open picking exists yet, Then first creates an incoming receipt using `_prepare_picking()`: https://github.com/odoo/odoo/blob/ed0d1bd21131920cb9c7b2ae69d7baedc2818e85/addons/purchase_stock/models/purchase_order.py#L382 The generated receipt is configured with: - source location: Vendor - destination location: Stock - picking type: Incoming - origin: Purchase Order reference Stock moves are then created on that receipt through: https://github.com/odoo/odoo/blob/ed0d1bd21131920cb9c7b2ae69d7baedc2818e85/addons/purchase_stock/models/purchase_order.py#L387-L388 For negative PO quantities, the move is initially created with a negative demand: ``` Vendor -> Stock, quantity -10 ```` During `stock.move._action_confirm()`, stock identifies such moves in `neg_r_moves`: [https://github.com/odoo/odoo/blob/ed0d1bd21131920cb9c7b2ae69d7baedc2818e85/addons/stock/models/stock_move.py#L1604](https://github.com/odoo/odoo/blob/ed0d1bd21131920cb9c7b2ae69d7baedc2818e85/addons/stock/models/stock_move.py#L1604) Those moves are automatically converted into return moves by: * swapping source and destination locations, * converting the quantity to positive, * assigning the return picking type when available: [https://github.com/odoo/odoo/blob/ed0d1bd21131920cb9c7b2ae69d7baedc2818e85/addons/stock/models/stock_move.py#L1612-L1614](https://github.com/odoo/odoo/blob/ed0d1bd21131920cb9c7b2ae69d7baedc2818e85/addons/stock/models/stock_move.py#L1612-L1614) The resulting move becomes: ``` Stock -> Vendor, quantity 10 ``` The converted move is then reassigned through: [https://github.com/odoo/odoo/blob/ed0d1bd21131920cb9c7b2ae69d7baedc2818e85/addons/stock/models/stock_move.py#L1630](https://github.com/odoo/odoo/blob/ed0d1bd21131920cb9c7b2ae69d7baedc2818e85/addons/stock/models/stock_move.py#L1630) This correctly creates the outgoing return picking. However, the original incoming receipt created earlier by `purchase_stock` is left without any moves, resulting in an empty ghost receipt linked to the Purchase Order. Solution: --------- - Track whether `_create_picking()` created a new incoming receipt during the current confirmation flow. - After stock move confirmation, if that newly-created receipt no longer contains any moves, unlink it and remove it from the pickings to confirm. - This preserves the valid outgoing return picking while preventing empty incoming receipts from being generated. NOTE: ------ This Issue not reproduce from saas-19.3. Fix in this [commit](https://github.com/odoo-dev/odoo/commit/81caaaa3f7212ad5d9d72cd446b65c5fb0a9bef9) --- opw-6131329 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#265704
Before this commit: Deleting a record that uses a filterable selection field with `whitelist_fname` raises a traceback because the record field data becomes undefined during deletion. Steps to reproduce: 1. Install Belgium Accounting (l10n_be). 2. Create a contact and set a Peppol scheme and endpoint. 3. Delete the contact -> a traceback is raised. After this commit: The selection field safely handles undefined record field data during record deletion without causing an error. no-t
Original PR description
Before this commit: Deleting a record that uses a filterable selection field with `whitelist_fname` raises a traceback because the record field data becomes undefined during deletion. Steps to reproduce: 1. Install Belgium Accounting (l10n_be). 2. Create a contact and set a Peppol scheme and endpoint. 3. Delete the contact -> a traceback is raised. After this commit: The selection field safely handles undefined record field data during record deletion without causing an error. no-task Forward-Port-Of: odoo/odoo#280153 Forward-Port-Of: odoo/odoo#278808
### Issue: A user who can modify `tax_exigibility` but does not have `account.group_account_readonly` is blocked from switching to `on_payment` There is no way to complete the operation as the account field is not visible and cannot be filled before saving ### Cause: When `tax_exigibility` is set to `on_payment`, the view expects `cash_basis_transition_account_id` to be filled before saving`_constrains_cash_basis_transition_account` then validates that the account is reconcilable `cas
Original PR description
### Issue: A user who can modify `tax_exigibility` but does not have `account.group_account_readonly` is blocked from switching to `on_payment` There is no way to complete the operation as the…
### Issue: A user who can modify `tax_exigibility` but does not have `account.group_account_readonly` is blocked from switching to `on_payment` There is no way to complete the operation as the account field is not visible and cannot be filled before saving ### Cause: When `tax_exigibility` is set to `on_payment`, the view expects `cash_basis_transition_account_id` to be filled before saving`_constrains_cash_basis_transition_account` then validates that the account is reconcilable `cash_basis_transition_account_id` was restricted to `account.group_account_readonly`, hiding it from other users The field is never rendered, so it cannot be filled The `required` constraint is never evaluated client-side and `_constrains_cash_basis_transition_account` raises a `ValidationError` on save because the account is empty There is no reason to restrict `cash_basis_transition_account_id` independently — if a user can modify `tax_exigibility`, they must also be able to set the linked account ### Steps to reproduce: - Install `account` - Enable Cash Basis in Settings (On RunBot ensure no default account is set) - Enable Developer Mode in Settings - Go to Settings > Users & Companies > Users - Open the current user and disable both: `Show Accounting Features - Readonly` and `Show Full Accounting Features` (if set) - Go to Invoicing > Configuration > Taxes - Open any tax and go to Advanced Options - Change Tax Exigibility to Based on Payment Before the fix, the account selector is not displayed and saving raises an error opw-6359173 Forward-Port-Of: odoo/odoo#274728
Since https://github.com/odoo/odoo/pull/239898 Opening a livechat session in debug mode as a user without livechat access throws a traceback: `Invalid props for component BadgeTag: onDelete is not a function` This happens because `null` is passed to the `onDelete` prop, while the component validation expects a function. This commit fixes the issue. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Original PR description
Since https://github.com/odoo/odoo/pull/239898 Opening a livechat session in debug mode as a user without livechat access throws a traceback: `Invalid props for component BadgeTag: onDelete is not a function` This happens because `null` is passed to the `onDelete` prop, while the component validation expects a function. This commit fixes the issue. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Before this commit, the `empty a many2one field in list view` test sometimes failed, because the many2one value wasn't correctly unset (`first record` was selected). This happened because we cleared the input and automatically validated (typically with tab). However, it could happen that the validation occurred after the dropdown was opened, so the first value of the dropdown was selected. As a matter of fact, adding `await runAllTimers()` after clearing the input is a way to make the test fail
Original PR description
Before this commit, the `empty a many2one field in list view` test sometimes failed, because the many2one value wasn't correctly unset (`first record` was selected). This happened because we cleared…
Before this commit, the `empty a many2one field in list view` test sometimes failed, because the many2one value wasn't correctly unset (`first record` was selected). This happened because we cleared the input and automatically validated (typically with tab). However, it could happen that the validation occurred after the dropdown was opened, so the first value of the dropdown was selected. As a matter of fact, adding `await runAllTimers()` after clearing the input is a way to make the test fail deterministically. This commit avoids the issue by emptying the many2one without validation, so it basically only set the input value to the empty string, but doesn't tab/enter or anything else, hence it never selects an unwanted value. runbot error-941430 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#280351
This is rather an attempt of fix, as we couldn't reproduce the error locally or in a multi build., so we have no guarantee that this strenghtens the test. The test sometimes fails as `.o_crop_icon` can't be found within 200ms. Before that, we wait for the video element to be ready (we rely on a patch of the `isVideoReady` method of the component to know that the video is ready). Once it is, the isReady flag in the state is set to true and the CropOverlay component renders its `o_crop_icon` el
Original PR description
This is rather an attempt of fix, as we couldn't reproduce the error locally or in a multi build., so we have no guarantee that this strenghtens the test. The test sometimes fails as `.o_crop_icon`…
This is rather an attempt of fix, as we couldn't reproduce the error locally or in a multi build., so we have no guarantee that this strenghtens the test. The test sometimes fails as `.o_crop_icon` can't be found within 200ms. Before that, we wait for the video element to be ready (we rely on a patch of the `isVideoReady` method of the component to know that the video is ready). Once it is, the isReady flag in the state is set to true and the CropOverlay component renders its `o_crop_icon` element. Our guess is that we may sometimes early return in `isVideoReady`, because the component has been destroyed (a new rendering might be on the way). To ensure that we don't take that as the ready signal in the test, we now only consider that we're ready if isVideoReady returned true (i.e. no early return). runbot error-241798 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#280329
When a customer invoice is digitized through OCR, the salesperson may be set to the "Public User" instead of the internal user who uploaded the document. Steps to reproduce: - Upload a PDF invoice of an existing customer - Send the PDF to OCR - Reload the page Issue: Observe the Salesperson field: it is set to the Public User. Analysis: This occurs because when the partner is filled in, the compute of the salesperson will trigger. On SaaS this happens through the extraction c
Original PR description
When a customer invoice is digitized through OCR, the salesperson may be set to the "Public User" instead of the internal user who uploaded the document. Steps to reproduce: - Upload a PDF invoice of an existing customer - Send the PDF to OCR - Reload the page Issue: Observe the Salesperson field: it is set to the Public User. Analysis: This occurs because when the partner is filled in, the compute of the salesperson will trigger. On SaaS this happens through the extraction completion webhook a public route processed in sudo. that does not change the current user (public user). As self.env.user is the fallback of the compute, it may be set as salesperson. opw-6296330 Forward-Port-Of: odoo/odoo#279724
### Steps to reproduce: - Create 3 AVCO products: Super Kit, Kit, Comp - Super Kit BoM: 2 x Kit - Kit BoM: 1 x Comp - Create and confirm a purchase order for 1 X Super Kit at 100 - Validate the receipt of 2 Comp - Go to the valuation > Both units of Comp are valued at 100 for a total of 200 ### Cause of the issue: The issue has been introduced by: https://github.com/odoo/odoo/pull/158849/changes/713701a5035d342263e3fef2a2819b5696b6d063 To be more precise, the price unit of each u
Original PR description
### Steps to reproduce: - Create 3 AVCO products: Super Kit, Kit, Comp - Super Kit BoM: 2 x Kit - Kit BoM: 1 x Comp - Create and confirm a purchase order for 1 X Super Kit at 100 - Validate the…
### Steps to reproduce: - Create 3 AVCO products: Super Kit, Kit, Comp - Super Kit BoM: 2 x Kit - Kit BoM: 1 x Comp - Create and confirm a purchase order for 1 X Super Kit at 100 - Validate the receipt of 2 Comp - Go to the valuation > Both units of Comp are valued at 100 for a total of 200 ### Cause of the issue: The issue has been introduced by: https://github.com/odoo/odoo/pull/158849/changes/713701a5035d342263e3fef2a2819b5696b6d063 To be more precise, the price unit of each unit of Comp is expected to be computed by the `_get_price_unit`. This method used to rely on the `product_qty` appropriately: https://github.com/odoo/odoo/pull/158849/changes/713701a5035d342263e3fef2a2819b5696b6d063#diff-687527af1723e60816358020c4d83687479df62ca0cfd71079f0a82afb4b3efeL27 However, backorder adapt the move demand and hence did not provide the appropriate demand in this flow that computation logic was changed to rely on the `bom` and `bom_line` quantities: https://github.com/odoo/odoo/blob/29977a6a80442af49ecefa7fef54f085483d8f77/addons/purchase_mrp/models/stock_move.py#L20-L40 This new computation is not correct in case of nested boms since the `bom_line` only carries the unit demand on the last explosion stage. ### Additional issue: If nested kit boms lead to the creation of 2 moves with the same `cost_share` and `bom_line_id`, these moves will be merged without summing their `cost_share` leading to an under pricing of the kit since its related `stock_move`'s `cost_share` will not sum up to 100 percents anymore. This issue is tested in `test_avco_purchase_nested_kit_explode_cost_share_backorder_2` and fixed similarly to demand merging: https://github.com/odoo/odoo/blob/613f3cb7b2f4813ce6c8f53718a6cca841b081ad/addons/stock/models/stock_move.py#L1122-L1134 ### Note: We also modify the test `test_valuation_with_backorder` to be understandable and to make appropriate asserts. opw-6253776 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#279890 Forward-Port-Of: odoo/odoo#276242
4 changes
Resolved issues and error corrections
**Steps to reproduce:** - Install l10n_pl_edi and enable Allow KSeF integration from **Accounting** settings. - Switch to a Polish company. - Create an EU customer with a valid VAT number. - Create a sale order containing a service product taxed with 0% Steel (or any tax that has a tag of K11). - Confirm the sale order and create a down payment invoice. - Send the invoice to KSeF and inspect the generated XML. **Observed behavior:** The generated KSeF XML does not contai
Original PR description
**Steps to reproduce:** - Install l10n_pl_edi and enable Allow KSeF integration from **Accounting** settings. - Switch to a Polish company. - Create an EU customer with a valid VAT number. - Create a…
**Steps to reproduce:** - Install l10n_pl_edi and enable Allow KSeF integration from **Accounting** settings. - Switch to a Polish company. - Create an EU customer with a valid VAT number. - Create a sale order containing a service product taxed with 0% Steel (or any tax that has a tag of K11). - Confirm the sale order and create a down payment invoice. - Send the invoice to KSeF and inspect the generated XML. **Observed behavior:** The generated KSeF XML does not contain the `P_13_8` field. **Cause:** For invoices involving the tax of tag `K11` (mainly these taxes are used for the supplies that are outside the territory of Poland), the value corresponding to `P_13_8` was not being assigned during XML generation, causing the tag to be omitted from the exported KSeF document. **Fix:** Populate the value of `P_13_8` during KSeF XML generation for invoices, ensuring the field is correctly included in the exported XML. This PR updates the computation of tag `P_13_10` with its test case to ensure consistency with the expected reporting logic, where the tag is computed solely from `K_31`. Here is the [Documentation](https://ksef.podatki.gov.pl/media/gtjhkeek/information-sheet-on-the-fa-3-logical-structure-04032026.pdf) link for the reference of the Ksef structure. **opw**-6294181 Forward-Port-Of: odoo/odoo#276887
Before this commit, the `empty a many2one field in list view` test sometimes failed, because the many2one value wasn't correctly unset (`first record` was selected). This happened because we cleared the input and automatically validated (typically with tab). However, it could happen that the validation occurred after the dropdown was opened, so the first value of the dropdown was selected. As a matter of fact, adding `await runAllTimers()` after clearing the input is a way to make the test fail
Original PR description
Before this commit, the `empty a many2one field in list view` test sometimes failed, because the many2one value wasn't correctly unset (`first record` was selected). This happened because we cleared…
Before this commit, the `empty a many2one field in list view` test sometimes failed, because the many2one value wasn't correctly unset (`first record` was selected). This happened because we cleared the input and automatically validated (typically with tab). However, it could happen that the validation occurred after the dropdown was opened, so the first value of the dropdown was selected. As a matter of fact, adding `await runAllTimers()` after clearing the input is a way to make the test fail deterministically. This commit avoids the issue by emptying the many2one without validation, so it basically only set the input value to the empty string, but doesn't tab/enter or anything else, hence it never selects an unwanted value. runbot error-941430 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#280351
This is rather an attempt of fix, as we couldn't reproduce the error locally or in a multi build., so we have no guarantee that this strenghtens the test. The test sometimes fails as `.o_crop_icon` can't be found within 200ms. Before that, we wait for the video element to be ready (we rely on a patch of the `isVideoReady` method of the component to know that the video is ready). Once it is, the isReady flag in the state is set to true and the CropOverlay component renders its `o_crop_icon` el
Original PR description
This is rather an attempt of fix, as we couldn't reproduce the error locally or in a multi build., so we have no guarantee that this strenghtens the test. The test sometimes fails as `.o_crop_icon`…
This is rather an attempt of fix, as we couldn't reproduce the error locally or in a multi build., so we have no guarantee that this strenghtens the test. The test sometimes fails as `.o_crop_icon` can't be found within 200ms. Before that, we wait for the video element to be ready (we rely on a patch of the `isVideoReady` method of the component to know that the video is ready). Once it is, the isReady flag in the state is set to true and the CropOverlay component renders its `o_crop_icon` element. Our guess is that we may sometimes early return in `isVideoReady`, because the component has been destroyed (a new rendering might be on the way). To ensure that we don't take that as the ready signal in the test, we now only consider that we're ready if isVideoReady returned true (i.e. no early return). runbot error-241798 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#280329
When a customer invoice is digitized through OCR, the salesperson may be set to the "Public User" instead of the internal user who uploaded the document. Steps to reproduce: - Upload a PDF invoice of an existing customer - Send the PDF to OCR - Reload the page Issue: Observe the Salesperson field: it is set to the Public User. Analysis: This occurs because when the partner is filled in, the compute of the salesperson will trigger. On SaaS this happens through the extraction c
Original PR description
When a customer invoice is digitized through OCR, the salesperson may be set to the "Public User" instead of the internal user who uploaded the document. Steps to reproduce: - Upload a PDF invoice of an existing customer - Send the PDF to OCR - Reload the page Issue: Observe the Salesperson field: it is set to the Public User. Analysis: This occurs because when the partner is filled in, the compute of the salesperson will trigger. On SaaS this happens through the extraction completion webhook a public route processed in sudo. that does not change the current user (public user). As self.env.user is the fallback of the compute, it may be set as salesperson. opw-6296330 Forward-Port-Of: odoo/odoo#279724
16 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
Resolved issues and error corrections
The Barcode app now correctly shows the instruction to scan a package when package handling is enabled. This helps warehouse users find transfers from package barcodes directly from the welcome screen and avoids confusion during scanning workflows.
Original PR description
When packages are enabled, the barcode scanner welcome screen does not display the instructions for scanning a package. ## Steps to reproduce - Enable `Packages` in Inventory settings. - Open the Barcode application. - Observe the instructions listed on the welcome/landing page. - Notice that the instruction `Scan a package to find a transfer` is missing, even though packages are enabled. ## Issue The MainMenu component has a getter barcodeHomeHelper that checks this.packageEnabled to construct the barcode scanner helper bullet points. However, during setup, the configuration value was incorrectly assigned to this.packagesEnabled. ## Fix Correct the variable name typo in the main menu setup so that the package related instructions are correctly displayed when packaging is enabled. [^1] [^1]:  Forward-Port-Of: odoo/enterprise#124658
AI chat now works more reliably when users move into areas such as Shopfloor where view options may not be available. The change prevents an error that could stop users from sending AI chat messages in those contexts.
Original PR description
### Issue When AI chat is used in views where `config.viewSwitcherEntries` is not initialized, sending a message raises a `TypeError` because the code attempts to call `.map()` on an undefined value.…
### Issue
When AI chat is used in views where `config.viewSwitcherEntries` is not initialized, sending a message raises a `TypeError` because the code attempts to call `.map()` on an undefined value.
### Steps to Reproduce
[Video](https://drive.google.com/file/d/1i7kWDnmv4mebGtf1to12BNHCG5omBTXl/view?usp=sharing)
1. Click the **Ask AI** button.
2. Open the AI chat.
3. Keep it open and navigate to the **Shopfloor** app.
4. Send a message in the AI chat.
### Error
```text
TypeError: Cannot read properties of undefined (reading 'map')
at WithSearch.getCurrentViewInfo
```
### Fix
Safely handle cases where `config.viewSwitcherEntries` is undefined by using optional chaining and falling back to an empty array.
**Before**
```js
result.available_view_types = config.viewSwitcherEntries.map((v) => v.type);
```
**After**
```js
result.available_view_types =
config.viewSwitcherEntries?.map((v) => v.type) || [];
```
opw-6414684
Forward-Port-Of: odoo/enterprise#126738
Forward-Port-Of: odoo/enterprise#125821This fixes payroll behavior so refund payslips are not recalculated when a user clicks Compute. This helps preserve the intended refund amounts while still allowing users to clear lines with Reset when needed.
Original PR description
When a payslip is a refund payslip, we don't want to compute it if we click on "compute". The lines can still be reset when clicking on "reset" Forward-Port-Of: odoo/enterprise#126845
Peruvian electronic invoices now calculate down payment amounts consistently when withholding tax is involved. This prevents XML total mismatches that could cause validation or reporting issues, and avoids referencing cancelled down payment invoices.
Original PR description
### Issue: When an invoice has a down payment on a line with a withholding tax, the `PrepaidPayment/PaidAmount` in the UBL XML included the withholding tax amount, causing a mismatch with…
### Issue: When an invoice has a down payment on a line with a withholding tax, the `PrepaidPayment/PaidAmount` in the UBL XML included the withholding tax amount, causing a mismatch with `LegalMonetaryTotal/PrepaidAmount` which correctly excludes it ### Cause: `PrepaidPayment/PaidAmount` was set directly from `prepayment_move.amount_total`, which includes all taxes `LegalMonetaryTotal/PrepaidAmount` uses `_aggregate_base_line_tax_details` to exclude withholding taxes, but this was not applied to the `PrepaidPayment` node ### Fix: Using `prepayment_move.amount_total` directly includes all taxes and does not match the rounding logic of `LegalMonetaryTotal` Instead, `_aggregate_base_line_tax_details` is used with the same `total_grouping_function` as `LegalMonetaryTotal`, ensuring both nodes use the same rounding logic and exclude withholding taxes Reversed down payment moves are also excluded from `AdditionalDocumentReference` to avoid referencing cancelled invoices ### Steps to reproduce: - Install `l10n_pe_edi` and `sale_management` with demo data - Switch to the PE company - Create and confirm a Sale Order (Customer: PE Company, Product: Any, Unit Price: 200, Taxes: VAT 18% and 3% IGV Withholding) - Create, confirm and pay a Down Payment Invoice (Fixed: 28.92) - Go back to the SO and create the Regular Invoice - Confirm it and click Process Now - Open the EDI Document tab and download the XML Before the fix, the sum of `PrepaidPayment/PaidAmount` did not match `LegalMonetaryTotal/PrepaidAmount` opw-6273903 Forward-Port-Of: odoo/enterprise#126776 Forward-Port-Of: odoo/enterprise#121733
Australian payroll data updates now include salary rule category information. This prevents scheduled payroll updates from failing when new categories are added, helping payroll maintenance run more reliably.
Original PR description
ir_cron_update_payroll_data updates the payroll data including rules but fails if a new rule category is introduced. This commit adds the hr_salary_rule_category_data file to the list of data files to update. task-6351929 Forward-Port-Of: odoo/enterprise#122392
Appointment cancellation emails are now sent in the language of the customer who booked the appointment, matching the behavior of confirmation emails. This prevents customers from receiving cancellation notices in the organizer's language and improves communication clarity.
Original PR description
**Problem:** When an appointment is cancelled, the cancellation email is always sent in the organizer's (staff member's) language, ignoring the booking customer's language. The booking confirmation,…
**Problem:**
When an appointment is cancelled, the cancellation email is always sent in the organizer's (staff member's) language, ignoring the booking customer's language. The booking confirmation, by contrast, is correctly localized.
**Steps to reproduce:**
1. Set a contact's language to a non-default one (e.g. Romanian).
2. Book an appointment for that contact (they are the booker/attendee).
3. Cancel the appointment.
4. The customer received the confirmation in Romanian but the cancellation email arrives in English.
**Current behavior:**
The cancellation email is rendered in the organizer's language.
**Expected behavior:**
The cancellation email is rendered in the booking customer's language, like the confirmation/invitation email.
**Cause of the issue:**
The cancellation uses `appointment_canceled_mail_template`, whose `lang` is `{{ object.partner_id.lang }}`. On `calendar.event`, `partner_id` is `related='user_id.partner_id'`, i.e. the organizer, not the customer. The template is posted once per event (via `_track_template`), so its single rendering language applies to every recipient, including attendees whose own language differs. The confirmation email is unaffected because it is the per-attendee `attendee_invitation_mail_template` (model `calendar.attendee`), rendered once per attendee in that attendee's language.
**Fix:**
Deriving the language from `appointment_booker_id` makes the cancellation consistent with the other appointment mails, which are meant for the person who booked the meeting. It falls back to `partner_id` when there is no booker (e.g. an event not created through the appointment flow), preserving the previous behavior in that case.
opw-6323179
Forward-Port-Of: odoo/enterprise#125927
Forward-Port-Of: odoo/enterprise#124116Code cleanup and technical improvements
The IoT connection layer was updated to use Odoo’s newer plugin approach, making the underlying code more flexible and easier to maintain. This should not change day-to-day workflows, but it helps improve reliability and future development for IoT, Point of Sale IoT, and self-ordering IoT features.
Original PR description
OWL3 introduced the concept of Plugins, which can be used similarly to services but are more flexible and have stronger typing. This commit does the following: - Converts the longpolling and websocket services to plugins. - Converts the IoT HTTP service to a plugin, but retains a service wrapper as it is still used in many places. - Use the plugin instead of the service for ORM and notification in all IoT code.
This update keeps German POS certification and Mexican POS e-invoicing modules aligned with a recent loyalty system refactor. It renames an internal coupon reference to a card reference, reducing inconsistency and helping prevent future maintenance issues without changing business workflows.
Original PR description
As part of the pos_loyalty refactor, the pos_orderline.coupon_id field was renamed to pos_orderline.card_id. This PR renames wherever the field is used to keep everything consistent Community PR-[#274951](https://github.com/odoo/odoo/pull/274951) Upgrade PR-[#10948](https://github.com/odoo/upgrade/pull/10948)
9 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
Resolved issues and error corrections
The Vietnamese financial reports now classify short-term loan balances under held-to-maturity investments instead of cash equivalents. This aligns the balance sheet with Circular 99/2025, improving regulatory accuracy for companies using Vietnam localization reports.
Original PR description
### Expected behavior: As per circular 99/2025, short-term loan (12831) balance is required to fall under Held to Maturity Investment (Code 123) instead of 112, translated: ``` Short-term held-to-maturity investments (Code 123): includes held-to-maturity investments with a remaining term of 12 months or less from the end of the accounting period, such as term deposits, bonds, commercial paper, loans, and other debt securities. This item does not include held-to-maturity investments that have been presented in the item “Cash equivalents” ``` ### Steps to reproduce: Install `l10n_vn_reports` module ### Fix: PO validated: Update the Balance Sheet code formula for the 12381 account opw-6413120 Forward-Port-Of: odoo/enterprise#126763
Shared Knowledge articles with transcript blocks now load correctly for portal users instead of showing a blank page. Portal users can view the transcript component, while recording remains unavailable to them.
Original PR description
Steps to reproduce: 1. Open knowledge 2. Share an article with a portal user 3. Use /transcript inside that article 4. Open it as a portal. ----> The page cannot load, it's a white screen with an error in the console Technical ---------- Voice transcription component can also be visible in Knowledge for portal users with permissions. Currently, the transcription component requested data from the server inaccessible to the portal. This change updates the transcription component such that portal users can view and use the transcript component while keeping the recording feature inaccessible. Task-6320461
Users can now successfully re-invite a portal member to a shared Documents folder after that member's previous access expired. This prevents misleading success messages and ensures the invited person regains access as expected.
Original PR description
Sharing a folder with a portal user with an expiration date cannot be done again after the access has expired. The sharing dialog reports success, but the user does not get access and is no longer…
Sharing a folder with a portal user with an expiration date cannot be done again after the access has expired. The sharing dialog reports success, but the user does not get access and is no longer listed. ### Steps to reproduce - In Documents, share a folder with a portal user and set an expiration date. - Wait until the expiration date has passed. - Share the same folder with the same user again from the invite box. => The dialog says the member was added, but the user has no access and does not appear under "People with access". ### Cause The invite box has no expiration field. When re-inviting a user, it updates the existing `documents.access` record and passes `None` for the expiration, which keeps the old `expiration_date`. If that date is already in the past, the user remains expired even though the invite reports success. ### Fix Pass `False` instead of `None` when inviting a member so the existing record's expiration date is cleared. Re-inviting an expired user now restores access. Setting an expiration from the "People with access" list is unchanged. opw-6387559
Shipments sent through Sendcloud now always use the expected English VAT label in customs information. This prevents parcels from being rejected when a country uses a local tax label, such as Austria's USt, improving reliability for international shipping.
Original PR description
Issue ----- For some countries, the `vat_label` field is hardcoded to some value other than the English name `VAT`. This leads to shipments being rejected by Sendcloud as only the English name is accepted. For Austria for example, it is hardcoded as `USt`: https://github.com/odoo/odoo/blob/3aec1c317aad547b1ad0c85a21cc53f735c5cbfd/odoo/addons/base/data/res_country_data.xml#L79-L86 Sendcloud API doc: https://sendcloud.dev/api/v2/parcels/create-a-parcel-or-parcels#body-one-of-0-parcel-customs-information-tax-numbers-sender-items-name ----- Ticket: opw-6445962
The Timesheet and Planning Analysis report now includes planning slots for employees without a fixed working schedule. This ensures flexible employees are represented accurately in planning and workload reporting.
Original PR description
Steps to reproduce: ------------------- 1. Install project_timesheet_forecast. 2. Create a fully flexible employee (without a working schedule). 3. Create a planning slot. 4. Open the Timesheet/planning Analysis report. Issue: ------ Planning slots for fully flexible employees are not included in the report. Cause: ------ https://github.com/odoo/enterprise/blob/7d4b43cfa1934856d41992cbe8242eaf62575c2c/project_timesheet_forecast/report/timesheet_forecast_report.py#L142-L161 The report assumes every resource has a working schedule and only considers resources with a resource calendar. As a result, resources without a calendar are excluded from the report. Solution: --------- Handle resources without a working schedule separately so that planning slots for fully flexible employees are also included in the report. opw-6361571 Forward-Port-Of: odoo/enterprise#126682 Forward-Port-Of: odoo/enterprise#125072
Uploading a document from a contact now sends it to the intended My Drive/All workspace instead of reusing the last folder selected in Documents. This prevents files from being misplaced, reducing confusion and cleanup for users who work across Contacts and Documents.
Original PR description
Steps to reproduce ================== 1. Open Documents. 2. Select Finance. 3. Return to the home page and open the Contacts app. 4. Open any contact. 5. Click the Documents stat button. 6. Upload a document. Issue ===== The document is uploaded to the Finance folder instead of My Drive. Reason ====== When uploading a document using the upload button, we use `currentFolderAccessToken` to determine the destination folder. When opening the Documents view from a contact, `searchpanel_default_folder_id` is set to `False` so that documents are uploaded to the `All` workspace. However, when the search model is loaded, we do not reset `currentFolderAccessToken` when `folder_id` is `False`, causing the previously selected folder (Finance) to be reused. Task-6352242 Forward-Port-Of: odoo/enterprise#123285
2 changes
Resolved issues and error corrections
MyInvois expects the CountrySubentityCode to be fixed as '17' for customers located outside Malaysia, since Malaysian state codes don't apply to them. Set it to '17' instead of the raw state name for non-Malaysian customers. task-6388521 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280550
Original PR description
MyInvois expects the CountrySubentityCode to be fixed as '17' for customers located outside Malaysia, since Malaysian state codes don't apply to them. Set it to '17' instead of the raw state name for non-Malaysian customers. task-6388521 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#280550
Following the change in https://github.com/odoo/odoo/pull/225544 we added a timezone to `jules_emp` to avoid discrepancies when running the tests with and without demo data. However the `resource_calendar_id` of the contract used by this employee didn't have a timezone leading to similar errors in other tests. This commit adds the same timezone to the calendar used by test employee to prevent the errors. [error-938862 ](https://runbot.odoo.com/odoo/error/938862) --- I confirm I h
Original PR description
Following the change in https://github.com/odoo/odoo/pull/225544 we added a timezone to `jules_emp` to avoid discrepancies when running the tests with and without demo data. However the `resource_calendar_id` of the contract used by this employee didn't have a timezone leading to similar errors in other tests. This commit adds the same timezone to the calendar used by test employee to prevent the errors. [error-938862 ](https://runbot.odoo.com/odoo/error/938862) --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
1 change
Resolved issues and error corrections
Steps to reproduce: - Install pos_loyalty and pos_online_payment. - Configure a nominative loyalty program (trigger=auto, applies_on=both) awarding points, and an online payment method. - In the PoS, add a first-time customer and a product, then pay the order with the online payment method. Issue: The customer earns no point, and no loyalty.card is even created for a first-time customer. Orders paid in cash are not affected. The loss is intermittent for customers whose card is already cac
Original PR description
Steps to reproduce: - Install pos_loyalty and pos_online_payment. - Configure a nominative loyalty program (trigger=auto, applies_on=both) awarding points, and an online payment method. - In the PoS,…
Steps to reproduce: - Install pos_loyalty and pos_online_payment. - Configure a nominative loyalty program (trigger=auto, applies_on=both) awarding points, and an online payment method. - In the PoS, add a first-time customer and a product, then pay the order with the online payment method. Issue: The customer earns no point, and no loyalty.card is even created for a first-time customer. Orders paid in cash are not affected. The loss is intermittent for customers whose card is already cached. Cause: Once paid, the order is rebuilt from the server data and re-selected. `pos.order._export_for_ui` has no `couponPointChanges` field, so the rebuilt order starts with an empty one. `set_order` does schedule a recompute, but it does not await it, and `_postPushOrderResolve` reads `couponPointChanges` right after, before the recompute settles, hence sending nothing to `confirm_coupon_programs`. A first-time customer additionally needs a `fetchLoyaltyCard` RPC, so the recompute can never win the race. Fix: Wait for the pending recompute before compiling the coupon data. `Mutex.getUnlockedDef` is used rather than awaiting a new `_updateRewards` call, so that no extra recompute is queued on an order that is already finalized and saved on the server. 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