Wednesday, August 5, 2026
11 changes · saas-19.4
Enhancements to existing features
SEPA direct debit batch validation has been optimized to handle large payment batches more efficiently. Businesses processing hundreds or thousands of payments should see shorter validation and notification times, reducing delays during payment collection workflows.
Original PR description
- Replace the `id:recordset` aggregation in `_get_expiry_date_per_mandate()` with `date:max` to compute the latest payment date directly in SQL. - Render `email_from` for all payments in batch and cache the computed authors by sender email to avoid repeated partner lookups during SDD pre-notification. This reduces ORM/cache overhead when validating large SEPA batches containing thousands of payments. Measured on a production-sized database: | metric | before | after | factor | |--------|-------:|------:|-------:| | `_get_expiry_date_per_mandate` (500 payments) | 564 ms | 111 ms | ~5x | | `_send_after_validation` notification (500 payments) | 92.9 s | 55.7 s | ~1.7x | | `_get_expiry_date_per_mandate` (1000 payments) | 890 ms | 178 ms | ~5x | | `_send_after_validation` notification (1000 payments) | timed out (>159 s) | 108.9 s | completed | OPW-6377340 Forward-Port-Of: odoo/enterprise#126429 Forward-Port-Of: odoo/enterprise#125439
Improves the process that suggests reconciliation rules when assigning accounts to bank statement lines. This prevents memory errors and speeds up handling of long payment references, making bank reconciliation setup more reliable for users with large or detailed transaction descriptions.
Original PR description
When setting an account to an account.bank.statement there is a step to automatically create a reconciliation rule/model if one does not exist already. To do so, we retrieve 5…
When setting an account to an account.bank.statement there is a step to automatically create a reconciliation rule/model if one does not exist already. To do so, we retrieve 5 account.bank.statement.lines and use them to define the reconciliation model config. A matching is done on the payment_ref of the account.bank.statement.lines by finding the longest common substring in the reference. ### Current Implementation The current algorithm does so by first generating all the possible substrings for all the payment_ref before doing the intersection between these sets and returning the max if `len(substring) >=10`. This is reasonable when the payment_ref follows either a SEPA communication national standard like the Belgian one or the Creditor Reference standard (ISO 11649). For transactions with large, unstructured communication with more than 100 chars, the method `_get_common_substrings` quickly overfill the memory, sometimes raising a MemoryErorr, and takes a significant amount of time. That's because the nested function `_generate_all_substrings` generates n*(n+1)/2 substrings, with n being the lenght of a payment_ref, called `label` in `generate_all_substrings`. ### Proposed Fix This commit introduces another algorithm to find the largest common substring. It starts by taking the two smallest labels to find their substrings intersection. We know that for an arbitrary collection of labels, the intersection of their substrings sets A ∩ B ∩...∩ Z is included in the intersection of any two substrings sets. The underlying assumption of the first step is that for an arbitrary collection of labels the intersection of the substrings sets of the two smallest labels will be the smallest intersection of any given pair of substrings sets. This won't hold true everytime and using a metric such as label similarity instead of shortest string might be better. But on average this should be good enough and it's easier to implement + it removes the need of preprocessing the labels to compute the similarity. The point of the new nested function `common_substrings` is to discard common substrings as we build them. Using the current `generate_all_substsrings` on either the smallest label or both smallest labels would still generate and store a lot of substrings, especially for large labels. By yielding the common substrings as we find them, the memory footprint is vastly reduced. Lastly, the next substring in the common_substrings iterable is only checked against the remaining labels if it's longer than the current match. This speeds up the whole process ### speedup In a customer database with some account.bank.statement.line with payment_ref > 500 chars, setting a specific account (code 4970) on transactions goes from MemoryError to < 1Mb memory consumption. Because of the memory consumption it was not possible to gather timing value on the current version. Testing the new algorithm in a shell and using as labels the 5 longest payment_ref in the customer database (831, 831, 1117, 1178, 1300 chars), averaging to 2000 chars once normalised, the average time to execute `_get_common_substrings` is 900 ms ± 10.3 ms. Forward-Port-Of: odoo/enterprise#121575 Forward-Port-Of: odoo/enterprise#118824
Resolved issues and error corrections
Bank transaction imports now ignore archived bank account records when automatically identifying the partner. This prevents old or inactive bank details from assigning transactions to the wrong partner, improving reconciliation accuracy.
Original PR description
Steps to reproduce: - Have a partner with a bank account, then archive the res.partner.bank record (keep the partner active). - Import or create a bank transaction (e.g. via bank sync) whose account number matches that archived bank account, and whose label/payment_ref would otherwise match a reconciliation model for a different partner. - Let the transaction go through automatic partner retrieval. => The archived bank account's partner is assigned, even though a normal manual entry (which skips the account-number match) would have used the label instead. Cause of the issue: `AccountBankStatementLine._retrieve_partner()` matches statement lines to partners in batch using raw SQL joining `res_partner_bank`. The query's WHERE clause filters out archived partners (`AND partner.active`) but never filters `res_partner_bank.active`. opw-6340479 Forward-Port-Of: odoo/enterprise#124748 Forward-Port-Of: odoo/enterprise#124537
The aged payable and receivable report drill-down now hides fully paid invoices and bills, so users only see items that were actually outstanding. It also respects the selected report date, improving accuracy for historical balance reviews.
Original PR description
Steps to Reproduce: 1. Create a vendor/customer with multiple bills/invoices. 2. Fully pay one or more, leaving at least one still open for the same partner. 3. Open Accounting > Reporting > Partner…
Steps to Reproduce:
1. Create a vendor/customer with multiple bills/invoices.
2. Fully pay one or more, leaving at least one still open for the same partner.
3. Open Accounting > Reporting > Partner Reports > Aged Payable/Receivable.
4. Set to any date and click into an aging bucket for that partner.
Issue:
The drill-down list shows fully settled bills (residual = 0.00) alongside genuinely outstanding ones. Only surfaces when the partner has at least one open balance — if everything is paid, there is no bucket to click into.
Root Cause:
aged_partner_balance_audit builds the drill-down domain filtering only by reconcile flag, journal type, and date range — never checking residual. Additionally it completely overwrites the XML action domain (account.action_amounts_to_settle) which already had ('amount_residual', '!=', 0), losing that protection entirely.
Fix:
Added ('residual_at_date', '!=', 0) to the domain in aged_partner_balance_audit and set recon_limit in the action context so residual_at_date computes as of the report's 'as of' date rather than today's value:
action['context'] = {
'recon_limit': options['date']['date_to'],
}
Without recon_limit, residual_at_date falls back to amount_residual (today's value) which incorrectly excludes bills that were genuinely open on the report date but paid after it.
Result:
The drill-down now correctly shows only genuinely outstanding items regardless of whether the report is run as of today or a historical date.
opw 6333699
Forward-Port-Of: odoo/enterprise#126021Fixed an issue that prevented users from confirming multiple Brazilian customer invoices at once when Avalara Brazil tax mapping was enabled. This removes an error during batch validation, allowing finance teams to process invoices together as expected.
Original PR description
**_Steps to reproduce:_** * Install `l10n_br_avatax` and configure Avalara Brazil. * Create at least two customer invoices. * Ensure the fiscal position is set to **Automatic Tax Mapping (Avalara…
**_Steps to reproduce:_** * Install `l10n_br_avatax` and configure Avalara Brazil. * Create at least two customer invoices. * Ensure the fiscal position is set to **Automatic Tax Mapping (Avalara Brazil)**. * Select the invoices and click **Action → Confirm Entries**. **_Observed behavior:_** * A traceback is raised with `ValueError: Expected singleton: account.move(...)` and the invoices cannot be validated. **_Cause:_** * During tax extraction, `_extract_tax_values_from_l10n_br_avatax_detail` accesses `self.invoice_filter_type_domain` while `self` may contain multiple `account.move` records. * Accessing `invoice_filter_type_domain` on a multi-recordset raises an `Expected singleton` error, preventing the validation of multiple invoices. **_Fix:_** * Build the returned tax values by iterating over each invoice in the recordset and using the corresponding `invoice_filter_type_domain`. * This ensures `_extract_tax_values_from_l10n_br_avatax_detail` correctly handles multiple invoices during validation without raising a singleton error. opw-6334761 Forward-Port-Of: odoo/enterprise#126437 Forward-Port-Of: odoo/enterprise#124993
Rental orders that use a custom make-to-order buying route now correctly generate the expected return transfer alongside the delivery and purchase. This prevents missing return logistics for rental products and helps teams keep rental stock movements accurate.
Original PR description
### Steps to reproduce: - In the settings enable: Multi-Steps Routes and rental transfers - Unarchive the MTO route - Create a rental product P with a buy route and a set vendor - Create a rental…
### Steps to reproduce: - In the settings enable: Multi-Steps Routes and rental transfers - Unarchive the MTO route - Create a rental product P with a buy route and a set vendor - Create a rental order for 1 x P and set the the MTO route on the sol - Confirm the order #### > The delivery as well as the purchase for 1 unit of P was generated but the return was not. ### Cause of the issue: The procurement generated to handle both the delivery and the return rental picking are handled by the `_create_procurements`: https://github.com/odoo/enterprise/blob/b0e48baaf99bdc4faefd2ffdd3bd5637fb548593/sale_stock_renting/models/sale_order_line.py#L353-L374 The `route_ids` set and used is the `mto_route` set on the sol: https://github.com/odoo/odoo/blob/0f061503e26ac8c441d62d91947419119e48c47a/addons/sale_stock/models/sale_order_line.py#L415-L422 https://github.com/odoo/odoo/blob/0f061503e26ac8c441d62d91947419119e48c47a/addons/sale_stock/models/sale_order_line.py#L282-L297 However, in the present case, the mto route does not contain any rule with a relevant `location_src_id` in the rental location so that the return will not be generated. opw-6361322 Forward-Port-Of: odoo/enterprise#126410 Forward-Port-Of: odoo/enterprise#124097
Electronic invoices for Peru now use the address structure required by SUNAT’s current UBL 2.1 standard. This helps invoices pass official validation by correctly formatting districts and urban subdivisions.
Original PR description
Update electronic invoicing address nodes to align with current SUNAT requirements. This transitions the geographic data formatting from the legacy UBL 2.0 schema to the standard UBL 2.1 specification, ensuring proper structural validation for districts and urban subdivisions. Documentation used: https://cpe.sunat.gob.pe/sites/default/files/inline-files/guia+xml+factura+version+2-1+1+0+(2)_0+(2).pdf opw-6282314 Forward-Port-Of: odoo/enterprise#126380 Forward-Port-Of: odoo/enterprise#121390
Cancelled Mexican CFDI invoices now reprint with their required fiscal details, including QR codes, digital stamps, and fiscal folio information. This helps businesses keep legally relevant invoice records complete even after cancellation.
Original PR description
### Issue before this commit: When an invoice's CFDI cancellation request is confirmed (state moves from sent to cancel), reprinting the invoice PDF no longer displays the fiscal information (QR…
### Issue before this commit: When an invoice's CFDI cancellation request is confirmed (state moves from sent to cancel), reprinting the invoice PDF no longer displays the fiscal information (QR code, digital stamps, fiscal folio/UUID, etc.), even though the signed CFDI attachment is still present on the invoice. ### Steps to reproduce the issue: 1. Download Accounting and l10n_mx 2. Create an invoice and send it to CFDI 3. Select 'Request for cancel' 4. Wait and click retry button in the CFDI tab until the invoice is marked as cancelled 5. Print the invoice again 6. See PDF does not show fiscal information (QR, fiscal folio, etc.) ### Cause of the issue: https://github.com/odoo/enterprise/blob/f6c94d4ca3ef4211a5ab00bf0b39f6a7675c8f79/l10n_mx_edi/models/account_move.py#L904-L909 Once the CFDI is cancelled, the computed field switches to 'cancel', so the condition fails and the method falls back to the generic account.report_invoice_document template, which has no knowledge of CFDI fiscal fields. ### Reason to introduce the fix: A cancelled CFDI invoice is still a legally relevant fiscal document in Mexico and must be reprintable with its fiscal data intact (to prove it was issued and later cancelled). The fix extends the condition to also cover the 'cancel' state, ensuring the CFDI-specific report template is used whenever a valid attachment exists, regardless of whether the CFDI is currently signed or cancelled. opw-6393656 Forward-Port-Of: odoo/enterprise#126466
Attachments added to employee records and leave requests now create documents in the correct employee-related folders instead of the general Employees root folder. Sick leave attachments also reliably create the expected document, making HR document organization more consistent and easier to manage.
Original PR description
Before this commit, when adding an attachment to a leave or a employee version the mixin was configured to create the document in the root folder of Employees which was not very convenient. In addition, when creating a Sick leave with an attachment, no document was ever created. This commit fix both those bugs. Task-6095811 Forward-Port-Of: odoo/enterprise#121942 Forward-Port-Of: odoo/enterprise#112993
The appointment booking page no longer shows empty months when a minimum booking delay pushes the first available slot into a later month. This ensures customers can reliably see and choose valid appointment times when navigating the calendar.
Original PR description
On the website booking page, moving to a later month can show no available times even though the weekly schedule clearly has some. ### Steps to reproduce - Install Appointments. - Create a recurring…
On the website booking page, moving to a later month can show no available times even though the weekly schedule clearly has some. ### Steps to reproduce - Install Appointments. - Create a recurring appointment type available on a single weekday (say Monday), with a user or resource assigned and a date range spanning a few months. - Set `Allow bookings at least` (the minimum booking delay) so that the current time plus the delay falls after this month's last Monday. Close to the end of a month, a day or two of delay is enough. - Open the booking page: the first month shown is next month, because the delay skipped this month's last slot. - Click the arrow to move forward one more month. => the reached month shows no slots, even though it has Monday availability. ### Cause The calendar computes availability one month at a time. It builds a list of months, and the browser refers to each month by its position in that list (0, 1, 2, ...). Clicking the next arrow sends that position back to the server. The server turns the position into a real month by adding it to a start month, which it computes as `now` plus the minimum booking delay. But the list shown to the visitor does not start there: it starts at the month of the first slot that can actually be booked. These two are usually the same, so the position lines up. They stop matching when the delay moves the earliest bookable time past the last availability day of the current month. In the steps above, `now` plus the delay lands after the month's last Monday, so the first bookable slot is a Monday in the next month. The visitor's list then starts one month later than the server assumes, every position points one month too early, and the server computes availability for a month the visitor is not looking at. The reached month comes back empty. ### Fix Count the visitor's month position from the same first bookable slot the list starts from, instead of from `now` plus the delay. The navigation offset is passed to the slot computation and resolved against that slot, so the filled month always matches the month the visitor sees. opw-6353569 Forward-Port-Of: odoo/enterprise#123994 Forward-Port-Of: odoo/enterprise#122494
Date and datetime fields now disappear from the pivot popup once all available time breakdowns have already been used. This prevents users from adding duplicate entries, avoiding confusing drag-and-drop behavior and keeping spreadsheet pivots consistent.
Original PR description
Current behavior before PR: - Date and datetime fields remained visible even when all their granularities were already added to the pivot. - Users could add the same field with the same granularity multiple times, creating duplicate IDs and causing unexpected drag and drop behavior. Desired behavior after PR is merged: - Hide date and datetime fields from the popup once all available granularities have already been added to the pivot. - This prevents duplicate field IDs and keeps the popup behavior consistent with spreadsheet pivots during drag-and-drop. Task: [6295794](https://www.odoo.com/odoo/project/2328/tasks/6295794) Forward-Port-Of: odoo/enterprise#126772 Forward-Port-Of: odoo/enterprise#123280