Friday, September 11, 2026
2 changes · 18.0
Resolved issues and error corrections
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.