Wednesday, August 26, 2026
33 changes · saas-19.4
Resolved issues and error corrections
The Peru sales ledger now reports the gross value of sales when a 3% IGV withholding applies, instead of reducing the sale total by that withholding. This aligns the report with SUNAT expectations, since the withholding is handled as a payment-time mechanism rather than a reduction of the sale amount.
Original PR description
The 3% IGV withholding is a negative sale tax, so it reduced amount_total and the 14.4 ledger reported a net total. SUNAT expects the gross total of the operation, the withholding being a payment-time mechanism. task-5935227 Forward-Port-Of: odoo/enterprise#128875 Forward-Port-Of: odoo/enterprise#128849
Fixed a timing issue that could cause point-of-sale orders using AvaTax to reload incorrectly after payment, sometimes making the selected order line disappear. The change improves reliability during checkout and strengthens the related automated test so the issue is less likely to return.
Original PR description
The POS Avatax tour sporadically failed after returning from the payment screen. The race was reproduced locally by delaying the mocked Avatax response in mocked_request(). There's three somewhat…
The POS Avatax tour sporadically failed after returning from the payment screen. The race was reproduced locally by delaying the mocked Avatax response in mocked_request(). There's three somewhat related fixes. Firstly, get_order_tax_details() calls sync_from_ui(), which emits a SYNCHRONISATION notification. Unlike the normal POS sync path, the Avatax RPC did not pass the device context. The browser therefore treated its own notification as coming from another device and started an independent reload of open orders. That reload could replace the current order state after the tour returned to the product screen, causing the selected order line to disappear. We now pass the normal sync context so the browser can properly ignore its own notification. Secondly, we'll keep the complete sync_from_ui() response and replace its order, line, tax, and tax group data after the AvaTax calculation. We then simplify the processing client-side by moving towards the established pattern in the POS: missingRecursive() to load any other referenced records, and then pass that through loadConnectedData(). Lastly, clickPayButton() only waits for the payment screen element to be displayed. The AvaTax request starts from the screen's onMounted() callback, leaving a short window where the screen and its buttons exist but the request and UI blocker have not started yet. The next tour step can probably run during that window. To make sure this can't happen we explicitly waitRequest(). This first waits for requests to appear and then waits for them to complete. runbot-error-944281 Forward-Port-Of: odoo/enterprise#125944
Copied project tasks now preserve the correct dependency order between their sub-tasks. This prevents duplicated tasks, task templates, and recurring tasks from showing misleading or reversed dependency chains, helping teams rely on copied project plans without manual cleanup.
Original PR description
**Problem:** Duplicating a task, creating a task from a task template, or generating the next occurrence of a recurring task scrambles the dependencies between its sub-tasks: each copied sub-task…
**Problem:** Duplicating a task, creating a task from a task template, or generating the next occurrence of a recurring task scrambles the dependencies between its sub-tasks: each copied sub-task carries the dependencies of a different sub-task instead of its own. **Steps to reproduce:** 1. Enable Task Dependencies on a project. 2. Create a task with three sub-tasks and chain them: the second depends on the first, the third depends on the second. 3. Duplicate the task, or use "Create from template" if the task is a template. 4. Open the sub-tasks of the new task and look at their dependencies. **Current behavior:** The dependencies of the copied sub-tasks are shifted: the chain runs in the reverse order of the original one. **Expected behavior:** Each copied sub-task depends on the copy of the sub-task its original depended on, so the new task reproduces the original chain. **Cause of the issue:** `_create_task_mapping` builds the original to copy mapping by pairing `original_task.child_ids` with `copied_task.child_ids` positionally, on the assumption stated in its docstring that both recordsets share the same index order. They do not. `project.task._order` ends with `id desc`, so `child_ids` is read newest-first, while the copies are created by iterating the original `child_ids` in that same order. The copies' ids therefore ascend along the original list, and reading them back through `child_ids` returns them in the exact reverse order. `zip` then pairs each original with the copy of the sub-task at the mirrored position, and `_resolve_copied_dependencies` writes every `depend_on_ids` and `dependent_ids` onto the wrong copy. This affects every caller of that method: `copy`, the task template action, and the creation of the next occurrences of a recurring task. **Fix:** Sorting the copied children by id restores the correspondence because id order is the order in which the copies were created from the original list, an invariant that holds whatever `_order` does, whereas the previous code silently depended on `_order` producing the same sequence on both sides. `test_duplicate_project_with_subtask_dependencies` and `test_recurrence_copy_task_dependency` were reading the copies by `child_ids` index too, which the mirrored mapping happened to satisfy, so they passed on a wrong result; they now index them in creation order as well. opw-6386578 Forward-Port-Of: odoo/odoo#284211 Forward-Port-Of: odoo/odoo#280893
This fix prevents a signer who appears more than once in the same document from being prompted to sign again before the intervening signer has completed their step. It helps ensure agreements follow the configured signing sequence and reduces the risk of documents being completed out of order.
Original PR description
### Steps to Reproduce: 1. Create a sign request and have 3 total signers (User, Customer, Employee) 2. Enable Signing Order and make the order as follows: (1) User, (2) Customer, (3) Employee But…
### Steps to Reproduce: 1. Create a sign request and have 3 total signers (User, Customer, Employee) 2. Enable Signing Order and make the order as follows: (1) User, (2) Customer, (3) Employee But make the User and Employee the same contact 3. Send and sign the request > Notice that (1) is able to sign for (3) immediately after, (2) has not signed yet. ### Description of the issue/feature this PR addresses: **Issue:** The signing order is ignored when the same user has to sign multiple times on a document, even if it is configured for a different person to sign in between. This happens because all signature request items are initialized in the 'sent' state upon creation, rather than strictly advancing based on the order. As a result, the system prematurely allows users to sign out of order and prompts them with their next turn too early. **Solution:** To resolve this, the controller was updated to include an `is_mail_sent = True` domain filter. This ensures that the UI's post-sign popup only displays documents where it is explicitly the user's active turn, rather than prompting a premature sign. ### Current behavior before PR: Users are able to sign prematurely, and the system will disregard the configured signing order. ### Desired behavior after PR: Users will only be prompted and able to sign a document when it is explicitly their turn, per the `mail_sent_order`. This way, documents are signed in order. opw-6417327 Forward-Port-Of: odoo/enterprise#128487 Forward-Port-Of: odoo/enterprise#125573
Employees can now check in from the attendance shortcut when their employee record belongs to a non-default company. This prevents the check-in button from disappearing or selecting the wrong company in multi-company setups.
Original PR description
Steps to reproduce: - Install employees and attendance app - Make sure there are 2 companies - Make user's employee record for Company B, but not A - Make company A the default company for user - Enable "attendances from backend" setting - Click on the attendance dot (systray) Current Behavior: The dot disappears and you can't check in Expected Behavior: You are able to check in Other bug scenario: If you have employee records in both Company A and Company B, you can check in. However, you can never check in for Company B as the default company is always selected in the server code opw-6392301 Forward-Port-Of: odoo/odoo#281082 Forward-Port-Of: odoo/odoo#278377
Customers using Authorize.Net can once again save a new payment method from the portal. The fix stores the payment details before the authorization is cancelled, preventing the saved method from being lost.
Original PR description
Issue: --- Authorize payment tokenization doesn't work. Steps: 1- Setup authorize payment provider. 2- Using portal page, add a new payment method for the user. The created payment method is not…
Issue: --- Authorize payment tokenization doesn't work. Steps: 1- Setup authorize payment provider. 2- Using portal page, add a new payment method for the user. The created payment method is not saved. Cause: --- The issue was introduced in efc2788dfccd13ee6feb309430ff57e49664ff97. Before that, we were calling `_tokenize` before voiding the tx. In that PR, the `_tokenize` call was moved to `_process()`, after `_apply_updates()`. So now what happens is that we void the tx, then call `_tokenize()`. Inside tokenize we try to create a customer profile, which fails because the tx is already voided. Fix: --- We can fix it by calling `_tokenize()` once before voiding the tx. The redundant tokenize call inside the general payment tx `_process` is rendered ineffective by two safeguards: 1- There is a check for `tx.tokenize`, which neutralizes double tokenization: https://github.com/odoo/odoo/blob/fffd987cc98d1ea0cd04e24dda2ed8b64a219cdc/addons/payment/models/payment_transaction.py#L754-L755 https://github.com/odoo/odoo/blob/fffd987cc98d1ea0cd04e24dda2ed8b64a219cdc/addons/payment/models/payment_transaction.py#L893-L896 2- If `token_id` is already set, no token value is returned: https://github.com/odoo/odoo/blob/fffd987cc98d1ea0cd04e24dda2ed8b64a219cdc/addons/payment_authorize/models/payment_transaction.py#L237-L243 opw-6426847 Forward-Port-Of: odoo/odoo#283134 Forward-Port-Of: odoo/odoo#281014
Fixed a point-of-sale issue where eWallet or gift card payments could discount an order by one cent less than the amount taken from the card when certain tax settings were used. This keeps the customer charge aligned with the redeemed card balance and prevents small payment discrepancies.
Original PR description
When paying with an eWallet or gift card in POS, the reward line could end up 0.01 short of the actual card balance if the discount product's tax is configured as tax-excluded through a per-tax…
When paying with an eWallet or gift card in POS, the reward line could end up 0.01 short of the actual card balance if the discount product's tax is configured as tax-excluded through a per-tax override, regardless of the tax's own default configuration. The card is still debited for the full balance, but the order is only discounted by one cent less, so the amount charged to the customer no longer matches the amount consumed from the card. Steps to reproduce: ------------------- * Top up an eWallet (or gift card) with a balance of 10.00 * On the eWallet/gift card program's discount product, set an 18% tax whose Tax Computation is overridden to "Excluded" (price_include_override = tax_excluded), independently of the company's default tax configuration * In POS, add a product to an order and pay (partly) with that eWallet/gift card > Observation: Only 9.99 is deducted from the order total, while the backend correctly shows 10 consumed on the wallet/gift card. Why the fix: ------------ The reward line's price_unit was reconstructed from a one-time backward tax computation, then kept only the tax amount for taxes whose price_include field was true, dropping it for any tax forced excluded. That price_unit was later re-taxed forward using the tax's real (excluded) configuration, and the two roundings don't agree for rates like 18%, losing a cent. We now force special_mode "total_included" whenever an eWallet/gift card reward line's taxes are computed, not just at creation, so its tax-included total always equals the exact redeemed amount regardless of how the tax is configured, and store price_unit as that target amount directly. opw-5819389 Forward-Port-Of: odoo/odoo#284397 Forward-Port-Of: odoo/odoo#278568
Website pages are now refreshed correctly when a visitor changes their cookie preference from denied to accepted. This prevents visitors from seeing an outdated cached version of a page and helps ensure the website reflects the correct consent-based behavior.
Original PR description
Initially with [commit 958b41c4], when cookies were denied (the page is cached a 1st time), then accepted (the page cache must be invalidated), cached pages would be computed again. This behavior was lost with [6c8a90ec], since which website pages are cached more aggressively. [commit 958b41c4]: https://github.com/odoo/odoo/commit/958b41c4acec7e1700ca4d6e0b25ee0ad2aac9f1 [6c8a90ec]: https://www.github.com/odoo/odoo/commit/6c8a90ecba45fb99addf1b86fe237fd626fba650 task-6471290 Forward-Port-Of: odoo/odoo#283863 Forward-Port-Of: odoo/odoo#282737
A problem during failed subscription payments could make the recurring billing process crash instead of handling the failed transaction cleanly. This fix prevents that interruption, helping automated subscription invoicing continue more reliably when a saved payment method is invalid.
Original PR description
Step to reproduce: - create a faulty token that won't work and link it to a subscription - launch the recurring invoice cron - the following traceback occurs ``` last_tx_sudo = (self.transaction_ids…
Step to reproduce:
- create a faulty token that won't work and link it to a subscription
- launch the recurring invoice cron
- the following traceback occurs
```
last_tx_sudo = (self.transaction_ids - existing_transactions).sudo()
```
When the payment fails, the system rollback and we store the last_tx_sudo value in a dedicated variable. After rollback, the record does not exists anymore. Therefore, accessing the value fails.
```
File "/home/odoo/src/enterprise/saas-18.2/sale_subscription/models/sale_order.py", line 1703, in _handle_automatic_invoices
if not last_tx_sudo or last_tx_sudo.renewal_state in ['pending', 'authorized']:
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/saas-18.2/odoo/orm/fields.py", line 1439, in __get__
self.compute_value(record)
File "/home/odoo/src/odoo/saas-18.2/odoo/orm/fields.py", line 1603, in compute_value
records._compute_field_value(self)
File "/home/odoo/src/odoo/saas-18.2/odoo/orm/models.py", line 4575, in _compute_field_value
determine(field.compute, self)
File "/home/odoo/src/odoo/saas-18.2/odoo/orm/fields.py", line 69, in determine
return needle(*args)
^^^^^^^^^^^^^
File "/home/odoo/src/enterprise/saas-18.2/sale_subscription/models/payment_transaction.py", line 25, in _compute_renewal_state
if tx.state in ['draft', 'pending']:
^^^^^^^^
File "/home/odoo/src/odoo/saas-18.2/odoo/orm/fields.py", line 1406, in __get__
raise MissingError("\n".join([
odoo.exceptions.MissingError: Record does not exist or has been deleted.
```
Moreover, since https://github.com/odoo/enterprise/pull/45236/files#diff-c36fd7952cc2bef40716419a668de41963d49e1aa4177d9319d503fc260da588R1678-R1682
```
if not last_tx_sudo or not last_tx_sudo.renewal_state not in ['pending', 'authorized']:
```
has become
```
if not last_tx_sudo or last_tx_sudo.renewal_state in ['pending', 'authorized']:
```
But it feels strange to unlink the invoice when the payment succeed.
This PR fixes it.
Forward-Port-Of: odoo/enterprise#83913Refunds made in Odoo are now correctly recognized when Stripe sends a refund confirmation webhook. This prevents duplicate refund records for the same Stripe refund, improving payment accuracy and reducing reconciliation confusion.
Original PR description
Steps to reproduce: - Configure Stripe with manual capture. - Authorize and capture an online payment. - Refund the captured payment from Odoo. - Let the `charge.refunded` webhook be processed. The refund initiated from Odoo is created as a child of the capture transaction, while the webhook resolves the charge to the source transaction. The webhook only checked direct refund children of that source transaction, so it missed the existing refund and created a second refund transaction with the same Stripe refund reference. Look up existing Stripe refund transactions in the child and grandchild transactions of the source transaction before creating webhook refund transactions, so the webhook recognizes refunds already created under capture children. opw-6359020 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#283790 Forward-Port-Of: odoo/odoo#276154
Intrastat reports now only include rental orders when the rental duration meets the required two-year threshold. This prevents short-term rentals from being reported incorrectly, improving compliance and report accuracy.
Original PR description
Problem: Some rental orders are showing in Intrastat reports when they should not be showing. Only rental orders with duration of 2 years or more should be shown in Intrastat reports. However, all rental orders are being shown. <img width="783" height="768" alt="intrastat_leasing" src="https://github.com/user-attachments/assets/7419e3dc-7b3e-4234-809f-6973fef93fc1" /> Cause: When querying the lines to show in the Intrastat report, there is no condition that checks for the duration of rental orders. opw-6351456 Forward-Port-Of: odoo/enterprise#125042
Timesheets now automatically clear a selected task when the project is changed to one that does not include that task. This prevents incorrect project-task combinations when users update timesheets in bulk or through automated processes, improving data accuracy for reporting and billing.
Original PR description
When modifying project_id on a timesheet through mass edit/rpc or anything that is not triggering `onChange`. The task_id would not be reset if it doesnt' belong to the new project set on the timesheet. Steps to reproduce: ------------------- * Install studio for easier reproducing of the issue * Open the timesheet list view * Open studio and activate the mass edit on the view * Modify the project_id on multiple records > Observation: The task_id stays the same even if they do not belong to the new set project Why the fix: ------------ Instead of relying only on the onChange we add an inverse to the project_id that will reset the task when needed. opw-6259149 Forward-Port-Of: odoo/odoo#283882
Argentina accounting users can now create invoices for foreign customers even when export sales journals are unavailable or archived. The system falls back to a standard Invoice B document type, avoiding an unnecessary workflow block while keeping invoice creation possible.
Original PR description
### Issue before this commit: Before this commit, users were completely blocked from creating an invoice for a foreign partner (e.g., "Cliente del Exterior") if all exportation journals were archived…
### Issue before this commit: Before this commit, users were completely blocked from creating an invoice for a foreign partner (e.g., "Cliente del Exterior") if all exportation journals were archived or unavailable, as the system would immediately trigger a RedirectWarning error. ### Steps to reproduce the issue: 1. Download Accounting and l10n_ar 2. Go to contacts and create a new one with: 1. Country as United States 2. VAT number ex. 55000002126 3. AFIP Responsibility Type as Cliente del Exterior 3. Go to Journals, filter for sales journals and archive: 1. Electronic Exportation Invoice (FEX) 2. Expo Sales Journal 4. Go to invoices and create a new one for the client you just created 5. As soon as you insert the client you will receive the error: You are trying to create an invoice for foreign partner but you don't have an exportation journal ### Cause of the issue: https://github.com/odoo/odoo/blob/014d58e3204d17db6dcba3c8ab7d8ad35003300e/addons/l10n_ar/models/account_move.py#L186-L189 The _onchange_partner_journal method rigidly enforced the use of an exportation journal for foreign AFIP responsibility types (codes 8, 9, and 10). If the query failed to find an active export journal, the code intentionally threw a hard error instead of providing a fallback mechanism. ### Reason to introduce the fix: This fix is introduced to prevent unnecessary workflow blocks. By catching the missing journal and defaulting the document type to "Invoice B" (code 6), the user can now successfully generate the invoice using a standard domestic sales journal without being forced to configure an exportation journal. opw-6442501 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#282971
Odoo now prevents users from selecting Peppol identifier codes that are deprecated or removed from the official specification. This helps avoid invalid Peppol registrations and partner records when creating or duplicating contacts.
Original PR description
Peppol EAS codes 0037, 0213, 9955, and 0193 are deprecated or removed from the Peppol specification but are still present in the selection field on stable branches, allowing users to register invalid identifiers. See: [eas codes](https://docs.peppol.eu/edelivery/codelists/v9.7/Peppol%20Code%20Lists%20-%20Participant%20identifier%20schemes%20v9.7.html) Before: - deprecated EAS codes were listed alongside valid ones in the partner's available Peppol EAS options, allowing users to select an outdated identifier for new or duplicated partners, or during Peppol registration. After: - Excluded deprecated EAS codes from the available Peppol EAS selection list on partners, preventing users from selecting them for new or duplicated partners, or during Peppol registration. Removed Deprecated codes in Master: odoo/odoo#271288 Task [link](https://www.odoo.com/odoo/project.task/6299691) task-6299691 Forward-Port-Of: odoo/odoo#283698 Forward-Port-Of: odoo/odoo#271435
This fix improves how timesheet suggestions created from calendar events are linked to the right project or task. It also corrects duration calculations when overlapping time entries are adjusted, helping users see more reliable timesheet information.
Original PR description
task: 6435164 Forward-Port-Of: odoo/enterprise#126680
Chilean electronic invoices received by email now import correctly when they include foreign-currency line details but do not provide a foreign-currency total in the header. This prevents failed imports and helps businesses process supplier invoices without manual intervention.
Original PR description
When importing an incoming DTE through the fetchmail server, the total amount is read from the MntTotOtrMnda as soon as a Moneda node is present in the document. Steps to reproduce: - Set up a CL company with a DTE mail server - Fetch a DTE that includes the line-level Moneda node but does not include the header OtraMoneda block, so no MntTotOtrMnda - Run the fetchmail cron and check the logs Issue: The DTE fails to import Analysis: Occurs since https://github.com/odoo-dev/enterprise/commit/5805a92f91411846fdffa245cb047397cfc9b1f3 Moneda is defined at line level while MntTotOtrMnda in the optional header block Encabezado/OtraMoneda. Instead of assuming MntTotOtrMnda is always present whenever the document carries a foreign currency, fall back to the base-currency total MntTotal when it is missing. opw-6432612 Forward-Port-Of: odoo/enterprise#128766 Forward-Port-Of: odoo/enterprise#126869
This fixes an issue where a required operation-level quality check could disappear after partially receiving goods in the Barcode app and returning to the transfer. Businesses can now rely on quality controls remaining in place until the full receipt is properly handled, reducing the risk of missed inspections.
Original PR description
Steps to reproduce --- 1. Create a quality control point on the Receipts operation type with Control per set to Operation. 2. Confirm a receipt of 2 units of the product: one pending operation…
Steps to reproduce --- 1. Create a quality control point on the Receipts operation type with Control per set to Operation. 2. Confirm a receipt of 2 units of the product: one pending operation quality check is created. 3. In the Barcode app, receive 1 unit and go back to the transfer with the back button. 4. The pending operation quality check is gone. Issue --- Going back from the Barcode app calls `post_barcode_process`, which on a partial reception splits the picked move into a done move and a remaining move, then merges the transient duplicate back with `_merge_moves`. https://github.com/odoo/enterprise/blob/b89614661682ecbd131d940539aac8afdd9d7289/stock_barcode/models/stock_move.py#L57-L60 `_merge_moves` cancels that transient duplicate through `_action_cancel` before unlinking it. https://github.com/odoo/odoo/blob/8f3100ca597559945cc42d9ef9517edbb40a900b/addons/stock/models/stock_move.py#L1400-L1401 The `quality_control` override of `_action_cancel`, picks the pending checks to drop from `is_product_canceled`, a `defaultdict(lambda: True)` keyed by `(picking, product_id)`. An operation check has no `product_id`, so its key is never computed by the loop and reads back the `True` default, so it is deleted even though the transfer still has a live move. Since an operation check covers the whole transfer, it must be dropped only when every move of its picking is cancelled. https://github.com/odoo/enterprise/blob/b89614661682ecbd131d940539aac8afdd9d7289/quality_control/models/stock_move.py#L68-L76 opw-6439179 Forward-Port-Of: odoo/enterprise#129051 Forward-Port-Of: odoo/enterprise#127427
Changing the project on timesheet entries through mass editing or automated updates now clears any task that does not belong to the new project. This prevents incorrect project-task combinations and helps keep timesheet reporting accurate.
Original PR description
When modifying project_id on a timesheet through mass edit/rpc or anything that is not triggering `onChange`. The task_id would not be reset if it doesnt' belong to the new project set on the timesheet. Steps to reproduce: ------------------- * Install studio for easier reproducing of the issue * Open the timesheet list view * Open studio and activate the mass edit on the view * Modify the project_id on multiple records > Observation: The task_id stays the same even if they do not belong to the new set project Why the fix: ------------ Instead of relying only on the onChange we add an inverse to the project_id that will reset the task when needed. opw-6259149 Forward-Port-Of: odoo/enterprise#128799
Financial report snapshots are now blocked when an open-ended tax or fiscal lock exception keeps a period editable. This prevents users from seeing outdated report amounts and removes snapshots created during that exception period.
Original PR description
An open-ended fiscal or tax lock exception keeps the period editable, but snapshot generation did not consider it and could serve stale amounts. Prevent snapshots while a full exception is active and clear snapshots created during it. opw-6427776 Forward-Port-Of: odoo/enterprise#127525
The point of sale variant selection popup now shows the truly available quantity after reserved stock is deducted. This prevents staff from seeing misleading stock levels when items have already been committed to sales orders.
Original PR description
## Steps to reproduce: - Create another warehouse - Create a product with a variant, like Color, values black and white - Track the product, add a qty on hand of 50 on the black product - Go to the…
## Steps to reproduce: - Create another warehouse - Create a product with a variant, like Color, values black and white - Track the product, add a qty on hand of 50 on the black product - Go to the sales app, make a quotation of 50 for the black product - Confirm the quotation - Go to the PoS, click on the product, check the available qty in the popup - It is still 50, even though the forecasted is correct at 0 ## Why the fix: Having the actual free qty was added in this commit 682bc82 to be able to check the qty that was really free instead of the available qty. This means that we subtract the reserved_qty from the qty_available to get the free_qty. The variant popup was forgotten in this commit, so it was still displaying the qty_available. This is why there was a difference in the qty if we press the product normally or if we long press it, because the variant popup was forgotten in said commit. opw-6382845 Forward-Port-Of: odoo/odoo#284321 Forward-Port-Of: odoo/odoo#280330
The email editor now skips unsupported figure layouts, such as figures without an image or with multiple images, instead of failing. This prevents Helpdesk tickets created from incoming emails from showing an error when those emails contain valid but uncommon figure formatting.
Original PR description
**Steps to reproduce:** - Install Helpdesk - Create an email with a figure that has no image - Send it to Helpdesk email alias - Open up auto-created ticket from the email - `OwlError` is raised on `CaptionPlugin.addImageCaption` **Issue:** `CaptionPlugin` [1] was designed for `<figure>` elements with a single `<img>` and a single `<figcaption>` (mainly for editor direct interactions). But the HTML specifications also allow `<figure>` with 0 or more than 1 `<img>` element(s), in which case an error is raised (or some elements are removed). (see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/figure) **Fix:** Ignore such `<figure>` for now as it would require a rework of the plugin. [1] https://github.com/odoo/odoo/commit/b9d112a5800cfe11dc434caa0d335fa3f3db7178 opw-6413422 Forward-Port-Of: odoo/odoo#284139 Forward-Port-Of: odoo/odoo#279981
UPS shipping rate requests now accept phone numbers that match UPS rules, including shorter numbers used in countries such as Luxembourg. This prevents valid customers from being blocked when adding UPS shipping to a quotation.
Original PR description
**Steps to reproduce:** - Create a contact with a phone number that has 9 characters - Setup an UPS carrier, a configuration that works is UPS Saver as Service Type and UPS Package/customer supplied…
**Steps to reproduce:** - Create a contact with a phone number that has 9 characters - Setup an UPS carrier, a configuration that works is UPS Saver as Service Type and UPS Package/customer supplied as a package type - Create a quotation, put the created contact as a client - Try adding a shipping and getting the rates - An User Error appears, the phone number is too short **Why the fix:** Before this commit, any phone number that was less than 10 characters would raise an User Error, but some countries, such as Luxembourg, use phone numbers that are nine characters long or even less. If we check the official UPS documentation (https://developer.ups.com/tag/Shipping?loc=en_EN#operation/Shipment), we can see in the Ship_to/Phone section, that the phone number should be a number between 1 and 15, not saying it should be 10 characters or more. <img width="495" height="473" alt="image" src="https://github.com/user-attachments/assets/fed82987-ffb8-4b84-b282-6c3d3b4f304e" /> After this commit, we adapt the way we prevent the user from inputing phone numbers to fit the official UPS documentation. opw-6307577 Forward-Port-Of: odoo/enterprise#128632 Forward-Port-Of: odoo/enterprise#122831
Deleting a draft invoice for timesheet-based services no longer removes or changes the sales order item tied to the original timesheets. This prevents billed hours from disappearing from the original order or being reassigned incorrectly, keeping delivered quantities accurate when invoices are cancelled and recreated.
Original PR description
Deleting a draft customer invoice linked to timesheets resets their timesheet_invoice_id so the hours become invoiceable again. This write also marks the timesheets' so_line for recompute, and the…
Deleting a draft customer invoice linked to timesheets resets their timesheet_invoice_id so the hours become invoiceable again. This write also marks the timesheets' so_line for recompute, and the re-derivation runs while the lines are no longer protected by the invoice link. When the task or project no longer resolves to a sale order item (e.g. it was unlinked after invoicing), the timesheets lose their sale order item or get reassigned to another one, so the delivered hours silently disappear from the original order line. Protect so_line during the write and drop the pending recompute: deleting an invoice must only make the hours invoiceable again, not change their allocation. Steps to reproduce: - Install Sales and Timesheets - Create a service product with invoice policy "Based on Timesheets" and "Create a task in a new project" - Create and confirm a sale order with this product - Log a timesheet on the generated task - Create the invoice (keep it in draft) - Remove the Sales Order Item from the task and from the project settings (or point them to a sale order item of another order) - Delete the draft invoice - Open the timesheet: its Sales Order Item is emptied (or replaced by the other order's item, whose delivered quantity now includes the hours sold on the original order), and the original line's delivered quantity is reset --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#283831 Forward-Port-Of: odoo/odoo#279552
Fixes an accounting reconciliation error that occurred when users worked with a parent company and branch company selected at the same time. The change ensures the correct company is used for currency conversion, allowing journal items in multi-company setups to reconcile without crashing.
Original PR description
When having multiple companies selected at the same time, _get_conversion_rate returns: File "/data/build/odoo/odoo/orm/fields_misc.py", line 114, in get raise ValueError("Expected singleton: %s" %…
When having multiple companies selected at the same time, _get_conversion_rate returns:
File "/data/build/odoo/odoo/orm/fields_misc.py", line 114, in get
raise ValueError("Expected singleton: %s" % record)
1 - Create a new company with currency EUR.
2 - Create a branch company underneath the main company.
3 - In Accounting, install fiscal localization, e.g. Belgian Companies on the company configuration settings.
4 - Select an account like 600000 Raw Materials, and enable Allow Reconciliation on this account. The exact account isn't important, only that we can make credits / debits to it to be reconciled.
5 - With only the top level company selected, make a debit of 100 USD, e.g. Vendor Bill, set in currency USD to the account 600000.
6 - Now with only the branch level company selected, make a credit of 100EUR, e.g. Customers Invoices, set in currency EUR to the same account with an amount equal to the credit in step 5. (if 1USD == 1EUR, 1-1), so that there is no residual amount, i.e. credit == debit.
7 - Now select both the top level company and the sub branch company in the company context.
8 - In Journal Items, reconcile the unreconciled journal items for the Account 600000.
With this commit we select the first company of the aml instead of every companies on the amls.
opw-6290703
Forward-Port-Of: odoo/enterprise#123774Fixes seven mislabeled entries in the Mexican chart of accounts so exported electronic accounting files use the official SAT descriptions. This helps Spanish-language Mexican databases report account group names accurately while leaving trial balance and journal policy exports unchanged.
Original PR description
Seven entries of the Mexican chart of accounts template carry a name belonging to a **different** group, copied from a neighbouring entry. Each record's XML ID still states the intended name, which…
Seven entries of the Mexican chart of accounts template carry a name belonging
to a **different** group, copied from a neighbouring entry. Each record's XML ID
still states the intended name, which is what this restores.
| Code | Field | Before | After |
|---|---|---|---|
| `6` | `name@es` | Gastos generales | Gastos |
| `252.07` | `name@es` | `account_subgroup_hipotecas_por_pagar_a_largo_plazo_nacional` | Hipotecas por pagar a largo plazo nacional |
| `602` | `name`, `name@es` | Cost of sales / Costo de venta | Selling expenses / Gastos de venta |
| `613` | `name@es` | Amortización contable | Depreciación contable |
| `614` | `name` | Accounting depreciation | Accounting amortisation |
| `701.06` | `name`, `name@es` | Interest on foreign bank charges / Intereses a cargo bancario extranjero | Interest payable by national natural persons / Intereses a cargo de personas físicas nacional |
| `702` | `name@es` | Utilidad cambiaria | Productos financieros |
### Why it is not cosmetic
The electronic accounting Chart of Accounts XML takes the `Desc` attribute of
every `<Ctas>` element from the *account group name* — `cfdicoa.xml`
(`t-att-Desc="account.get('name')"`), fed by `trial_balance.py`
`_l10n_mx_get_coa_values()`. Any `es_*` database therefore declares:
```xml
<catalogocuentas:Ctas CodAgrup="702" NumCta="702" Desc="Utilidad cambiaria" Nivel="1" Natur="A"/>
```
whereas the SAT catalogue (Anexo 24) publishes `702` as *Productos financieros*,
with `702.01 Utilidad cambiaria` … `702.10 Otros productos financieros` beneath
it. `CodAgrup` comes from `code_prefix_start` and stays correct, so the file
still validates against the XSD, but the declared description does not match the
official nomenclature. Trial Balance and Pólizas are unaffected — neither
exports group names.
### Evidence
- `252.07` contains its own XML ID as the Spanish name.
- `602` duplicates `501.01`, yet its children are `Sueldos y Salarios`,
`Compensaciones`, `Tiempos extras`.
- `613` and `614` are swapped in one language each: `613`'s children are
depreciations, `614`'s are amortisations.
- `701.06` duplicates `701.05` in both languages; the correct name is symmetric
to `701.07` and to `702.06`.
- `6` is the only single-digit root group whose Spanish name does not match its
XML ID (`account_group_gastos`).
### Notes
Introduced in d782b8b92557; correct in 15.0, where the names lived in
`account.account.tag.csv`. Still present in 18.0, 19.0 and master, hence
targeting 17.0. Template data only — existing databases are unaffected until the
chart is (re)installed, and renaming a group moves no balance.
Forward-Port-Of: odoo/odoo#277426
Forward-Port-Of: odoo/odoo#278328
Forward-Port-Of: odoo/odoo#277891UPS return shipments now show the required commercial invoice in the order chatter, matching outbound international shipments. The fix also prevents UPS delivery rejections for US customers using ZIP+4 postal codes by formatting those postal codes correctly before sending them to UPS.
Original PR description
Issues ----- 1. Commercial invoice is not forwarded to the user for the return delivery. 2. US postal codes of format 12345-6789 cause the delivery to be rejected. ----- Steps to reproduce issue 1…
Issues ----- 1. Commercial invoice is not forwarded to the user for the return delivery. 2. US postal codes of format 12345-6789 cause the delivery to be rejected. ----- Steps to reproduce issue 1 ----- - Set up UPS with return labels - Create an INTL delivery & confirm > OUT delivery has a commercial invoice in chatter, but the return doesn't Cause ----- The OUT and return call are not made using the same function. The OUT call is made via `ups_rest_send_shipping` which explicitly extracts the commercial invoice from the UPS response https://github.com/odoo/enterprise/blob/1a7c8ac34348ebc1ebe2da4100bdaec57484056f/delivery_ups_rest/models/delivery_ups.py#L204-L205 We should adapt `ups_rest_get_return_label` to match. ----- Steps to reproduce issue 2 ----- - Set up UPS - Create an american customer with a 9 digit zip (eg 20500-0003) - Create an delivery to the customer & confirm > Error: Invalid sold to postal code. Valid length is 0 to 9 alphanumeric Cause ----- The zip code is transmitted as-is, so we should sanitise it beforehand. https://github.com/odoo/enterprise/blob/c8c2f13b7fd17e215044fc62774f2b4a378aaf8c/delivery_ups_rest/models/ups_request.py#L368 Doc: https://github.com/UPS-API/api-documentation/blob/69e8a3cee7f9d3bf80735ae329aed0d8be156f97/Shipping.yaml#L5410-L5420 ----- Ticket: opw-6422500 Forward-Port-Of: odoo/enterprise#127375
This update corrects tax configuration details for Hungary in Odoo's local accounting and electronic invoicing setup. It helps ensure Hungarian tax records and related electronic tax handling are aligned with the expected configuration, reducing the risk of incorrect tax setup for affected companies.
Original PR description
Adjusting incorrect tax configuration elements for Hungary. task-6397915 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#284432 Forward-Port-Of: odoo/odoo#282697
Receipt emails for paid self-service orders now include the generated receipt image as an attachment. This ensures customers receive a complete proof of purchase by email, while draft orders remain unaffected.
Original PR description
Before this commit: ======================== * Receipt emails were sent without attachments for both paid and draft orders. * `fullTicketImage` and `basicTicketImage` were hardcoded to `false`. * As a result, paid orders were also sent without a receipt attachment. After this commit: ====================== * Receipt emails for paid orders now include the generated receipt image. * `fullTicketImage` and `basicTicketImage` are correctly handled to generate and attach the requested receipt image. Task-5353350 Forward-Port-Of: odoo/odoo#283947 Forward-Port-Of: odoo/odoo#237688
This fixes website redirects so newer search and AI crawlers can access default-language pages even when they send language preferences. It helps pages be inspected and indexed correctly while keeping normal visitor language behavior unchanged.
Original PR description
Modern crawlers now send an `Accept-Language` header (for example, `en-US,en;q=0.9`), whereas historically they did not. When that language differs from the website's default language,…
Modern crawlers now send an `Accept-Language` header (for example, `en-US,en;q=0.9`), whereas historically they did not. When that language differs from the website's default language, `ir.http._match()` issues a 303 redirect from `/page` to `/<lang>/page`. Since crawlers do not retain cookies, unrecognized agents are redirected on every request and never reach the default-language page. Customers reported that Google Search Console URL Inspection live tests only receive a redirect and that pages remain unindexed. Googlebot itself is not affected because it already matches the existing `bot` token. `_match()` already skips language redirects for recognized bots by serving the default-language page directly. Extend the `bots` user-agent list with modern crawler identifiers, each verified against vendor documentation: * `google-inspectiontool`: Search Console URL Inspection / Rich Results Test * `googleother`: Google generic crawler (`GoogleOther`, `GoogleOther-Image`, `GoogleOther-Video`) * `meta-external`: `meta-externalagent`, `meta-externalfetcher`, and `meta-externalads`, successors to the already-listed `facebookexternalhit` * `meta-webindexer`: Meta AI search indexer * `chatgpt-user`: OpenAI user-request fetcher (currently matched only through the `bot` substring in its info URL, which is fragile) * `claude-user`: Anthropic user-request fetcher * `perplexity-user`: Perplexity user-request fetcher The redirect behavior remains unchanged for human visitors. Localized pages continue to be crawlable through their own URLs (for example, `/fr/page`) via `hreflang` alternates. As a side effect, `link_tracker` and `mass_mailing_sms` no longer count clicks from these crawlers, and website visitor tracking skips them. task-6213245 Forward-Port-Of: odoo/odoo#275571
This fixes an error when filtering sales orders by information coming from linked project tasks, such as a task stage. Users can now apply these custom filters without the sales order search failing.
Original PR description
step to reproduce : 1. Create a related field on `sale.order`, for example: x_studio_production_stage = tasks_ids.stage_id.name 2. Use this field in a filter: [('x_studio_production_stage', 'ilike',…
step to reproduce :
1. Create a related field on `sale.order`, for example:
x_studio_production_stage = tasks_ids.stage_id.name
2. Use this field in a filter:
[('x_studio_production_stage', 'ilike', 'Dispatch')]
3. Applying the filter raises:
```python
Traceback (most recent call last):
File "/home/odoo/src/odoo/19.0/odoo/http.py", line 2329, in _serve_db
return service_model.retrying(serve_func, env=self.env)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/service/model.py", line 188, in retrying
result = func()
^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/http.py", line 2384, in _serve_ir_http
response = self.dispatcher.dispatch(rule.endpoint, args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/http.py", line 2599, in dispatch
result = self.request.registry['ir.http']._dispatch(endpoint)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/addons/base/models/ir_http.py", line 353, in _dispatch
result = endpoint(**request.params)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/http.py", line 838, in route_wrapper
result = endpoint(self, *args, **params_ok)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/addons/web/controllers/dataset.py", line 32, in call_kw
return call_kw(request.env[model], method, args, kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/service/model.py", line 97, in call_kw
result = method(recs, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/addons/web/models/models.py", line 67, in web_search_read
records = self.search_fetch(domain, specification.keys(), offset=offset, limit=limit, order=order)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/models.py", line 1408, in search_fetch
query = self._search(domain, offset=offset, limit=limit, order=order or self._order)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/models.py", line 5366, in _search
domain = domain.optimize_full(self)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 446, in optimize_full
return self._optimize(model, OptimizationLevel.FULL)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 460, in _optimize
previous, domain = domain, domain._optimize_step(model, next_level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 654, in _optimize_step
children = self._flatten(child._optimize(model, level) for child in self.children)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 609, in _flatten
for child in children:
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 654, in <genexpr>
children = self._flatten(child._optimize(model, level) for child in self.children)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 460, in _optimize
previous, domain = domain, domain._optimize_step(model, next_level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 654, in _optimize_step
children = self._flatten(child._optimize(model, level) for child in self.children)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 609, in _flatten
for child in children:
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 654, in <genexpr>
children = self._flatten(child._optimize(model, level) for child in self.children)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 460, in _optimize
previous, domain = domain, domain._optimize_step(model, next_level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 962, in _optimize_step
domain = self._optimize_field_search_method(model)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 1008, in _optimize_field_search_method
computed_domain = field.determine_domain(model, operator, value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/fields.py", line 1928, in determine_domain
return determine(self.search, records, operator, value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/fields.py", line 81, in determine
return needle(*args)
^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/addons/sale_project/models/sale_order.py", line 76, in _search_tasks_ids
query = self.env['project.task']._search(task_domain)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/models.py", line 5355, in _search
domain = Domain(domain)
^^^^^^^^^^^^^^
File "/home/odoo/src/odoo/19.0/odoo/orm/domains.py", line 259, in __new__
raise ValueError(f"Domain() invalid item in domain: {item!r}")
ValueError: Domain() invalid item in domain: ('id', 'any!', [('id', 'any!', <odoo.tools.query.Query object at 0x7aca184f4170>)])
```
Cause:
When searching on the related field, [_search_related()](https://github.com/odoo/odoo/blob/19.0/odoo/orm/fields.py#L768) converts the related path into an `any!` domain:
('tasks_ids', 'any!',
[('stage_id', 'any!', [('name', 'ilike', 'Dispatch')])]
)
During [Domain.optimize_full()](https://github.com/odoo/odoo/blob/19.0/odoo/orm/domains.py?utm_source=chatgpt.com#L436), [_optimize_field_search_method()](https://github.com/odoo/odoo/blob/19.0/odoo/orm/domains.py?utm_source=chatgpt.com#L1008) calls the field's search method, which invokes `_search_tasks_ids()` with `operator='any!'` and the related domain as `value`.
The existing [_search_tasks_ids()](https://github.com/odoo/odoo/blob/57c7c9938725d392a6f2cd6c89a861d2a8385c44/addons/sale_project/models/sale_order.py#L76) expects a normal search value and therefore generates an invalid nested domain.
Fix :
`_search_tasks_ids()` to directly pass the domain to `project.task._search()` when the operator is `any` or `any!`.
upg - 4584778
opw - 6475804
[here]: https://github.com/odoo/odoo/blob/19.0/odoo/orm/fields.py#L768
[here]: https://github.com/odoo/odoo/blob/19.0/odoo/orm/models.py#L5366
[here]: https://github.com/odoo/odoo/blob/19.0/odoo/orm/domains.py?utm_source=chatgpt.com#L436
[here]: https://github.com/odoo/odoo/blob/57c7c9938725d392a6f2cd6c89a861d2a8385c44/addons/sale_project/models/sale_order.py#L76
Description of the issue/feature this PR addresses:
Current behavior before PR:
Desired behavior after PR is merged:
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#284101This fix ensures users granted editor rights on a Documents folder can update that folder's sharing settings as intended. It removes a permissions blocker that prevented legitimate editors from changing internal user access, improving consistency in folder management.
Original PR description
1. Create a non-company root folder 2. Edit rights as follows: * add Marc Demo as editor member * access for internal users and link to None 3. As Marc Demo, try updating Internal users access to "editor" ⮕ You can't. Task-6410610 Forward-Port-Of: odoo/enterprise#129054 Forward-Port-Of: odoo/enterprise#125191
Reauthorizing a Shopee shop now correctly links it to the selected Shopee account when different API credentials are used. This prevents shops from staying connected to the wrong account after reauthorization, reducing setup errors for sales integrations.
Original PR description
Context: when a user re-authenticate a shop, they might use different shopee.account (API key). Currently Odoo will not change the shopee.account when they re-auth with another shopee.account. Enable a shopee.shop switches to another shopee.account when we run `create_or_update_shop` function. Forward-Port-Of: odoo/enterprise#128837 Forward-Port-Of: odoo/enterprise#92446
Contacts now use the country set on the partner record to identify the issuing country for tax numbers, instead of guessing from the first two characters. This prevents valid tax IDs, such as Mexican RFC numbers that happen to start with another country code, from being rejected incorrectly.
Original PR description
**Issue**: If a partner has a tax number that begins with a different country’s code (for instance, a Mexican RFC number that begins with “RO”, matching Romania), when a user tries to add the tax…
**Issue**: If a partner has a tax number that begins with a different country’s code (for instance, a Mexican RFC number that begins with “RO”, matching Romania), when a user tries to add the tax number to the partner, there will be a validation error.
**Steps to reproduce** (on fresh database with Contacts app and l10n_mx module installed):
1. Make a new contact.
2. Give the contact a Mexican address.
3. Give the contact the RFC number (or `vat` field): ROS561231GR8.
4. Try to save this change. Observe the validation error.
**Explanation**:
The `get_all_identifiers` method uses the first two characters of `partner.vat` as a heuristic to detect the issuing country, since many VAT formats start with a country code (e.g. RO1234567897). This prefix was used unconditionally whenever it matched an item from `get_tin_metadata_of_country`. without checking whether the VAT actually belongs to that country. Some countries' identifier formats begin with letters which are not country codes. In Mexico, for instance, RFC numbers start with letters derived from the partner's name, so a partner named “Sofia Rodriguez” would get an RFC starting with “RO”. Therefore, this heuristic can produce false-positive matches against unrelated countries.
**Solution**:
We no longer use a partner's vat number to detect the issuing country. Instead, we use the partner's `country_code` field as the issuing country.
opw-6471006
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr