Daily updates from Odoo
Thursday, August 6, 2026
71 changes
14 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
Resolved issues and error corrections
Replacing a document in the Sign app now keeps multiple signature fields correctly linked to the same signer. This prevents duplicate signer entries and helps users avoid confusion or incorrect signing assignments after updating a document.
Original PR description
### Description of the issue/feature this PR addresses: This PR fixes an issue in the Sign module where replacing a document breaks the link between a signer and their multiple signature fields,…
### Description of the issue/feature this PR addresses: This PR fixes an issue in the Sign module where replacing a document breaks the link between a signer and their multiple signature fields, causing Odoo to erroneously generate separate signers for each individual field. ### Current behavior before PR: When a document with multiple signature fields assigned to the same person is replaced, the _copy_sign_items_to function duplicates the sign.item records. During this duplication process, Odoo duplicates the old responsible_ids, creating copies with new ids. These new copies overwrite the old responsible_ids, ensuring that the newly created sign_items have entirely new responsible_ids. Because a shared responsible_id is the primary key Odoo uses to group multiple signature items under a single signer, this change in ID causes the system to lose the grouping. As a result, Odoo treats each copied field as belonging to a completely new, separate signer. _Note_: Because of the limitation mentioned before, any responsible_id that is passed through the copy function, and thereby the copy_data function, is overwritten with new ids. The only work-around then is to update the responsible_id value attached to the new_sign_item after the copy_data function has completed and the new_sign_item has been created. ### Desired behavior after PR is merged: The original responsible_id is explicitly carried over and assigned to the newly copied sign.item immediately after the copy operation completes. This ensures the copied signature fields retain their original role IDs and grouping, keeping them correctly assigned to the single original signer. opw-6354334 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/enterprise#123628
Peruvian electronic invoices now calculate down payment amounts consistently when withholding taxes are involved. This prevents mismatches in submitted XML totals and avoids referencing cancelled down payment invoices, reducing validation issues for affected 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
This 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
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
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
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
15 changes
Enhancements to existing features
Bank synchronization now recognizes a new type of temporary error from Odoofin without marking the bank connection as failed. This helps avoid unnecessary disruption for users when an issue does not require stopping the connection.
Original PR description
Odoofin now sends a 'non_blocking_error' error response to indicate that the state on account.online.link shouldn't be set to error. In this commit, we start using it. Task ID: 6358809 Forward-Port-Of: odoo/enterprise#126737 Forward-Port-Of: odoo/enterprise#123287
Resolved issues and error corrections
Replacing a Sign document now keeps multiple signature fields assigned to the same signer instead of splitting them into separate signers. This prevents confusion and extra manual cleanup when updating documents that are already configured for signing.
Original PR description
### Description of the issue/feature this PR addresses: This PR fixes an issue in the Sign module where replacing a document breaks the link between a signer and their multiple signature fields,…
### Description of the issue/feature this PR addresses: This PR fixes an issue in the Sign module where replacing a document breaks the link between a signer and their multiple signature fields, causing Odoo to erroneously generate separate signers for each individual field. ### Current behavior before PR: When a document with multiple signature fields assigned to the same person is replaced, the _copy_sign_items_to function duplicates the sign.item records. During this duplication process, Odoo duplicates the old responsible_ids, creating copies with new ids. These new copies overwrite the old responsible_ids, ensuring that the newly created sign_items have entirely new responsible_ids. Because a shared responsible_id is the primary key Odoo uses to group multiple signature items under a single signer, this change in ID causes the system to lose the grouping. As a result, Odoo treats each copied field as belonging to a completely new, separate signer. _Note_: Because of the limitation mentioned before, any responsible_id that is passed through the copy function, and thereby the copy_data function, is overwritten with new ids. The only work-around then is to update the responsible_id value attached to the new_sign_item after the copy_data function has completed and the new_sign_item has been created. ### Desired behavior after PR is merged: The original responsible_id is explicitly carried over and assigned to the newly copied sign.item immediately after the copy operation completes. This ensures the copied signature fields retain their original role IDs and grouping, keeping them correctly assigned to the single original signer. opw-6354334 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/enterprise#123628
Peruvian electronic invoices now calculate down payment amounts consistently when withholding tax is involved. This prevents mismatched XML totals and avoids referencing cancelled down payment invoices, reducing the risk of rejected or incorrect electronic documents.
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
Click and collect rental orders now check availability only in the warehouse selected by the customer. This prevents items stored in one pickup location from incorrectly blocking rentals from another location, reducing false out-of-stock 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#126545 Forward-Port-Of: odoo/enterprise#124969
Opening bank reconciliation from a direct link or bookmark now keeps the same automation behavior as opening it from the Accounting dashboard. Matching rules are applied automatically, and upload options stay hidden for journals connected to online bank feeds, reducing manual cleanup and confusion.
Original PR description
### Issue: When accessing the Bank Reconciliation view directly via URL or bookmark, auto-matching with reconciliation models may not trigger and the upload button may be visible on synchronized…
### Issue: When accessing the Bank Reconciliation view directly via URL or bookmark, auto-matching with reconciliation models may not trigger and the upload button may be visible on synchronized journals ### Cause: `_action_open_bank_reconciliation_widget` injects two context keys: - `auto_statement_processing`: triggers auto-reconciliation on statement creation - `bank_statements_source`: hides the upload button for synchronized journals When the view is accessed directly, these keys are not present, causing the UI to ignore them `auto_statement_processing` is now set directly in the user context via `onWillRender`/`onWillDestroy` in `BankRecKanbanController` `bank_statements_source` requires an ORM call to fetch the journal's value and is resolved via `fetchBankStatementsSourceInto` on startup Notes: The fix for `bank_statements_source` was added opportunistically while addressing `auto_statement_processing` Steps to reproduce: - Install `accountant` with demo data - Duplicate the Bank Journal and set Bank Feeds to Online Synchronization - Open the Accounting Dashboard and open the Bank (copy) - Create a transaction (Label: Test, any amount) and click Add & Close - In the 3 dots menu, choose Manage Models - Create a Reconciliation Model (Label contains: Test, Lines: any account, default values) - Click Automate - Go back to the Bank Reconciliation page and verify: -- The transaction is reconciled automatically -- No Upload button is displayed - Create a new transaction, it should be reconciled automatically - Copy the URL and open it in a new tab - Create a new transaction Before the fix, the transaction is not reconciled and the Upload button is present opw-6391107 Forward-Port-Of: odoo/enterprise#126706 Forward-Port-Of: odoo/enterprise#126359
A bug in accounting reports allowed users to trigger the same line expansion multiple times by clicking quickly or using a slow connection. The fix blocks repeat expansion requests while the first one is still loading, so report lines fold and unfold reliably.
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
Automatic bank reconciliation rules now use more reliable text matching and consider transaction amounts and direction when creating or reusing rules. Users should see more relevant reconciliation suggestions for the selected journal and fewer incorrect or stale automatic rules.
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
The Vietnam reporting module now places short-term loan balances under held-to-maturity investments, aligning the balance sheet with Circular 99/2025. This helps businesses produce compliant Vietnamese financial reports without manual reclassification.
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
Cohort reports now calculate average retention based on the size of each cohort rather than treating every cohort equally. This gives business users a more representative view of overall retention, especially when comparing groups of very different sizes.
Original PR description
Steps to reproduce: - Open a retention cohort with cohorts of different sizes - Compare the average row with the overall retained population Issues: The average row gives every cohort the same weight. A cohort of one record at 0% retention and a cohort of nine records at 100% retention therefore displays 50% instead of 90%. Solution: Weight each cohort percentage by its initial cohort value.
The barcode app now correctly keeps only one delivery line selected when an operation includes both packaged and unpackaged products. This prevents confusion for warehouse users and reduces the risk of processing the wrong line during barcode-based deliveries.
Original PR description
**Steps to reproduce:** - Enable "Move Entire Packages" setting on deliveries - Make a product A, that has a package P1, on hand qty of 1 - Make product B that don't have a package, but on hand qty…
**Steps to reproduce:** - Enable "Move Entire Packages" setting on deliveries - Make a product A, that has a package P1, on hand qty of 1 - Make product B that don't have a package, but on hand qty of 1 - Make a delivery that has both of those products, requested qty of 1 for both - Mark it as todo - Go to the barcode app, select the delivery - Select the line with product B - Select the line with product A --> The line with product B is not unselected **Why the fix:** When we have a mix of packaged products and products without a package on the same operation, they are handled separately. The products without a package are handled in https://github.com/odoo/enterprise/blob/98c79af3fb6cb354f46fd2f58e642a72a9271443/stock_barcode/static/src/models/barcode_model.js#L388-L392 that calls https://github.com/odoo/enterprise/blob/98c79af3fb6cb354f46fd2f58e642a72a9271443/stock_barcode/static/src/models/barcode_model.js#L1277-L1284 But as you can see, there are no mention of the selected package line, which is stored in **this.lastScanned.packageId**. As we do not touch this variable, the selected package line stays selected. The same is true for the other way around, when we select a package line we call https://github.com/odoo/enterprise/blob/98c79af3fb6cb354f46fd2f58e642a72a9271443/stock_barcode/static/src/models/barcode_model.js#L394-L398 This function does not care for the **selectedLineVirtualId** which represents the selected line without a package. To avoid this and make it so that only one line is selected even if they have different package, we now set the corresponding value to false to unselect the other line in all situation. This is basically how it's done in https://github.com/odoo/enterprise/blob/98c79af3fb6cb354f46fd2f58e642a72a9271443/stock_barcode/static/src/models/barcode_model.js#L1202-L1208 to unselect every line regardless of packages. opw-6266203 Forward-Port-Of: odoo/enterprise#126790 Forward-Port-Of: odoo/enterprise#122038
This fixes an issue where Tyro card payments with a surcharge could sometimes validate before the surcharge was added to the point-of-sale order. Businesses using Tyro payments should now see more reliable order totals and surcharge recording at checkout.
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
Users can now disconnect a Belgian CodaBox connection using either the fiduciary password or a valid IAP token. This fixes a client-side gap so the revocation process works as already supported by the server.
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 stops approval requests from trying to send notifications while temporary form data is being recalculated. It prevents crashes when users edit forms that include approval request links, especially for approved or refused requests, making Studio-customized workflows more reliable.
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
The Timesheet Assistant now includes very small calendar events by adding their time to a matching larger event instead of ignoring them. This helps suggested timesheets reflect the full time spent, improving accuracy for users who rely on automated suggestions.
Original PR description
## Previous Behavior Before this PR: When events were to small to suggestion Timesheet Assistant would completely discard these events. This lead to a suggestion haveing a lower total time than it should. ## New Expected Behavior After this PR: When an event is too small to suggest and shares its name and group with one or more larger event, the duration of the smaller event is added to the last event with the same name and groupe. task-[6452987](https://www.odoo.com/odoo/project/4105/tasks/6452987)
UrbanPiper point-of-sale orders with tax-included prices now calculate the per-item price correctly when customers order more than one of the same product. This prevents overcharging or incorrect order totals for affected online orders.
Original PR description
Steps to reproduce: --- - Configure a Point of Sale with UrbanPiper credentials. - Create a product priced at 100 with a 5% GST (Tax Included). - Sync the product with UrbanPiper. - Place an online order with a quantity greater than 1. Issue: --- - `total_with_tax` was incorrectly treated as the unit price for multi-quantity tax-included orders. Fix: --- - Calculate the unit price by dividing `total_with_tax` by the ordered quantity before creating the POS order line. task-6427634 Forward-Port-Of: odoo/enterprise#126723 Forward-Port-Of: odoo/enterprise#125989
10 changes
Enhancements to existing features
Bank synchronization now recognizes a new type of warning from Odoo’s financial connection service that should not stop the connection. This helps avoid unnecessary error states, keeping bank links active when an issue is informational or temporary rather than blocking.
Original PR description
Odoofin now sends a 'non_blocking_error' error response to indicate that the state on account.online.link shouldn't be set to error. In this commit, we start using it. Task ID: 6358809 Forward-Port-Of: odoo/enterprise#126737 Forward-Port-Of: odoo/enterprise#123287
Odoo now checks whether a payment or batch payment exceeds the maximum amount allowed by the connected financial institution before attempting to send it. This helps prevent failed payment submissions and gives businesses earlier visibility when a bank-imposed limit blocks a payment.
Original PR description
Before trying to initiate payments through Odoo/Odoofin, we should check that the total amount for the (batch) payment does not exceed the maximum payment amount allowed by the institution (some Powens institutions introduced that limit). task-6310729 Forward-Port-Of: odoo/enterprise#126699 Forward-Port-Of: odoo/enterprise#121513
Resolved issues and error corrections
UrbanPiper orders with tax-included products now calculate the per-item price correctly when customers buy more than one of the same item. This prevents inflated Point of Sale order totals and improves billing accuracy for online orders.
Original PR description
Steps to reproduce: --- - Configure a Point of Sale with UrbanPiper credentials. - Create a product priced at 100 with a 5% GST (Tax Included). - Sync the product with UrbanPiper. - Place an online order with a quantity greater than 1. Issue: --- - `total_with_tax` was incorrectly treated as the unit price for multi-quantity tax-included orders. Fix: --- - Calculate the unit price by dividing `total_with_tax` by the ordered quantity before creating the POS order line. task-6427634 Forward-Port-Of: odoo/enterprise#126723 Forward-Port-Of: odoo/enterprise#125989
Replacing a document in Odoo Sign now keeps multiple signature fields correctly linked to the same signer. This prevents duplicate signer entries and helps ensure signing workflows remain accurate after a document is updated.
Original PR description
### Description of the issue/feature this PR addresses: This PR fixes an issue in the Sign module where replacing a document breaks the link between a signer and their multiple signature fields,…
### Description of the issue/feature this PR addresses: This PR fixes an issue in the Sign module where replacing a document breaks the link between a signer and their multiple signature fields, causing Odoo to erroneously generate separate signers for each individual field. ### Current behavior before PR: When a document with multiple signature fields assigned to the same person is replaced, the _copy_sign_items_to function duplicates the sign.item records. During this duplication process, Odoo duplicates the old responsible_ids, creating copies with new ids. These new copies overwrite the old responsible_ids, ensuring that the newly created sign_items have entirely new responsible_ids. Because a shared responsible_id is the primary key Odoo uses to group multiple signature items under a single signer, this change in ID causes the system to lose the grouping. As a result, Odoo treats each copied field as belonging to a completely new, separate signer. _Note_: Because of the limitation mentioned before, any responsible_id that is passed through the copy function, and thereby the copy_data function, is overwritten with new ids. The only work-around then is to update the responsible_id value attached to the new_sign_item after the copy_data function has completed and the new_sign_item has been created. ### Desired behavior after PR is merged: The original responsible_id is explicitly carried over and assigned to the newly copied sign.item immediately after the copy operation completes. This ensures the copied signature fields retain their original role IDs and grouping, keeping them correctly assigned to the single original signer. opw-6354334 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/enterprise#123628
Bank reconciliation now behaves correctly even when opened directly from a saved URL or bookmark. Automatic matching continues to run and upload options stay hidden for bank journals that use online synchronization, reducing manual work and avoiding confusing actions.
Original PR description
### Issue: When accessing the Bank Reconciliation view directly via URL or bookmark, auto-matching with reconciliation models may not trigger and the upload button may be visible on synchronized…
### Issue: When accessing the Bank Reconciliation view directly via URL or bookmark, auto-matching with reconciliation models may not trigger and the upload button may be visible on synchronized journals ### Cause: `_action_open_bank_reconciliation_widget` injects two context keys: - `auto_statement_processing`: triggers auto-reconciliation on statement creation - `bank_statements_source`: hides the upload button for synchronized journals When the view is accessed directly, these keys are not present, causing the UI to ignore them `auto_statement_processing` is now set directly in the user context via `onWillRender`/`onWillDestroy` in `BankRecKanbanController` `bank_statements_source` requires an ORM call to fetch the journal's value and is resolved via `fetchBankStatementsSourceInto` on startup Notes: The fix for `bank_statements_source` was added opportunistically while addressing `auto_statement_processing` Steps to reproduce: - Install `accountant` with demo data - Duplicate the Bank Journal and set Bank Feeds to Online Synchronization - Open the Accounting Dashboard and open the Bank (copy) - Create a transaction (Label: Test, any amount) and click Add & Close - In the 3 dots menu, choose Manage Models - Create a Reconciliation Model (Label contains: Test, Lines: any account, default values) - Click Automate - Go back to the Bank Reconciliation page and verify: -- The transaction is reconciled automatically -- No Upload button is displayed - Create a new transaction, it should be reconciled automatically - Copy the URL and open it in a new tab - Create a new transaction Before the fix, the transaction is not reconciled and the Upload button is present opw-6391107 Forward-Port-Of: odoo/enterprise#126706 Forward-Port-Of: odoo/enterprise#126359
Automatic document fields now keep numeric zero values instead of replacing them with blanks, and decimal numbers are rounded consistently. This helps prevent incorrect values from appearing in salary contract and signature workflows.
Original PR description
Before this commit, falsy values were always set to '' even when they corresponded to numbers. Moreover, floats values were not rounded.
Changing an employee payslip to a payroll structure that does not use worked day lines now clears old worked day data instead of leaving outdated values. Belgian payroll reporting was also adjusted so relevant off-cycle pay remains included even when no worked day lines are present.
Original PR description
hr_payroll: Previously, changing to a structure with `use_worked_day_lines = False` (e.g., 13th month) caused `valid_slips` to be empty and return early, leaving stale worked day lines on the payslip. This commit resets the worked_days_lines before filtering for valid payslips. l10n_be_hr_payroll: After fixing the payroll bug and clearing worked_days_lines correctly, the DMFA report fails to correctly consider remunerations since the off-cycle payslips do not have worked_days_lines anymore. This commit backports a fix from odoo/enterprise#106689 to not skip remunerations for payslips with no worked days lines. task-6401942 Forward-Port-Of: odoo/enterprise#124986
This fixes a payroll reporting issue that could incorrectly block payment report creation around midnight in some time zones. Payroll date defaults are now calculated consistently across affected country-specific payroll modules, improving reliability for automated tests and users working outside UTC.
Original PR description
### Steps to reproduce: - Set the environment timezone (`env.tz`) to a timezone ahead of UTC (e.g., Europe/Brussels) - Run the enterprise tests (L10n standalone, Single app, or Multi l10n) during the…
### Steps to reproduce: - Set the environment timezone (`env.tz`) to a timezone ahead of UTC (e.g., Europe/Brussels) - Run the enterprise tests (L10n standalone, Single app, or Multi l10n) during the late evening in UTC (e.g., 23:00 UTC) > UserError: The Payment Date cannot be later than the Value Date, please make sure that the correct dates are set ### Cause of Issue: In the payroll payment report wizards, a race condition occurs around midnight due to mismatched timezone context evaluations between different date fields. The `effective_date` field (defined in the base hr_payroll module) derives its default value using `fields.Date.context_today`, which correctly applies the client's timezone offset to the current server time. https://github.com/odoo/enterprise/blob/54b7035b4cb6b9427092a3eebe73f2bee7f2ae09/hr_payroll/wizard/hr_payroll_payment_report_wizard.py#L23-L26 However, `l10n_sa_wps_value_date` (and similar date fields in other localizations like AU, HK, AE) derives its default value using `fields.Date.today()`, which strictly relies on the server's UTC time. https://github.com/odoo/enterprise/blob/54b7035b4cb6b9427092a3eebe73f2bee7f2ae09/l10n_sa_hr_payroll/wizard/hr_payroll_payment_report_wizard.py#L13-L14 When the nightly Runbot builds execute late at night UTC time, the environment timezone frequently crosses midnight into "tomorrow" while the server time is still on "today". Because of this offset, `effective_date` rolls over to tomorrow, but `l10n_sa_wps_value_date` evaluates as today + 1 day (which is also tomorrow). The validation check `effective_date >= l10n_sa_wps_value_date` evaluates to True. https://github.com/odoo/enterprise/blob/54b7035b4cb6b9427092a3eebe73f2bee7f2ae09/l10n_sa_hr_payroll/wizard/hr_payroll_payment_report_wizard.py#L93-L94 ### Fix: Standardize the default date computations to ensure they are all evaluated within the same timezone context and prevent the midnight timezone rollover discrepancy. runbot-937793 Forward-Port-Of: odoo/enterprise#125364
Timesheet assistant suggestions now use the actual event duration instead of calculating time from start and end times. This ensures planning shifts and calendar events suggest the intended working hours, reducing incorrect timesheet entries.
Original PR description
*_: project_timesheet_forecast, timesheet_grid, timesheet_grid_calendar Previously, the timesheet assistant derived suggested entry durations from an event's start and stop datetimes. This worked for calendar events but produced incorrect suggestions for planning shifts whenever the allocated working hours differed from the overall scheduled time window. This commit introduces an explicit ``duration`` field in assistant events and updates all providers to supply it. Planning slots now use their allocated hours as the event duration, while calendar events expose their existing duration value. The assistant now consistently relies on this field instead of computing the duration from the event time range. As a result, suggested timesheet durations accurately reflect the intended working time for both planning shifts and calendar events. task-6366593 Forward-Port-Of: odoo/enterprise#125108
The Timesheet Assistant now merges selected suggestions that come from the same rule into one natural description, such as “Discussing with A and B” instead of repeating the phrase with semicolons. This makes generated timesheet entries easier to read and reduces manual cleanup for users.
Original PR description
In this task, we improved the Timesheet Assistant by merging descriptions generated from the same rule templates. When multiple suggestions are selected that use the same rule template, the assistant now combines them into a single timesheet description instead of joining with ';' Example: Rule template: `Discussing with $1` Before: Discussing with A; Discussing with B After: Discussing with A and B Task-6348575
11 changes
Enhancements to existing features
Bank synchronization now recognizes advisory errors from Odoofin that should not interrupt the connection. This helps avoid unnecessarily marking bank links as failed when the issue does not block synchronization.
Original PR description
Odoofin now sends a 'non_blocking_error' error response to indicate that the state on account.online.link shouldn't be set to error. In this commit, we start using it. Task ID: 6358809 Forward-Port-Of: odoo/enterprise#126737 Forward-Port-Of: odoo/enterprise#123287
Odoo now checks whether a payment or batch payment exceeds the maximum amount allowed by the connected financial institution before trying to process it. This helps prevent failed payment attempts and gives businesses earlier warning when a bank-imposed limit applies.
Original PR description
Before trying to initiate payments through Odoo/Odoofin, we should check that the total amount for the (batch) payment does not exceed the maximum payment amount allowed by the institution (some Powens institutions introduced that limit). task-6310729 Forward-Port-Of: odoo/enterprise#126699 Forward-Port-Of: odoo/enterprise#121513
Resolved issues and error corrections
Online orders from UrbanPiper now calculate the price per item correctly when taxes are included and customers order more than one unit. This prevents inflated POS order totals and helps ensure accurate billing, receipts, and sales reporting.
Original PR description
Steps to reproduce: --- - Configure a Point of Sale with UrbanPiper credentials. - Create a product priced at 100 with a 5% GST (Tax Included). - Sync the product with UrbanPiper. - Place an online order with a quantity greater than 1. Issue: --- - `total_with_tax` was incorrectly treated as the unit price for multi-quantity tax-included orders. Fix: --- - Calculate the unit price by dividing `total_with_tax` by the ordered quantity before creating the POS order line. task-6427634 Forward-Port-Of: odoo/enterprise#126723 Forward-Port-Of: odoo/enterprise#125989
This fixes an issue where clicking a financial report line repeatedly while it was loading could create duplicate expanded rows. Reports now ignore extra rapid clicks during loading, so users can expand and collapse lines reliably even on slower connections.
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
Planning slots for employees without a fixed working schedule now appear in the Timesheet/Planning Analysis report. This ensures reporting is complete for teams using fully flexible work arrangements and avoids missing planned work in analysis.
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
This fix prevents Odoo Studio from crashing when users edit fields that are added dynamically in specific accounting and Dutch reporting views. Instead of showing an error, Studio now handles the situation gracefully and hides technical-only fields that should not be edited.
Original PR description
* = account_invoice_extract, l10n_nl_reports Example of steps: - Install web_studio and `accountant` - Open the corresponding form view in Studio - With debug mode toggle "Show invisible Elements"…
* = account_invoice_extract, l10n_nl_reports Example of steps: - Install web_studio and `accountant` - Open the corresponding form view in Studio - With debug mode toggle "Show invisible Elements" and edit "Invisible" on the second partner_id field - Traceback `normalize()` compares the combined arch without the studio customization to the one with it, in order to compute the smallest possible set of xpaths. To do so, it calls `apply_inheritance_specs` (the low-level function from `odoo.tools.template_inheritance`) directly on the statically combined arch. Some models add or duplicate nodes dynamically in `_get_view()` (Python postprocessing, run after the static view combination). A studio operation can target such a node, since it is what the user actually sees and clicks on. But that node has no counterpart in the purely static combined arch used by `normalize()`, so `apply_inheritance_specs` raises a ValueError. `edit_view()` only catches `ValidationError` to fall back to an un-optimized (but valid) studio arch instead of failing the request. Since the low-level function raises a plain `ValueError` here, that fallback never triggers, and the exception is not caught anywhere. To fix this, we will keep the behavior from version 18.0 and catch the ValueError raised by `apply_inheritance_specs` in `normalize_with_keyed_tree` and re-raise it as a ValidationError, like `ir.ui.view.apply_inheritance_specs` already does elsewhere. This lets `edit_view()`'s existing fallback handle the case gracefully instead of crashing. Additionally, the two models responsible for the dynamically-added nodes described above are fixed at the source. `account_invoice_extract`'s duplicated `partner_id` field and `l10n_nl_reports`'s injected `company_id` field are now marked with `data-used-by`, the same attribute `_add_missing_fields` already sets in `ir_ui_view.py` for the fields it adds. Studio already skip rendering and computing xpaths for any node carrying this attribute (since https://github.com/odoo/enterprise/pull/92862), so these nodes are no longer exposed to the user and can no longer produce a studio operation that `normalize()` is unable to locate. opw-6332911 Forward-Port-Of: odoo/enterprise#122829
This fixes a crash that could happen when approval requests in approved or refused states were linked through a custom Studio field and a form recalculated values. Users can now continue editing affected forms without interruption, and tests now cover all approval request states.
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
This fixes an issue that could stop bulk product imports when subscription-related settings were changed. Restoring the missing logic helps businesses update product catalogs reliably without import errors.
Original PR description
The port https://github.com/odoo/enterprise/commit/68640b5bddf51a8cbf58d3af3628cd4b57e08913 added a call to self._get_confirmed_order_lines() in product.template.write() (on import, when recurring_invoice changes), but the helper itself was never ported to 19.0. Importing products in bulk then fails with AttributeError: 'product.template' object has no attribute '_get_confirmed_order_lines'. Restores the method from master (PR https://github.com/odoo/enterprise/pull/117046) at the end of the ProductTemplate class. Forward-Port-Of: odoo/enterprise#122146
Changing a payslip to a payroll structure that does not use worked day lines now clears old worked day data instead of leaving misleading information behind. Belgian payroll reporting was also adjusted so off-cycle payslips without worked day lines still include the correct remuneration amounts in DMFA reports.
Original PR description
hr_payroll: Previously, changing to a structure with `use_worked_day_lines = False` (e.g., 13th month) caused `valid_slips` to be empty and return early, leaving stale worked day lines on the payslip. This commit resets the worked_days_lines before filtering for valid payslips. l10n_be_hr_payroll: After fixing the payroll bug and clearing worked_days_lines correctly, the DMFA report fails to correctly consider remunerations since the off-cycle payslips do not have worked_days_lines anymore. This commit backports a fix from odoo/enterprise#106689 to not skip remunerations for payslips with no worked days lines. task-6401942 Forward-Port-Of: odoo/enterprise#124986
Users can now revoke a Belgian CodaBox connection using either the fiduciary password or a valid IAP token. This fixes a client-side gap so the revocation process works as already supported by the server.
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
Users can now be re-invited to a shared Documents folder after their previous access expired. This prevents misleading success messages and ensures portal users regain access when invited again.
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 Forward-Port-Of: odoo/enterprise#125140
4 changes
Enhancements to existing features
Odoo now checks whether a payment or batch payment exceeds the maximum amount allowed by the connected financial institution before trying to send it. This helps prevent failed payment attempts and gives businesses earlier feedback when a bank-imposed limit applies.
Original PR description
Before trying to initiate payments through Odoo/Odoofin, we should check that the total amount for the (batch) payment does not exceed the maximum payment amount allowed by the institution (some Powens institutions introduced that limit). task-6310729 Forward-Port-Of: odoo/enterprise#126699 Forward-Port-Of: odoo/enterprise#121513
Resolved issues and error corrections
The Vietnam reports module now places short-term loan balances under held-to-maturity investments in the balance sheet, aligning with Circular 99/2025. This helps businesses produce compliant financial reports with the correct classification.
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
Planning slots for employees without a fixed working schedule are now included in the Timesheet/Planning Analysis report. This ensures teams using fully flexible employees get complete reporting and more accurate workload visibility.
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
Changing a payslip to a type that does not use worked day lines now clears outdated worked day information, preventing incorrect payroll details from remaining. Belgian DMFA reporting was also adjusted so off-cycle payslips without worked day lines still include the right remuneration amounts.
Original PR description
hr_payroll: Previously, changing to a structure with `use_worked_day_lines = False` (e.g., 13th month) caused `valid_slips` to be empty and return early, leaving stale worked day lines on the payslip. This commit resets the worked_days_lines before filtering for valid payslips. l10n_be_hr_payroll: After fixing the payroll bug and clearing worked_days_lines correctly, the DMFA report fails to correctly consider remunerations since the off-cycle payslips do not have worked_days_lines anymore. This commit backports a fix from odoo/enterprise#106689 to not skip remunerations for payslips with no worked days lines. task-6401942 Forward-Port-Of: odoo/enterprise#124986
3 changes
Enhancements to existing features
Online bank synchronization now recognizes a new type of warning from Odoo’s financial connection service that should not stop the connection. This helps avoid unnecessary error statuses and keeps bank links active when the issue is not blocking.
Original PR description
Odoofin now sends a 'non_blocking_error' error response to indicate that the state on account.online.link shouldn't be set to error. In this commit, we start using it. Task ID: 6358809 Forward-Port-Of: odoo/enterprise#123287
Resolved issues and error corrections
Dutch SBR tax return exports now use the Tax Unit VAT number when a tax unit is selected, instead of incorrectly using the individual company's Omzetbelastingnummer. This helps prevent tax authority rejections for fiscal unity filings while keeping the company number behavior for single-company returns.
Original PR description
**Steps to reproduce:** * Install the **Netherlands - SBR** (`l10n_nl_reports_sbr`) and **Netherlands - SBR OB Nummer** (`l10n_nl_reports_sbr_ob_nummer`) modules. * Create two companies with Dutch…
**Steps to reproduce:** * Install the **Netherlands - SBR** (`l10n_nl_reports_sbr`) and **Netherlands - SBR OB Nummer** (`l10n_nl_reports_sbr_ob_nummer`) modules. * Create two companies with Dutch localization. * Go to **Accounting → Configuration → Tax Units** and create a Tax Unit with its own **Tax ID** (e.g. `NL826317558B01`), adding both companies. * On the main company form, fill in the **Omzetbelastingnummer** field (e.g. `123456782B90`). * Go to **Accounting → Reporting → Tax Return**, select the Tax Unit in the filter, and click **XBRL → Download XBRL File**. **Observed behavior:** * The `<xbrli:identifier>` in the exported XBRL file contains the company's **Omzetbelastingnummer** (`123456782B90`) instead of the Tax Unit's VAT (`826317558B01`). * The tax authority rejects the return because the identifier does not match the fiscal unity registration. **Cause:** * `_get_sbr_identifier()` in `l10n_nl_reports_sbr_ob_nummer` unconditionally returns `self.env.company.l10n_nl_reports_sbr_ob_nummer` before consulting the Tax Unit. * The `super()` call, which correctly routes to `tax_unit.vat` via `report.get_vat_for_export()`, is only reached when the company field is empty — so the Tax Unit's VAT is never used when a company OB-number is set. **Fix:** * When a Tax Unit is active in the report options, delegate immediately to `super()._get_sbr_identifier()`, which resolves `tax_unit.vat` through the existing `get_vat_for_export()` logic. * The company-level `l10n_nl_reports_sbr_ob_nummer` override is preserved as a fallback for the `company_only` (no Tax Unit) case. opw-6350840 Forward-Port-Of: odoo/enterprise#126876 Forward-Port-Of: odoo/enterprise#125167
Changing a payslip to a structure that does not use worked day lines now properly clears outdated worked day information. Belgian DMFA payroll reporting also continues to include the right remunerations for off-cycle payslips, improving payroll accuracy and compliance reporting.
Original PR description
hr_payroll: Previously, changing to a structure with `use_worked_day_lines = False` (e.g., 13th month) caused `valid_slips` to be empty and return early, leaving stale worked day lines on the payslip. This commit resets the worked_days_lines before filtering for valid payslips. l10n_be_hr_payroll: After fixing the payroll bug and clearing worked_days_lines correctly, the DMFA report fails to correctly consider remunerations since the off-cycle payslips do not have worked_days_lines anymore. This commit backports a fix from odoo/enterprise#106689 to not skip remunerations for payslips with no worked days lines. task-6401942 Forward-Port-Of: odoo/enterprise#124986
3 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
Spreadsheet 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)
Resolved issues and error corrections
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
6 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
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
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
3 changes
Resolved issues and error corrections
Dutch SBR tax returns for tax units now use the tax unit's VAT number instead of an individual company's omzetbelastingnummer. This prevents rejected filings when reporting for a fiscal unity while keeping the company number as the fallback for single-company returns.
Original PR description
**Steps to reproduce:** * Install the **Netherlands - SBR** (`l10n_nl_reports_sbr`) and **Netherlands - SBR OB Nummer** (`l10n_nl_reports_sbr_ob_nummer`) modules. * Create two companies with Dutch…
**Steps to reproduce:** * Install the **Netherlands - SBR** (`l10n_nl_reports_sbr`) and **Netherlands - SBR OB Nummer** (`l10n_nl_reports_sbr_ob_nummer`) modules. * Create two companies with Dutch localization. * Go to **Accounting → Configuration → Tax Units** and create a Tax Unit with its own **Tax ID** (e.g. `NL826317558B01`), adding both companies. * On the main company form, fill in the **Omzetbelastingnummer** field (e.g. `123456782B90`). * Go to **Accounting → Reporting → Tax Return**, select the Tax Unit in the filter, and click **XBRL → Download XBRL File**. **Observed behavior:** * The `<xbrli:identifier>` in the exported XBRL file contains the company's **Omzetbelastingnummer** (`123456782B90`) instead of the Tax Unit's VAT (`826317558B01`). * The tax authority rejects the return because the identifier does not match the fiscal unity registration. **Cause:** * `_get_sbr_identifier()` in `l10n_nl_reports_sbr_ob_nummer` unconditionally returns `self.env.company.l10n_nl_reports_sbr_ob_nummer` before consulting the Tax Unit. * The `super()` call, which correctly routes to `tax_unit.vat` via `report.get_vat_for_export()`, is only reached when the company field is empty — so the Tax Unit's VAT is never used when a company OB-number is set. **Fix:** * When a Tax Unit is active in the report options, delegate immediately to `super()._get_sbr_identifier()`, which resolves `tax_unit.vat` through the existing `get_vat_for_export()` logic. * The company-level `l10n_nl_reports_sbr_ob_nummer` override is preserved as a fallback for the `company_only` (no Tax Unit) case. opw-6350840 Forward-Port-Of: odoo/enterprise#126876 Forward-Port-Of: odoo/enterprise#125167
This fix prevents Instagram post syncing from crashing when a post has no media URL. Social Marketing users can open the module and continue automatic syncing without seeing an error traceback for this case.
Original PR description
The fix introduced in https://github.com/odoo/enterprise/commit/9e9c99712ad4b9d58dc7601da41a852e457ad097 didn't account for the fact that `post.get('media_url') ` could return a None value, which in…
The fix introduced in https://github.com/odoo/enterprise/commit/9e9c99712ad4b9d58dc7601da41a852e457ad097 didn't account for the fact that `post.get('media_url') ` could return a None value, which in turn would raise en error when trying to concatenate the value later.
This in turn:
- will block syncing of instagram instagram posts
- will raise a traceback when you open the Social Marketing module and the auto-sync kicks in.
### Example traceback
```
Traceback (most recent call last):
[...]
File "/home/odoo/src/enterprise/saas-19.2/social_instagram/models/social_stream.py", line 86, in _fetch_stream_data
return self._fetch_instagram_posts()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/enterprise/saas-19.2/social_instagram/models/social_stream.py", line 64, in _fetch_instagram_posts
values['message'] = (values['message'] + "\n" + post.get('media_url')).strip()
~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
TypeError: can only concatenate str (not "NoneType") to str
```
### Solution:
Fallback to an empty string if `post.get('media_url')` yields a None value.
OPW-6449357
Forward-Port-Of: odoo/enterprise#126987This fixes Uruguayan electronic delivery guide PDFs so long addenda text correctly triggers a dedicated addenda page. Businesses printing legal CFE documents will now get complete supporting information on delivery guides instead of missing overflow content.
Original PR description
**Description** When printing the legal PDF of a CFE, the report can request a dedicated addenda page (`adenda=true` report parameter) when the addenda does not fit in the small box of the standard…
**Description** When printing the legal PDF of a CFE, the report can request a dedicated addenda page (`adenda=true` report parameter) when the addenda does not fit in the small box of the standard report (roughly 6 lines of 140 characters). `l10n_uy_edi_document._get_report_params()` computed the addenda by calling `self.move_id._l10n_uy_edi_get_addenda()` directly. For e-remito EDI documents created from stock pickings, `move_id` is not set, so the addenda was always empty and the dedicated addenda page was never requested for delivery guides. **Changes** - Resolve the addenda from the document origin record: reuse the existing `_get_origin_record()` abstraction. Add it on `l10n_uy_edi.document` (returns the move) and let `l10n_uy_edi_stock` extend it to fall back to `picking_id`; `_get_report_params()` now reads the addenda through it. - Remove a no-op `_get_pdf()` override in `l10n_uy_edi_stock`. - Add a unit test covering the dedicated addenda page threshold (6 lines x 140 chars) for delivery guides. **Note** This PR replaces branch `adhoc-dev:18.0-t-stock-edi-addenda-fix-kz` (original authorship preserved); resubmitted from a new branch to keep follow-up and tracking with the current maintainer.
2 changes
Resolved issues and error corrections
Dutch SBR tax return exports now use the Tax Unit VAT number when a tax unit is selected, instead of incorrectly using the individual company's turnover tax number. This prevents fiscal unity returns from being rejected by the tax authority due to a mismatched identifier.
Original PR description
**Steps to reproduce:** * Install the **Netherlands - SBR** (`l10n_nl_reports_sbr`) and **Netherlands - SBR OB Nummer** (`l10n_nl_reports_sbr_ob_nummer`) modules. * Create two companies with Dutch…
**Steps to reproduce:** * Install the **Netherlands - SBR** (`l10n_nl_reports_sbr`) and **Netherlands - SBR OB Nummer** (`l10n_nl_reports_sbr_ob_nummer`) modules. * Create two companies with Dutch localization. * Go to **Accounting → Configuration → Tax Units** and create a Tax Unit with its own **Tax ID** (e.g. `NL826317558B01`), adding both companies. * On the main company form, fill in the **Omzetbelastingnummer** field (e.g. `123456782B90`). * Go to **Accounting → Reporting → Tax Return**, select the Tax Unit in the filter, and click **XBRL → Download XBRL File**. **Observed behavior:** * The `<xbrli:identifier>` in the exported XBRL file contains the company's **Omzetbelastingnummer** (`123456782B90`) instead of the Tax Unit's VAT (`826317558B01`). * The tax authority rejects the return because the identifier does not match the fiscal unity registration. **Cause:** * `_get_sbr_identifier()` in `l10n_nl_reports_sbr_ob_nummer` unconditionally returns `self.env.company.l10n_nl_reports_sbr_ob_nummer` before consulting the Tax Unit. * The `super()` call, which correctly routes to `tax_unit.vat` via `report.get_vat_for_export()`, is only reached when the company field is empty — so the Tax Unit's VAT is never used when a company OB-number is set. **Fix:** * When a Tax Unit is active in the report options, delegate immediately to `super()._get_sbr_identifier()`, which resolves `tax_unit.vat` through the existing `get_vat_for_export()` logic. * The company-level `l10n_nl_reports_sbr_ob_nummer` override is preserved as a fallback for the `company_only` (no Tax Unit) case. opw-6350840 Forward-Port-Of: odoo/enterprise#125167
Swiss bank payment files generated with the newer PAIN format will no longer include a service level code that banks reject. This helps Swiss customers avoid failed SEPA payment submissions when moving to the newer required payment file version.
Original PR description
Code `NURG` is rejected by Swiss banks. Issue comes back again because of the end of the transition period for PAIN.001: - transition `pain.001.001.03.ch.02` to `pain.001.001.09` - end of transition is November 14th 2026 Many customers complain that when they switch to PAIN version `pain.001.001.09`, their files are rejected because of invalid `NURG` code for `SvcLvl`. The fix was done long time ago, for Odoo 15, and is still there. But it targets only the old schema: - odoo/enterprise#57104 - odoo/enterprise#56704 And the big refactoring happened later, only for Odoo 18: - odoo/enterprise#60393 So this bugfix should land on branch 16.0 and be ported to 17.0 only. Forward-Port-Of: odoo/enterprise#126821