Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,9 @@ registry.category("web_tour.tours").add("html_editor_disable_chatgpt_tour", {
trigger: ".o_form_button_cancel",
run: "click",
},
{
content: "Wait for discard to finish",
trigger: ".o_form_renderer.o_form_saved",
},
],
});
229 changes: 229 additions & 0 deletions web_widget_autocomplete/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
=======================
Web Widget Autocomplete
=======================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:962bbb051b41b6c023a4326613614a30d9c9aab2590bdb65e5ca0cb40cbbcda8
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
:target: https://github.com/OCA/web/tree/18.0/web_widget_autocomplete
:alt: OCA/web
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_widget_autocomplete
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module adds a backend Char field widget that typeahead-fills the
field from a public ``@api.model`` method on the same model.

Type in the field; after a configurable number of characters the widget
calls the method with the trimmed input, shows matching rows, and on
select writes the Char plus supported extra scalar and relational fields
that exist on the model and in the current view. ``min_symbols`` ``0``
opens that list on focus and calls the method with ``False``, so the
field can be used before any typing. See Usage for supported value
formats.

**Table of contents**

.. contents::
:local:

Configuration
=============

No configuration is required. Enable the module; consuming addons
declare Char fields and the method.

To try the widget on a partner City field (Umbria cities), install
``web_widget_autocomplete_demo``.

Usage
=====

Use ``widget="autocomplete"`` on a ``fields.Char`` and name a public
``@api.model`` method in ``options``.

The method receives the trimmed input string, or ``False`` when
``min_symbols`` is ``0`` and the field is focused with an empty input,
and must return a list of dicts. Each dict’s keys are field names. The
widget field’s key is the label shown in the dropdown and the value
written to the Char. Any number of extra keys may be present; on
**select** they are written to sibling fields that exist on the **model
and in the same view** (visible or ``invisible``). Keys missing from the
model or view, unsupported field types, and invalid values are skipped.
The ``id`` key is always ignored.

Supported extra values (Python values are converted to JSON by the RPC):

+--------------------------+------------------------------------------+
| Field type | Value returned by the method |
+==========================+==========================================+
| Char, Text, HTML | String; use an empty string to clear |
+--------------------------+------------------------------------------+
| Integer, Float, Monetary | A value convertible to a finite number, |
| | as with the existing Integer behavior |
+--------------------------+------------------------------------------+
| Boolean | ``True`` or ``False`` |
+--------------------------+------------------------------------------+
| Selection | An exact selection key, or ``False`` to |
| | clear |
+--------------------------+------------------------------------------+
| Date | ``"YYYY-MM-DD"``, or ``False`` to clear |
+--------------------------+------------------------------------------+
| Datetime | ``"YYYY-MM-DD HH:MM:SS"`` in UTC, or |
| | ``False`` to clear |
+--------------------------+------------------------------------------+
| Many2one | A positive integer ID, |
| | ``[id, display_name]``, or ``False`` to |
| | clear |
+--------------------------+------------------------------------------+
| One2many, Many2many | A list of the ORM commands described |
| | below |
+--------------------------+------------------------------------------+

Invalid dates are skipped. Datetimes are converted from UTC to the
client timezone by Odoo. A bare Many2one ID is resolved through Odoo to
obtain its display name; the related record must exist and be readable
by the user.

Supported x2many commands are ``(0, 0, values)`` to create a related
record, ``(4, id)`` or ``(4, id, 0)`` to link an existing record, and
``(6, 0, ids)`` to replace the relation. For example, ``[(4, 12)]``
links record 12 and ``[(0, 0, {"name": "Visit"})]`` creates a related
record. Create values must use the related model's server-side field
formats.

**Replace removes relations omitted from the supplied IDs.** In
particular, ``[(6, 0, [])]`` clears the relation; for One2many fields,
Odoo's inverse field deletion policy can also delete removed records. An
empty command list ``[]`` makes no relation changes. Other opcodes and
malformed command shapes are rejected for that field as a whole. These
checks validate the payload format; the model method must enforce its
own authorization and Odoo access rights still apply when related
records are read or saved.

Free typing (without selecting a row) updates only the Char. Extra
fields keep their previous values until the next select.

