Friday, September 11, 2026
23 changes · 18.0
Security fixes and vulnerability patches
This update prevents unauthenticated users from changing customer or partner information in point-of-sale related flows. It helps protect sensitive business and customer data by ensuring only signed-in users can make these changes.
Original PR description
This also ensures that only authenticated users can modify partner data.
This fix prevents self-order checkouts from relying on customer details that the frontend does not provide. It reduces validation errors and helps ensure only authenticated users can change customer information.
Original PR description
The partner is never added by the frontend, so we can safely ignore it. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Enhancements to existing features
This update aligns Mexican electronic invoicing payment calculations with SAT requirements, especially around rounding in payment documents. It helps reduce rejected invoices or compliance issues when issuing payment complements in Mexico.
Resolved issues and error corrections
Mexican payment complements now use the correct number of decimal places for tax amounts based on the payment currency. This prevents valid payments from being rejected by the PAC/SAT validation, especially when invoices and payments use different currencies.
Original PR description
The 'ImpuestosP' node of the payment complement (Pagos 2.0) is reported with 6 decimals while the SAT expects the amounts to be expressed with the number of decimals supported by the currency of the…
The 'ImpuestosP' node of the payment complement (Pagos 2.0) is reported with 6 decimals while the SAT expects the amounts to be expressed with the number of decimals supported by the currency of the payment ('MonedaP'), as published in the 'c_Moneda' catalog, for example 2 decimals for MXN.
Steps to reproduce:
- Have an MX Company setup with Quadrum as PAC.
- Create and sign a customer invoice in MXN with a 16% IVA tax.
- Register a full payment and send the payment complement to the PAC.
Issue:
Payment will be rejected
```
Code : CRPER654
Message : El importe del campo BaseP que corresponde a Traslado, no tiene la cantidad de decimales que soporta la moneda (MonedaP)
```
Analysis:
Quadrum recently aligned its validation on that rule and now rejects the document having fields with too much decimals and, currently, fields like BaseP, ImporteP are pinned to 6 decimals in the template.
Reporting them with the decimals of the payment currency is not enough when the payment settles a document expressed in another currency: those amounts are also compared with the ones of the related documents converted with 'EquivalenciaDR', and the PAC expects the values truncated or rounded
```
Code : CRP20274
Extra Info : Traslados: La sumatoria de ImporteDR es mayor al ImporteP 1.30.
Valores minimos permitidos: Truncado: 1.29 o redondeado: 1.3
```
opw-6561617Documentation and clarification updates
Brendon Ignath has signed the Odoo Individual Contributor License Agreement. This is a legal documentation update that allows their contributions to be accepted under Odoo's contributor terms, with no effect on product functionality.
Original PR description
Signing the Odoo Individual Contributor License Agreement v1.0.
Luxembourg payroll settings have been updated with the 2026 contribution, employer, general, and tax credit parameters. This helps payroll teams calculate employee pay and related obligations using the latest expected rules for the new year.
Original PR description
Update Luxembourg payroll rule parameters for 2026. Task-6481735
The Belgian CODA Clean module now records clearer information when importing CODA bank files. This helps support teams diagnose import issues faster, reducing investigation time when customers report problems.
Original PR description
This commit will improve the logs of _l10n_be_codaclean_import_coda_files to help the support team to debug possible problem. task-5436868
French partner records now get a more accurate electronic invoicing identifier by checking official directory and Peppol information before falling back to the SIRET-based default. When several possible identifiers are found, users are prompted to choose the correct one, helping avoid incorrect invoice routing.
Original PR description
for existing french partners that dont have their EAS set to the FRCTC, a check will be made to see if they are on the annuaire using the IAP annuaire lines, if one line exists, the identifier is set to that, if more than one line exists, there is no way for us to know which one belongs to that partner, so we prompt the user to go to the partner's settings and choose the correct identifier, if they are not on the annuaire but on peppol, we set the EAS and identiifer to the correct correspoding value found on peppol, if all cases fail, we default to the FRCTC EAS and their SIRET as the identifier. task-id-6327357
This update makes log entries show the actual system time instead of simulated test time when faketime is used. This helps teams read runbot logs more accurately and analyze how long long-running tests really took, while keeping the simulated timestamp available separately if needed.
Original PR description
When using faketime the logs are not showing the real time but the faketime one which can be confusing when listed in the runbot interface. Before the usage of the JSONFormatter, the logs were using the PostgreSQL time, which is the real time. This restores the previous behaviour. Having the faked time oustide JsonFromater is also painful: long running faketimed tests end up with most of their logs stamped to nearly the same real date, preventing any analysis of execution time based on the logs. One drawback of this change is the inability to identify at first sight whether a log was faketimed or not, but at this point it is just a tradeoff, and the faked_created field on the record still allows getting this info if needed. Forward-Port-Of: odoo/odoo#287159
Merging helpdesk tickets now keeps the sales order item on related timesheets aligned with the destination ticket. This prevents billing or service tracking inconsistencies when tickets with different sales items are combined.
Original PR description
Currently when you merge helpdesk tickets with differing sale order items, the sales order item listed on the timesheets of the source ticket is not updated to match the destination ticket's sales order item. This PR ensures uniformity bugfix-6473396 Forward-Port-Of: odoo/enterprise#129060
Invoice imports now better handle small rounding differences when matching fixed taxes, especially on lines with quantities above one. This reduces failed or incorrect tax matching during import, helping accounting teams process supplier or customer invoices more reliably.
Original PR description
When importing an invoice with fixed taxes, if the line has a quantity of more than 1, sometimes, due to rounding errors, searching for the fixed tax fails to find it, as the values needed to exactly match, a new search method was added to give a margin of +/- 0.01 when searching for valid taxes. task-id-6307992
This change fixes an unreliable automated test in Odoo's messaging area that could fail even when the product behavior was correct. It helps keep validation runs stable, reducing false alarms during releases and maintenance.
Original PR description
Backport of 9a8f127a6c9a (odoo/odoo#280680), currently on saas-19.4 and on master (6da9738c26e2). Before this commit, the discuss test "Message shows up even if channel data is incomplete" failed on runbot with "Websocket subscription not received.". This happens because waitUntilSubscribe() only resolves on a subscription received after it is called, and the test calls it after running all timers. The forced channel update is debounced, so that timer run sends the subscription, and the wait then times out. This commit calls waitUntilSubscribe() before forcing the channel update, so the subscription that update sends resolves the wait. https://runbot.odoo.com/odoo/error/947146
Website checkout now prevents on-site payment in situations where gift cards or eWallets are not supported. This avoids customers reaching unsupported payment flows and helps reduce checkout errors or support cases.
Original PR description
We don't want to support gift cards and eWallets in certain conditions opw-6483282
Mexican payment complements now format tax amounts using the decimal precision required for the payment currency. This prevents valid payments from being rejected by the PAC/SAT validation when amounts previously had too many decimal places, including mixed-currency settlement cases.
Original PR description
The 'ImpuestosP' node of the payment complement (Pagos 2.0) is reported with 6 decimals while the SAT expects the amounts to be expressed with the number of decimals supported by the currency of the…
The 'ImpuestosP' node of the payment complement (Pagos 2.0) is reported with
6 decimals while the SAT expects the amounts to be expressed with the number
of decimals supported by the currency of the payment ('MonedaP'), as published
in the 'c_Moneda' catalog, for example 2 decimals for MXN.
Steps to reproduce:
- Have an MX Company setup with Quadrum as PAC.
- Create and sign a customer invoice in MXN with a 16% IVA tax.
- Register a full payment and send the payment complement to the PAC.
Issue:
Payment will be rejected
```
Code : CRPER654
Message : El importe del campo BaseP que corresponde a Traslado, no tiene la cantidad de decimales que soporta la moneda (MonedaP)
```
Analysis:
Quadrum recently aligned its validation on that rule and now rejects the document
having fields with too much decimals and, currently, fields like BaseP, ImporteP
are pinned to 6 decimals in the template.
Reporting them with the decimals of the payment currency is not enough when the
payment settles a document expressed in another currency. Those amounts are also
checked against the converted ones with 'EquivalenciaDR'. With the full 10 decimals,
we exceed the lower/upper limits so the document is rejected with CRP20274.
opw-6561617Amazon order syncing now keeps a small safety buffer when checking for new or updated orders. This prevents orders from being skipped permanently when Amazon’s data takes a short time to become searchable, improving reliability for sales operations.
Original PR description
## Problem #114591 migrated `sale_amazon`'s order synchronization from Orders API v0 to v2026-01-01 (`getOrders` → `searchOrders`). As part of that migration, `_sync_orders`'s handling of the…
## Problem
#114591 migrated `sale_amazon`'s order synchronization from Orders API v0
to v2026-01-01 (`getOrders` → `searchOrders`). As part of that migration,
`_sync_orders`'s handling of the `last_orders_sync` watermark changed in a
way that can silently and permanently exclude orders from ever being
synchronized.
**Before #114591** (v0 `getOrders`), the response's `LastUpdatedBefore`
field was guaranteed present, and the cursor was always set to that
API-provided, already-consistent value:
```python
status_update_upper_limit = dateutil.parser.parse(
orders_batch_data['LastUpdatedBefore'] # direct access, always present in v0
)
...
account.last_orders_sync = status_update_upper_limit.replace(tzinfo=None) # unconditional
```
**After #114591** (v2026-01-01 `searchOrders`), the equivalent field,
`lastUpdatedBefore`, is optional in the response and is omitted entirely
whenever a page has no orders to report (`{'orders': []}`). The new code
added a fallback for that case, but the fallback is unbuffered:
```python
if last_updated_before := orders_batch_data.get('lastUpdatedBefore'):
status_update_upper_limit = dateutil.parser.parse(last_updated_before)
...
if status_update_upper_limit:
account.last_orders_sync = status_update_upper_limit
else:
account.last_orders_sync = sync_start # raw request time, no buffer
```
Per Amazon's own [`searchOrders` reference](https://developer-docs.amazon/sp-api/reference/searchorders)
(`createdBefore`/`lastUpdatedBefore` parameter descriptions; same wording in the
[`orders_2026-01-01` OpenAPI model](https://github.com/amzn/selling-partner-api-models/blob/96d516badc8d69a566a4160e3c7b315600e043a7/models/orders-api-model/orders_2026-01-01.json)):
> If you include `lastUpdatedAfter` in the request, `lastUpdatedBefore` is
> optional, and if provided must be equal to or after the
> `lastUpdatedAfter` date and **at least two minutes before the time of
> the request.**
i.e. Amazon itself documents that order data can lag up to ~2 minutes
behind eventual consistency. The `sync_start` fallback ignores this
entirely.
## Failure sequence
1. An order's status changes on Amazon at time `T`.
2. Amazon hasn't indexed that change yet (within its own documented ~2 min
window).
3. A poll fires inside that window, genuinely finds nothing yet, gets back
`{'orders': []}` — no `lastUpdatedBefore`.
4. `status_update_upper_limit` stays `None` → `last_orders_sync` is set to
`sync_start`, already later than `T`.
5. Amazon finishes indexing the change. Doesn't matter: every future
`lastUpdatedAfter` query now starts after `T`, so this status change can
never be returned by `searchOrders` again.
6. The order isn't delayed — it's **permanently excluded**, with no
exception and no log entry anywhere.
This isn't a rare edge case: an empty page is the *normal* result of most
polls for any account without constant order volume, so the unbuffered
fallback fires routinely rather than exceptionally. It's easy to miss in
testing/QA at the default 60-minute poll interval (`ir_cron_sync_amazon_orders`),
where most hourly windows contain at least one order and the safe branch
is what usually runs — it becomes far more visible at shorter polling
intervals, which is how we found it.
## Real-world impact
Reconstructed from production logs, with request IDs, for order
`028-5161591-7232333` (`lastUpdatedTime=2026-08-21T10:10:53.060Z`):
- **10:12:21** — `searchOrders` request `c6833e60-4a17-4c25-b70d-7da55d5b303d`.
Response: `{'orders': []}`. The request's own upper bound is ~10:10:21
(2 min before request time), so this order — updated at 10:10:53 — is
correctly excluded: genuinely too new for this request, per Amazon's own
rule. Nothing wrong yet.
- Because the page is empty, `status_update_upper_limit` stays `None` and
the fallback sets `last_orders_sync` to this run's raw request time,
~10:12:21 — no buffer.
- **10:17:33** — next run, request `628290bf-fa59-4d66-8494-e865ff1ef73d`.
Lower bound is now `10:12:21` (the unbuffered cursor from the previous
run). The order's own `10:10:53` is *before* that lower bound. Response:
`{'orders': []}` again — the order is now permanently unreachable.
The order fell into a ~90-second gap: too new for one request's upper
bound, already behind the next request's lower bound, with no query ever
covering the interval between. It sat as `UNSHIPPED` on Amazon,
completely invisible to Odoo, until manually re-surfaced days later by
resetting the account's watermark back to before this window and
re-running the sync. In that one recovery run, 23 previously-unseen
orders on this account came back, every one still `UNSHIPPED`, all with
the same signature: a `lastUpdatedTime` sitting in a gap between two
consecutive empty `searchOrders` responses.
## Fix
Buffer the fallback by the same indexing-lag margin Amazon documents,
instead of using the raw request time:
```python
account.last_orders_sync = sync_start - ORDERS_SYNC_INDEXING_LAG # timedelta(minutes=2)
```
Added a regression test (`test_sync_orders_watermark_buffered_on_missing_upper_limit`)
covering the empty-page case.This fixes an issue where upgrading the Website Sale Collect app could turn the "Pay on Site" payment option back on after a merchant had disabled it. Merchants' payment settings and display preferences are now preserved during upgrades, helping prevent unpaid checkout orders from being accidentally accepted.
Original PR description
Steps to reproduce: =================== 1. Go to the payment providers and set "Pay on Site" to disabled. 2. Go to Apps, clear the filter, look up "website_sale_collect" and upgrade it. This also…
Steps to reproduce:
===================
1. Go to the payment providers and set "Pay on Site" to disabled.
2. Go to Apps, clear the filter, look up "website_sale_collect" and upgrade it. This also happens on its own, as upgrading any custom module that depends on it upgrades it too.
3. Go back to the payment providers.
=> "Pay on Site" is enabled again, and published as well from 19.0 on. Customers are offered it at checkout and place orders that are never paid, without the merchant ever enabling anything.
Root cause:
===========
`data/payment_provider_data.xml` is loaded in update mode because it carries no `noupdate`, and it hardcodes the state of the provider:
<field name="state">enabled</field>
So every upgrade of the module writes that value back over whatever the merchant configured. Every other provider ships its data with `noupdate="1"` and leaves the state alone, this module is the exception.
The file has been loaded this way since the module was added:
- [1] created the module with an updatable provider record.
Fix:
====
Load the file with `noupdate="1"`. The record is still created, enabled, when the module is installed, it is simply not written again on later upgrades. The flag is read from the file rather than from the `ir.model.data` row, so databases where the provider already exists are covered on their next upgrade, no data migration needed.
[1]: 087c48c4ed2e
opw-6528110
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-prThis fix prevents the Chilean electronic invoicing status check from failing when the tax authority returns an empty response. Scheduled processing can now continue instead of being interrupted by an error.
Original PR description
When checking the status of a DTE, sometimes the response will have no `text`. This causes a traceback error that halts execution of the cron `cron_run_sii_workflow`. opw-6322106 Forward-Port-Of: odoo/enterprise#129582
Products with multiple variants now remain available to add to cart when at least one variant has stock. This prevents shoppers from seeing a product as sold out simply because the first listed variant is unavailable.
Original PR description
Steps to reproduce: =================== 1. Create a storable product with two variants and untick "Continue selling when out-of-stock". 2. Leave the first variant at 0 in stock, put some stock on the…
Steps to reproduce: =================== 1. Create a storable product with two variants and untick "Continue selling when out-of-stock". 2. Leave the first variant at 0 in stock, put some stock on the second one. 3. try the "Add to Cart" option of the shop page. => The product has no add to cart button, as if it were sold out, while its second variant can be bought. Reordering the variants so that the one in stock comes first brings the button back. Root cause: =========== `product.template._is_sold_out()` delegates to `product_variant_id`, which is `product_variant_ids[:1]`, so a whole template is declared sold out on the sole basis of its first variant. `_website_show_quick_add()` then hides the button for the template, whatever the stock of the other variants. The helper has looked at that single variant since it was written: - [1] added it to hide the button of sold out products. Fix: ==== Consider the template sold out only when all of its variants are. The check stops at the first variant in stock, so a product that can be bought still costs a single stock lookup. [1]: https://github.com/odoo/odoo/commit/4ae197cc770cec2058e1f113209c4f93a4a0f4b2 opw-6520052 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
This fixes an issue in the HTML editor where clicking at the end of text inside a button moved the cursor outside the button. Users can now place the cursor correctly within button content, making button text editing smoother and less frustrating.
Original PR description
Problem: When trying to place the caret at the end of a button's content with the mouse, the caret always moves outside of the button instead of staying inside it. Cause: This happens because of the previous commit https://github.com/odoo-dev/odoo/commit/c8e93dbc806b5ea511cce695afbc9e81e30a1ca9 (which aimed to fix placing the caret after a link when at the end of a paragraph in the editable), which was not fixing the issue properly. Solution: Check if the click happens at the end of a link and the caret will move inside the link then manually place the selection after the link. Steps to reproduce: - Add a button with one character. - Try to put selection after that character. - Selection always jumps after the button. opw-6499237 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
This fix prevents packaged items from being reassigned to the wrong package when multiple delivery steps are merged into one final shipment. Businesses using multi-step warehouse flows get more accurate package tracking and fewer shipping errors.
Original PR description
Steps to reproduce --- 1. Set the warehouse to deliver in 3 steps. 2. Create a delivery pick not tied to a procurement group (not from a sale order) with a consumable product, and validate it. 3. On…
Steps to reproduce --- 1. Set the warehouse to deliver in 3 steps. 2. Create a delivery pick not tied to a procurement group (not from a sale order) with a consumable product, and validate it. 3. On the pack transfer, put the goods in a package and validate. 4. Duplicate the pick, validate it, put its goods in a second package on the pack transfer, and validate. 5. Open the final delivery: both lines sit in the same package instead of one line per package. Issue --- The two picks share no procurement group, so their delivery moves merge onto a single move keyed by partner. Validating the second pack tops up that already partially reserved move: for a consumable, `_action_assign` builds its lines from `_get_available_move_lines`, which reports the availability of every upstream package without discounting what the move already reserved. https://github.com/odoo/odoo/blob/05af9e6877fd0f044bb46c983fe7300eb1db9307/addons/stock/models/stock_move.py#L1907-L1909 The package already reserved on the first line is therefore offered again and reused, collapsing both lines onto one package. The reserved-product branch below already subtracts the move's own reservation before allocating; doing the same here leaves each package on its own line. opw-6498532 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
PINT electronic invoices are now generated through the newer UBL export instead of the older BIS 2.0 process. This helps prepare the accounting and Peppol flows for removal of the legacy BIS implementation while keeping invoice export behavior aligned with current standards.
Original PR description
Problem --------- Currently, PINT uses the old BIS2.0 export. Objective --------- Decouple the exports as an effort to remove the old BIS implementation. no-task --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
This fixes an issue where tiny timestamp differences could cause the same employee workday to be treated as separate days during overtime calculations. It helps prevent duplicate overtime records and keeps attendance processing reliable.
Original PR description
When an attendance check-out contains microseconds while its check-in does not, _get_day_start_and_day() preserves those microseconds while normalizing the time. This can produce two different grouping keys for the same employee day and make overtime recomputation attempt duplicate (employee_id, date) records. Normalize the day start completely before using it as a grouping key. A regression test covers an attendance whose check-in and check-out differ in microsecond precision. Fixes #280573
Generating the Peruvian "Inventory and Balance" General Ledger report now completes successfully instead of causing a server error. The export keeps the required SUNAT file format while using a safer CSV generation method, reducing disruption for accounting users.
Original PR description
### Description of the issue/feature this PR addresses: This PR fixes a server crash in the Peruvian localization (l10n_pe_reports_lib) that occurs when generating the "Inventory and Balance" General…
### Description of the issue/feature this PR addresses: This PR fixes a server crash in the Peruvian localization (l10n_pe_reports_lib) that occurs when generating the "Inventory and Balance" General Ledger report. The crash is triggered by strict validation rules within Python's csv module, which rejects the custom line terminator used to fulfill the SUNAT PLE formatting requirements. ### Current behavior before PR: When a user attempts to generate and export the "Inventory and Balance" report, the server crashes with a ValueError: bad delimiter or lineterminator value. This happens because the csv.DictWriter is initialized with lineterminator='|\n' to ensure every row ends with a pipe. Python's underlying csv implementation rejects this, as it expects standard line endings (\r, \n, or \r\n) and throws an error if the delimiter character (|) is included in the terminator string. ### Desired behavior after PR is merged: The "Inventory and Balance" report generates successfully without server errors. The code now uses the standard lineterminator='\n' to satisfy Python's validation rules. To maintain the mandatory trailing pipe (|) at the end of each row required by SUNAT, a dummy empty column (['']) is appended to the field names with restval=''. This prompts the writer to naturally append the final pipe as a column delimiter before the newline, resulting in the exact |\n output format required, safely and reliably. opw-6509674 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr