Friday, May 8, 2026
1 change · 19.0
Resolved issues and error corrections
This fix ensures that newly generated, unsaved line items in form screens are not silently dropped when there are more than the usual display limit. It prevents users from saving incomplete records, such as manufacturing orders missing components, when the system creates many related lines automatically.
Original PR description
## Summary - Fixes silent data loss when an `onchange` returns more virtual (`NewId`) x2many records than the `limit` declared in the form-view `fields_spec`. - Virtual records can no longer be…
## Summary
- Fixes silent data loss when an `onchange` returns more virtual (`NewId`) x2many records than the `limit` declared in the form-view `fields_spec`.
- Virtual records can no longer be re-fetched by the client, so truncating them inside `web_read` makes the dropped records disappear forever once the user clicks Save.
- After this change, `web_read` keeps every `NewId` and only paginates real (persisted) ids when a `limit` is provided.
Closes #263449.
## Reproducer
On any 19.0 database with `mrp` installed, create a `mrp.bom` with > 40 active component lines (consu, no service filter) for some product `P`, then:
```python
common = dict(
values={'product_id': P.id, 'company_id': 1, 'state': 'draft',
'product_qty': 1, 'move_raw_ids': [], 'bom_id': False},
field_names=['product_id'],
)
env['mrp.production'].onchange(
**common,
fields_spec={
'product_id': {}, 'company_id': {}, 'bom_id': {}, 'product_qty': {},
'move_raw_ids': {
'fields': {'product_id': {}, 'product_uom_qty': {}, 'bom_line_id': {}},
'limit': 40,
},
},
)
```
Before this PR, the response contains exactly 40 `Command.CREATE` entries even when the BoM produces 69 components. After this PR (or with `limit` omitted), it contains all 69.
The same compute path invoked via `env['mrp.production'].create({...})` already persists all 69 components correctly, which proves the truncation is specific to the `onchange` flow.
## Why this matters
The default limit on x2many list views without an explicit `limit` attribute is `40` (`addons/web/static/src/views/fields/field.js:316`). That default is fine for paginating *persisted* records — the client can always re-fetch later pages — but it is unsafe for *virtual* records produced by an onchange compute on a new record. Those records exist only in the response: dropping them silently corrupts the saved record and the user has no way to detect the loss without comparing component lists by hand.
End-to-end repro observed in production (Odoo 19.0+e):
- `mrp.production.create({'product_id': ..., 'bom_id': ..., 'product_qty': 1})` (no fields_spec) → saves 69 `stock.move` records.
- Same product/BoM via the form view → saves 40 `stock.move` records, missing 29.
## Test plan
- [ ] Add a regression test under `addons/web/tests/` that runs the reproducer above (or any equivalent: a model with a one2many compute that produces > 40 virtual records) and asserts that the `onchange` response contains all of them when `fields_spec.<x2many>.limit < total`.
- [ ] Manual: open a `mrp.production` form, choose a product whose BoM has > 40 component lines, click Save, and confirm `len(production.move_raw_ids) == bom_explode_count`.
- [ ] Manual: load an existing record with > 40 stored x2many entries and confirm pagination still applies (only `NewId` rows must escape the limit; real ids continue to paginate at `limit`).
## Notes
- Workaround for users who can't ship the fix immediately: add `limit="999"` (or any value above the largest expected x2many size) on the embedded `<list>` in the form view.
- The fix is intentionally minimal and targets only the `web_read` pre-pagination of x2many ids. If maintainers prefer addressing this earlier in the `onchange`/snapshot flow, I am happy to rework.