A **readonly** extra is still filled in the UI on select. It is
persisted on save only if that field’s view node has ``force_save="1"``.
Without it, the fill is dropped on save with no error.

The method must be publicly RPC-callable: not prefixed with ``_``, not
``@api.private``, and decorated with ``@api.model`` so the first
argument is the search string rather than record ids. If the call fails,
the dropdown stays empty and an error notification is shown.

.. code:: xml

<field
name="address_string"
widget="autocomplete"
options="{'function': 'address_auto_complete', 'min_symbols': 3, 'debounce': 5}"
/>
<field name="address_ref" readonly="1" force_save="1"/>
<field name="city" invisible="1"/>

.. code:: python

@api.model
def address_auto_complete(self, value):
"""Return autocomplete rows for ``value``.

:param str | bool value: trimmed input, or ``False`` when ``min_symbols``
is ``0`` and the input is empty
:return: list of dicts whose keys are field names on this model
:rtype: list[dict]
"""
return [
{
"address_string": "string1",
"address_ref": 1,
"city": "Perugia",
},
]

=============== ================= ==================================
Option Type Default if omitted
=============== ================= ==================================
``function`` str (method name) none — no RPC, Char still editable
``min_symbols`` int ``3``
``debounce`` int (ms) ``250``

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's not how the option is called, this is also wrong above

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @hbrunn , thank you very much for your feedback! Could you please explain this - what's exactly is wrong here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's delay

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

misread this, it indeed uses debounce as name in the options, but delay would be better anyways i think

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving the option name as debounce. It matches the arch option, extractProps, and USAGE. delay is only the prop on the AutoComplete subclass.

=============== ================= ==================================

``min_symbols`` ``0`` opens the dropdown as soon as the field is
focused, before the user types. The method is called immediately with
``False``, so it can return the full list. Typing still sends the
trimmed string. The debounce applies to typing, not to that focus call.

For a dropdown whose options are fetched once (or when a
``depending_on`` context key changes), use
``web_widget_dropdown_dynamic`` instead.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_widget_autocomplete%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Cetmix

Contributors
------------

- `Cetmix OÜ <https://cetmix.com>`__:

- Ivan Sokolov

- `Data Dance s.r.o. <https://www.datadance.eu/>`__:

- Radovan Skolnik <radovan@skolnik.info>

Other credits
-------------

The dropdown UI is Odoo Community ``web.AutoComplete``
(``addons/web/static/src/core/autocomplete/``).

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/web <https://github.com/OCA/web/tree/18.0/web_widget_autocomplete>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions web_widget_autocomplete/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0).
22 changes: 22 additions & 0 deletions web_widget_autocomplete/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 Cetmix OÜ
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0).

{
"name": "Web Widget Autocomplete",
"summary": "Autocomplete widget",
"version": "18.0.1.0.0",
"development_status": "Beta",
"category": "Hidden",
"website": "https://github.com/OCA/web",
"author": "Cetmix, Odoo Community Association (OCA)",
"license": "LGPL-3",
"depends": ["web"],
"assets": {
"web.assets_backend": [
"web_widget_autocomplete/static/src/**/*",
],
"web.assets_unit_tests": [
"web_widget_autocomplete/static/tests/**/*",
],
},
}
3 changes: 3 additions & 0 deletions web_widget_autocomplete/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
5 changes: 5 additions & 0 deletions web_widget_autocomplete/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
No configuration is required. Enable the module; consuming addons declare Char
fields and the method.

