Daily updates from Odoo
Thursday, July 16, 2026
17 changes · saas-18.3
Enhancements to existing features
French VAT declarations now handle SIRET numbers that include spaces by cleaning them before submission. The update also warns users when a bank account number appears incorrectly formatted, helping prevent failed filings.
Original PR description
This commit resolves an issue where VAT declarations failed when the provided SIRET number included spaces. Since check_siret verifies the format, we now strip all spaces from the input. Additionally, this commit introduces a validation for bank account numbers, ensuring that we warn the user if the account number is wrongly formatted. task-6253745 Forward-Port-Of: odoo/enterprise#120689
Move French e-invoicing technical statuses out of the main invoice UI and into the chatter. The PPF status is kept available in debug mode for troubleshooting, while regular users get a concise chatter summary with the PA status, PPF status, and any returned errors. Duplicate Lifecycle XML attachments are no longer posted in the chatter. Task-6273270 Forward-Port-Of: odoo/odoo#269465
Original PR description
Move French e-invoicing technical statuses out of the main invoice UI and into the chatter. The PPF status is kept available in debug mode for troubleshooting, while regular users get a concise chatter summary with the PA status, PPF status, and any returned errors. Duplicate Lifecycle XML attachments are no longer posted in the chatter. Task-6273270 Forward-Port-Of: odoo/odoo#269465
Test lints should be deterministic so retrying them doesn't make sense. While lints are not the slowest, pylint can take 10~15mn in later branches, and with retrying that's half an hour (and some) to get a failure, which is all that in wasted staging and build time. Forward-Port-Of: odoo/odoo#276600
Original PR description
Test lints should be deterministic so retrying them doesn't make sense. While lints are not the slowest, pylint can take 10~15mn in later branches, and with retrying that's half an hour (and some) to get a failure, which is all that in wasted staging and build time. Forward-Port-Of: odoo/odoo#276600
Resolved issues and error corrections
Fixes formatting of Uruguay electronic invoice addenda and terms so extra blank lines are not added. This helps keep short notes on the expected page and avoids unnecessary separate addenda pages in generated CFE PDFs.
Original PR description
## Context When generating a CFE (Comprobante Fiscal Electrónico) that contains both a configured addenda (e.g. bank account details stored in `l10n_uy_edi_addenda_ids`) and terms & conditions from…
## Context
When generating a CFE (Comprobante Fiscal Electrónico) that contains both a configured addenda (e.g. bank account details stored in `l10n_uy_edi_addenda_ids`) and terms & conditions from the invoice's `narration` field, the resulting addenda string could end up with unnecessary blank lines between the two sections, causing the addenda to be rendered on a separate page even when the logical content fits within the 6-line threshold.
## Root Cause
`_l10n_uy_edi_get_addenda` joins both parts without stripping whitespace from either of them first, and adds two lines between addendas and terms and conditions:
addenda = addenda + "\n\n" + term_and_conditions if addenda else term_and_conditions
Two sources independently introduce extra newlines around the separator:
1. **Addenda content** — `_get_legends` returns the raw `content` field value of each addenda record. These fields commonly end with a trailing `\n`, so the addenda string already ends with a newline before the `"\n"` separator is concatenated.
2. **`html2plaintext`** — the `narration` field is stored as HTML. When converted to plain text, `html2plaintext` typically wraps paragraph content in leading/trailing newlines.
The combination of the trailing `\n` from the addenda, the explicit `"\n\n"` separator, and the leading/trailing `\n` from `html2plaintext` produces 2–3 consecutive newlines, which `splitlines()` counts as blank lines.
A realistic 4-line addenda + 1-line narration thus produces **7 lines** instead of the expected 5, crossing the 6-line threshold in `_get_report_params` and triggering `adenda=true` — which forces the addenda onto a separate page unnecessarily.
## Steps to Reproduce
1. Configure a `l10n_uy_edi.addenda` record of type `addenda` with multi-line content (4 lines)
2. Create and confirm an invoice with `narration` set to a short single-line term
3. Generate the CFE PDF via Uruware.
4. Observe that the addenda is rendered on a separate page despite the logical content being only 5 lines.
<img width="1042" height="448" alt="image" src="https://github.com/user-attachments/assets/b538211c-5f37-4648-979d-99cd75cf31c2" />
## Fix
Strip leading and trailing whitespace (including newlines) from both parts before joining them. The ternary is also replaced with an explicit `if/else` for clarity:
def _l10n_uy_edi_get_addenda(self):
addenda = self.l10n_uy_edi_document_id._get_legends("addenda", self)
if self.narration:
term_and_conditions = html2plaintext(self.narration).strip()
if addenda:
addenda = addenda.strip() + "\n" + term_and_conditions
else:
addenda = term_and_conditions
return self._l10n_uy_edi_clean_non_ascii_chars(addenda)
This guarantees exactly one `\n` separator between sections regardless of how the content fields were stored or how `html2plaintext` formatted the narration.
The threshold logic in `_get_report_params` is unchanged: addendas that genuinely exceed 6 lines (after wrapping at 140 chars) continue to be printed on a dedicated page.
Result
<img width="1117" height="456" alt="image" src="https://github.com/user-attachments/assets/3a2d942c-8c37-40f6-bc25-470c0bd25b08" />
Forward-Port-Of: odoo/enterprise#119283Bank transaction matching now ignores archived bank accounts when automatically selecting a partner. This prevents transactions from being assigned to the wrong partner and keeps automated reconciliation aligned with expected manual matching behavior.
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
Invoices marked as excluded from follow-up are now also left out of follow-up email attachments and printed follow-up letters. This prevents customers from receiving documents for invoices that staff intentionally removed from the follow-up process.
Original PR description
Steps to reproduce: 1. Install Accounting and create an invoice for a customer which has a due date in the past 2. Make sure the payment term for the invoice is "Immediate Payment" and Send the…
Steps to reproduce: 1. Install Accounting and create an invoice for a customer which has a due date in the past 2. Make sure the payment term for the invoice is "Immediate Payment" and Send the invoice. 3. Open the contact form and click on the Customer Statement smart button 4. Exclude the invoice using the 'No Follow-Up' toggle 5. In the Accounting tab in the contact form, click on send 6. Open the internal link of the Content Template, go to the options tab and select 'Print Follow-up Letter' in Dynamic Reports 7. Save the configuration and send the email Issue: Excluded invoices still appeared as PDF attachments in the follow-up email and were merged into the printed follow-up letter PDF. Why this happens: Both `default_get` in `account_followup.manual_reminder` and `_get_invoices_to_print` in `res.partner` traversed `unreconciled_aml_ids` without filtering out lines where `no_followup = True`, so excluded invoices were included regardless. opw-6310602 Forward-Port-Of: odoo/enterprise#122249
Fixes Uruguay electronic credit notes so required reference amount information is still sent when the original e-Ticket total is 0.00. This prevents DGI rejections for valid zero-value credit notes and avoids related errors for document types that do not use the same field.
Original PR description
Problem: When generating an e-Ticket Credit Note for an original e-Ticket with a total amount of 0.00, the XML cleanup mechanism removes reference fields whose value is 0.00. As a result, the credit note is rejected by DGI with: "CODE 31: En línea de Referencia 1 si NO IndGlobal = 1 deben existir TpoDocRef, Serie, NroCFERef, MntCFERef, TpoMonedaRef." Solution: Ensure that MntCFERef is sent even if the value is 0.00. opw-6378783 Forward-Port-Of: odoo/enterprise#124377 Forward-Port-Of: odoo/enterprise#124354
Rather than assign `log_target` and blow up if the third branch is taken, use a `defaultdict` and just increment the count in each branch on hit. The continue in the third branch is not strictly necessary, but that way it protects us if anyone decides to add post-processing which also doesn't account for the third branch. Forward-Port-Of: odoo/odoo#276481
Original PR description
Rather than assign `log_target` and blow up if the third branch is taken, use a `defaultdict` and just increment the count in each branch on hit. The continue in the third branch is not strictly necessary, but that way it protects us if anyone decides to add post-processing which also doesn't account for the third branch. Forward-Port-Of: odoo/odoo#276481
Issue: ---------------------------------------- When resetting the mail template "Settings: 2Fa New Login" an error is raised. Steps to reproduce: ---------------------------------------- - Search for the mail template "Settings: 2Fa New Login" - Click "Reset Template" - Traceback Also happens when changing the template and saving. Cause: ---------------------------------------- Since b6b05f14ee37b5901abb8c5ee046727a28b9d94e there is an assert in the method `_get_totp_mail_code(
Original PR description
Issue: ---------------------------------------- When resetting the mail template "Settings: 2Fa New Login" an error is raised. Steps to reproduce: ---------------------------------------- - Search for the mail template "Settings: 2Fa New Login" - Click "Reset Template" - Traceback Also happens when changing the template and saving. Cause: ---------------------------------------- Since b6b05f14ee37b5901abb8c5ee046727a28b9d94e there is an assert in the method `_get_totp_mail_code()` to ensure it's used during 2FA. But when passing by `_check_can_be_rendered()` we render the template not in sudo and `_get_totp_mail_code()` resulting in a traceback. Solution: ---------------------------------------- Rendering the template in sudo in `_check_can_be_rendered()`. As this method is only to make sure the template has no broken qweb ([src](999df6d4a3b5d21648e5e09757661714e99e1154)), rendering in sudo should not be an issue. opw-6300337
CertificateAdapter presents a client certificate stored in the database instead of on disk when opening an HTTPS connection (used by the l10n_es EDI modules verifactu, sii and tbai). It loaded that certificate on the connection path of requests 2.31, but requests >= 2.32 changed that path (no longer calls get_connection()), so the step was skipped and the call crashed with: "TypeError: expected str, bytes or os.PathLike object, not certificate" Odoo pins requests 2.31.0 (max depending on Pyth
Original PR description
CertificateAdapter presents a client certificate stored in the database instead of on disk when opening an HTTPS connection (used by the l10n_es EDI modules verifactu, sii and tbai). It loaded that…
CertificateAdapter presents a client certificate stored in the database instead of on disk when opening an HTTPS connection (used by the l10n_es EDI modules verifactu, sii and tbai). It loaded that certificate on the connection path of requests 2.31, but requests >= 2.32 changed that path (no longer calls get_connection()), so the step was skipped and the call crashed with: "TypeError: expected str, bytes or os.PathLike object, not certificate" Odoo pins requests 2.31.0 (max depending on Python version), but online databases can use the version shipped by the OS (2.32.x on recent Ubuntu 26). Set the certificate up when the adapter is created instead of on that connection call. That step runs the same on every requests version, so the fix works both before and after 2.32. Steps to reproduce: - Spanish company with Veri*Factu and a certificate, on a server running requests >= 2.32 (saas-19.3 database for exemple on ubuntu 26) - Post a customer invoice and send it to Veri*Factu. => TypeError Reference: https://github.com/psf/requests/blob/f361ead047be5cb873174218582f7d8b9fcd9f49/HISTORY.md?plain=1#L146 Ticket [link](https://www.odoo.com/odoo/project.task/6366028) opw-6366028 Forward-Port-Of: odoo/odoo#275324
**PROBLEM** `_cron_migrate_local_to_cloud_storage()` is used to migrate attachment to a cloud storage. It delete the attachment from the database, rendering it innacessible from the server-side. The problem is it can delete attachment used in business logic. **STEP TO REPRODUCE** 1. Install and configure cloud_storage + a provider (e.g. cloud_storage_google) and cloud_storage_migration. 2. Install l10n_mx_edi and stamp a customer invoice. The CFDI XML is stored as an ir.attachment that:
Original PR description
**PROBLEM** `_cron_migrate_local_to_cloud_storage()` is used to migrate attachment to a cloud storage. It delete the attachment from the database, rendering it innacessible from the server-side. The…
**PROBLEM** `_cron_migrate_local_to_cloud_storage()` is used to migrate attachment to a cloud storage. It delete the attachment from the database, rendering it innacessible from the server-side. The problem is it can delete attachment used in business logic. **STEP TO REPRODUCE** 1. Install and configure cloud_storage + a provider (e.g. cloud_storage_google) and cloud_storage_migration. 2. Install l10n_mx_edi and stamp a customer invoice. The CFDI XML is stored as an ir.attachment that: - is referenced by a business Many2one l10n_mx_edi.document.attachment_id (copied into account.move.l10n_mx_edi_cfdi_attachment_id), - has res_field = NULL (record attachment, not a field binary), - is posted to the chatter → it has a row in message_attachment_rel. 3. Add account.move to cloud_storage_migration_message_models and set a low enough cloud_storage_min_file_size. 4. Run the cron _cron_migrate_local_to_cloud_storage. 5. Open the invoice and trigger any recompute of the EDI chain (e.g. Update Payments, a reconciliation, or re-stamping). **FIX** Filters out attachment linked to a model listed by the already existing function `_get_cloud_storage_unsupported_models()` opw-6347198 Forward-Port-Of: odoo/odoo#275945
Description of the issue/feature this PR addresses: Current behavior before PR: meeting.rrule is stored as a full dateutil rrule string, e.g.: "DTSTART:20250218T113209\nRRULE:FREQ=YEARLY;COUNT=720" Passing the full multi-line string as a single RRULE property value causes vobject to emit two RRULE lines, where the first one ("RRULE:DTSTART:...") has no FREQ. This is not standard-compliant and is rejected by calendar clients (e.g. Thunderbird: "invalid frequency null"). Desired behavior af
Original PR description
Description of the issue/feature this PR addresses:
Current behavior before PR: meeting.rrule is stored as a full dateutil rrule string, e.g.: "DTSTART:20250218T113209\nRRULE:FREQ=YEARLY;COUNT=720"
Passing the full multi-line string as a single RRULE property value causes vobject to emit two RRULE lines, where the first one ("RRULE:DTSTART:...") has no FREQ. This is not standard-compliant and is rejected by calendar clients (e.g. Thunderbird: "invalid frequency null").
Desired behavior after PR is merged: Only a single RRULE line is generated.
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#274490The partner credit limit warning on quotations and customer invoices depends on which company the user is currently working in, instead of the company of the document itself. Steps to reproduce: - Enable Sale Credit Limit in the settings of My Company (San Francisco) - Set a Credit Limit of 100 on a customer, e.g. Deco Addict - Create a draft quotation of 500 for that customer => The credit limit warning banner is displayed, as expected - Switch the active company to any other company, fo
Original PR description
The partner credit limit warning on quotations and customer invoices depends on which company the user is currently working in, instead of the company of the document itself. Steps to reproduce: -…
The partner credit limit warning on quotations and customer invoices depends on which company the user is currently working in, instead of the company of the document itself. Steps to reproduce: - Enable Sale Credit Limit in the settings of My Company (San Francisco) - Set a Credit Limit of 100 on a customer, e.g. Deco Addict - Create a draft quotation of 500 for that customer => The credit limit warning banner is displayed, as expected - Switch the active company to any other company, for example My Company (Chicago), keeping access to both companies - Open the same quotation again => The warning banner is gone, although neither the quotation nor the customer changed The credit fields used to build the warning are evaluated against the user's active company: credit_limit is a company-dependent field, and credit / credit_to_invoice are computed on the receivables of the current company. When the active company is not the document's company, the warning is checked against the wrong ledger and the wrong limit, so it can disappear on an over-limit customer or show up for a healthy one. Both computes already contain the line that was meant to handle this, but the result of with_company() was discarded, making it a no-op. Assign it, as every other compute in these files already does, so the warning is always evaluated in the document's company. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#276308
Some partners were registered on Peppol with EAS 9925 (Belgian VAT) but have since moved to 0208. They became unreachable via Peppol because we never check if they exist on the network with EAS 0208, which makes their status `not_valid`. This fix forces the re-checking of the status with EAS 0208 for partners having EAS 9925 and a `not_valid` status. task-6296017 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#270742
Original PR description
Some partners were registered on Peppol with EAS 9925 (Belgian VAT) but have since moved to 0208. They became unreachable via Peppol because we never check if they exist on the network with EAS 0208, which makes their status `not_valid`. This fix forces the re-checking of the status with EAS 0208 for partners having EAS 9925 and a `not_valid` status. task-6296017 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#270742
Steps to reproduce: - Create a partner without pincode - Create a picking and Challan for that partner - Click print Will result in the following traceback- ```py Traceback (most recent call last): File "<1103>", line 710, in template_l10n_in_ewaybill_report_ewaybill_1103 File "<1103>", line 692, in template_l10n_in_ewaybill_report_ewaybill_1103_content File "<1103>", line 674, in template_l10n_in_ewaybill_report_ewaybill_1103_t_call_0 File "<1103>", line 86, in template_l10n
Original PR description
Steps to reproduce: - Create a partner without pincode - Create a picking and Challan for that partner - Click print Will result in the following traceback- ```py Traceback (most recent call last):…
Steps to reproduce:
- Create a partner without pincode
- Create a picking and Challan for that partner
- Click print
Will result in the following traceback-
```py
Traceback (most recent call last):
File "<1103>", line 710, in template_l10n_in_ewaybill_report_ewaybill_1103
File "<1103>", line 692, in template_l10n_in_ewaybill_report_ewaybill_1103_content
File "<1103>", line 674, in template_l10n_in_ewaybill_report_ewaybill_1103_t_call_0
File "<1103>", line 86, in template_l10n_in_ewaybill_report_ewaybill_1103_t_call_1
File "/home/odoo/odoo18/community/addons/l10n_in_ewaybill/models/l10n_in_ewaybill.py", line 727, in _ewaybill_generate_direct_json
**self._prepare_ewaybill_base_json_payload(),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/odoo18/community/addons/l10n_in_ewaybill_stock/models/l10n_in_ewaybill.py", line 289, in _prepare_ewaybill_base_json_payload
ewaybill_json = super()._prepare_ewaybill_base_json_payload()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/odoo18/community/addons/l10n_in_ewaybill/models/l10n_in_ewaybill.py", line 669, in _prepare_ewaybill_base_json_payload
**prepare_details(
^^^^^^^^^^^^^^^^
File "/home/odoo/odoo18/community/addons/l10n_in_ewaybill/models/l10n_in_ewaybill.py", line 641, in prepare_details
f"{place}{key}": fun(partner, place) if key == "StateCode" else fun(partner)
^^^^^^^^^^^^
File "/home/odoo/odoo18/community/addons/l10n_in_ewaybill/models/l10n_in_ewaybill.py", line 674, in <lambda>
"Pincode": lambda p: int(p.zip) if p.country_id.code == "IN" else 999999,
^^^^^^^^^^
ValueError: invalid literal for int() with base 10: ''
```
In this commit, we resolve the traceback
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-prThis fixes two bugs in the web push subscription flow: - register_devices() compared partner records with 'is not' instead of '!='. Records loaded via sudo() live in a different environment than self.env.user, so 'is not' was always True and the ownership guard never behaved as intended. Use '!=', which compares record identity by model and id as Odoo's ORM intends. - webclient.js sent the previous subscription endpoint under the snake_case key 'previous_endpoint', while the server reads i
Original PR description
This fixes two bugs in the web push subscription flow:
- register_devices() compared partner records with 'is not' instead of '!='. Records loaded via sudo() live in a different environment than self.env.user, so 'is not' was always True and the ownership guard never behaved as intended. Use '!=', which compares record identity by model and id as Odoo's ORM intends.
- webclient.js sent the previous subscription endpoint under the snake_case key 'previous_endpoint', while the server reads it as 'previousEndpoint' (kw.get('previousEndpoint', endpoint)). The mismatch meant the lookup always fell back to the new endpoint, so a refreshed subscription created a duplicate device instead of updating the existing one. Send the camelCase key to match the server.
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#276082Miscellaneous changes
Fix field name typo. @qrtl QT6381 Forward-Port-Of: odoo/odoo#276119
Original PR description
Fix field name typo. @qrtl QT6381 Forward-Port-Of: odoo/odoo#276119