To try the widget on a partner City field (Umbria cities), install
`web_widget_autocomplete_demo`.
4 changes: 4 additions & 0 deletions web_widget_autocomplete/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- [Cetmix OÜ](https://cetmix.com):
- Ivan Sokolov
Comment thread
ivs-cetmix marked this conversation as resolved.
Resolved
- [Data Dance s.r.o.](https://www.datadance.eu/):
- Radovan Skolnik \<<radovan@skolnik.info>\>
2 changes: 2 additions & 0 deletions web_widget_autocomplete/readme/CREDITS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The dropdown UI is Odoo Community `web.AutoComplete`
(`addons/web/static/src/core/autocomplete/`).
9 changes: 9 additions & 0 deletions web_widget_autocomplete/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
This module adds a backend Char field widget that typeahead-fills the field
from a public `@api.model` method on the same model.

Type in the field; after a configurable number of characters the widget calls
the method with the trimmed input, shows matching rows, and on select writes
the Char plus supported extra scalar and relational fields that exist on the
model and in the current view. `min_symbols` `0` opens that list on focus and
calls the method with `False`, so the field can be used before any typing.
See Usage for supported value formats.
97 changes: 97 additions & 0 deletions web_widget_autocomplete/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
Use `widget="autocomplete"` on a `fields.Char` and name a public
`@api.model` method in `options`.

The method receives the trimmed input string, or `False` when
`min_symbols` is `0` and the field is focused with an empty input, and
must return a list of dicts. Each dict’s keys are field names. The widget field’s key is the
label shown in the dropdown and the value written to the Char. Any number
of extra keys may be present; on **select** they are written to sibling
fields that exist on the **model and in the same view** (visible or
`invisible`). Keys missing from the model or view, unsupported field types,
and invalid values are skipped. The `id` key is always ignored.

Supported extra values (Python values are converted to JSON by the RPC):

| Field type | Value returned by the method |
|------------|------------------------------|
| Char, Text, HTML | String; use an empty string to clear |
| Integer, Float, Monetary | A value convertible to a finite number, as with the existing Integer behavior |
| Boolean | `True` or `False` |
| Selection | An exact selection key, or `False` to clear |
| Date | `"YYYY-MM-DD"`, or `False` to clear |
| Datetime | `"YYYY-MM-DD HH:MM:SS"` in UTC, or `False` to clear |
| Many2one | A positive integer ID, `[id, display_name]`, or `False` to clear |
| One2many, Many2many | A list of the ORM commands described below |

Invalid dates are skipped. Datetimes are converted from UTC to the client
timezone by Odoo. A bare Many2one ID is resolved through Odoo to obtain its
display name; the related record must exist and be readable by the user.

Supported x2many commands are `(0, 0, values)` to create a related record,
`(4, id)` or `(4, id, 0)` to link an existing record, and `(6, 0, ids)` to
replace the relation. For example, `[(4, 12)]` links record 12 and
`[(0, 0, {"name": "Visit"})]` creates a related record. Create values must
use the related model's server-side field formats.
Comment on lines +30 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method is Python, so these are better built with odoo.fields.Command than with literal tuples — the ORM's own docstring encourages that namespace, and it serialises to exactly the integers the widget validates. Verified end to end on 18.0.

Suggested change
Supported x2many commands are `(0, 0, values)` to create a related record,
`(4, id)` or `(4, id, 0)` to link an existing record, and `(6, 0, ids)` to
replace the relation. For example, `[(4, 12)]` links record 12 and
`[(0, 0, {"name": "Visit"})]` creates a related record. Create values must
use the related model's server-side field formats.
Build these with `odoo.fields.Command` rather than literal tuples:
| Helper | Emitted command | Effect |
|--------|-----------------|--------|
| `Command.create(values)` | `(0, 0, values)` | create a related record |
| `Command.link(id)` | `(4, id, 0)` | link an existing record |
| `Command.set(ids)` | `(6, 0, ids)` | replace the relation |
```python
from odoo import api, models
from odoo.fields import Command
class ResPartner(models.Model):
_inherit = "res.partner"
@api.model
def city_autocomplete(self, value):
tags = self.env["res.partner.category"].search([("name", "=", "Umbria")])
return [{"city": "Perugia", "zip": "06121",
"category_id": [Command.set(tags.ids)]}]

The literal forms (0, 0, values), (4, id), (4, id, 0) and (6, 0, ids)
remain valid, and external RPC callers must use them since the helpers are not
available over RPC. Every other helper is rejected, Command.clear()
included
— it emits (5, 0, 0); use Command.set([]) to empty a relation.
Create values must use the related model's server-side field formats.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the comment, however Command is just a helper, so I think current version is totally fine. I'm personally still using tuples instead on Command myself as they are more explicit for me.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your version is correct and the table right above defines the opcodes, so this isn't a bug — it's about who the text is for.

USAGE.md is read by people who don't know this widget, and often don't know Odoo's command protocol. To them (6, 0, ids) is three magic constants: the 6 is meaningless without cross-referencing, and the 0 is a placeholder that looks like it must mean something. Command.set(ids) says what it does on sight. "More explicit for me" is the right test for your own code — you know the opcodes — but docs get read by people who aren't you, and this particular block is the bit they'll paste into their own module, so it seeds whichever idiom they carry forward.

Upstream leans the same way. The Command docstring says "Via Python, we encourage developers craft new commands via the various functions of this namespace", and core 18.0 follows its own advice — 5,754 Command.* calls across 685 addon files (create 3,498, set 1,181, link 697).

A middle ground that might suit you better than my original suggestion: keep the tuples in the reference table — that's accurate, since integers are literally what crosses the wire and what the validator checks — and use Command.* only in the worked example. The table then documents the protocol, the example demonstrates idiomatic Python, and neither misleads.

Separately, and regardless of which you pick:

For example, [(4, 12)] links record 12

That's a hardcoded database id in the example readers copy, and record 12 is something different in every database. [(4, tag.id)] with the id from a search makes the same point without inviting the paste.

Your call either way.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the tuple table. Those integers are what the widget checks and what JSON-RPC delivers. Command.* serialises to the same tuples, so either form works in Python. The table is the protocol. [(4, 12)] is only an example of that shape.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping the Command point — it is your call as author and I will not push it again. For the record I still do not like the tuple table: what I proposed was to keep it as the protocol reference and use Command.* only in the worked example, which I think reads better for someone meeting the widget for the first time. But that is a preference, and it is yours to make.

One thing in that comment is not about tuples vs Command, and I do not think it has been answered yet:

For example, [(4, 12)] links record 12

The objection there is the hardcoded id, not the syntax. Record 12 is a different record in every database, and this block is the one readers copy into their own module. [(4, tag.id)] with the id coming from a search says exactly the same thing, in the same tuple form, without inviting the paste.

Nit, not blocking.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't get me wrong, but sometimes Claude tends to post really weird things, and looks like this is just that case 😄
Anyway, thank you for the feedback!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough — that's a format illustration rather than something to paste, so the literal is fine there. Withdrawn.


**Replace removes relations omitted from the supplied IDs.** In particular,
`[(6, 0, [])]` clears the relation; for One2many fields, Odoo's inverse
field deletion policy can also delete removed records. An empty command
list `[]` makes no relation changes. Other opcodes and malformed command
shapes are rejected for that field as a whole. These checks validate the
payload format; the model method must enforce its own authorization and
Odoo access rights still apply when related records are read or saved.

Free typing (without selecting a row) updates only the Char. Extra fields
keep their previous values until the next select.

A **readonly** extra is still filled in the UI on select. It is persisted
on save only if that field’s view node has `force_save="1"`. Without it,
the fill is dropped on save with no error.

The method must be publicly RPC-callable: not prefixed with `_`, not
`@api.private`, and decorated with `@api.model` so the first argument is
the search string rather than record ids. If the call fails, the dropdown
stays empty and an error notification is shown.

```xml
<field
name="address_string"
widget="autocomplete"
options="{'function': 'address_auto_complete', 'min_symbols': 3, 'debounce': 5}"
/>
<field name="address_ref" readonly="1" force_save="1"/>
<field name="city" invisible="1"/>
```

```python
@api.model
def address_auto_complete(self, value):
"""Return autocomplete rows for ``value``.

:param str | bool value: trimmed input, or ``False`` when ``min_symbols``
is ``0`` and the input is empty
:return: list of dicts whose keys are field names on this model
:rtype: list[dict]
"""
return [
{
"address_string": "string1",
"address_ref": 1,
"city": "Perugia",
},
]
```

| Option | Type | Default if omitted |
|--------|------|-------------------|
| `function` | str (method name) | none — no RPC, Char still editable |
| `min_symbols` | int | `3` |
| `debounce` | int (ms) | `250` |

`min_symbols` `0` opens the dropdown as soon as the field is focused,
before the user types. The method is called immediately with `False`, so
it can return the full list. Typing still sends the trimmed string. The
debounce applies to typing, not to that focus call.

For a dropdown whose options are fetched once (or when a `depending_on`
context key changes), use `web_widget_dropdown_dynamic` instead.
Loading
Loading