diff --git a/web_m2m_inline/README.rst b/web_m2m_inline/README.rst
new file mode 100644
index 000000000000..7c323b48bda8
--- /dev/null
+++ b/web_m2m_inline/README.rst
@@ -0,0 +1,102 @@
+==============
+Web M2m Inline
+==============
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:2b35600f7a72d13ee75692f74827b74a0f0e057462a818e3e1eeb21d6851efff
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |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-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
+ :target: https://github.com/OCA/web/tree/17.0/web_m2m_inline
+ :alt: OCA/web
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/web-17-0/web-17-0-web_m2m_inline
+ :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=17.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module extends the functionality of the Many2Many field in Odoo to
+support inline adjustments. It allows users to directly add or edit
+Many2Many field values within the form view.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Usage
+=====
+
+To use this module, you need to add widget="m2m_inline":
+
+.. code-block:: XML
+
+::
+
+
+
+
+
+
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub 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 `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+-------
+
+* Camptocamp
+
+Contributors
+------------
+
+- ``Trobz ``
+
+ - Tris Doan tridm@trobz.com
+
+Other credits
+-------------
+
+The development of this module has been financially supported by:
+
+- Camptocamp
+
+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 `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/web_m2m_inline/__init__.py b/web_m2m_inline/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/web_m2m_inline/__manifest__.py b/web_m2m_inline/__manifest__.py
new file mode 100644
index 000000000000..d47438e8d5fa
--- /dev/null
+++ b/web_m2m_inline/__manifest__.py
@@ -0,0 +1,17 @@
+# Copyright 2025 Camptocamp
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+{
+ "name": "Web M2m Inline",
+ "summary": """Inline creation/update for M2M field""",
+ "version": "17.0.1.0.0",
+ "license": "AGPL-3",
+ "author": "Camptocamp,Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/web",
+ "depends": ["web"],
+ "assets": {
+ "web.assets_backend": [
+ "web_m2m_inline/static/src/**/*",
+ ],
+ },
+}
diff --git a/web_m2m_inline/pyproject.toml b/web_m2m_inline/pyproject.toml
new file mode 100644
index 000000000000..4231d0cccb3d
--- /dev/null
+++ b/web_m2m_inline/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/web_m2m_inline/readme/CONTRIBUTORS.md b/web_m2m_inline/readme/CONTRIBUTORS.md
new file mode 100644
index 000000000000..6c13d46e2ded
--- /dev/null
+++ b/web_m2m_inline/readme/CONTRIBUTORS.md
@@ -0,0 +1,3 @@
+* `Trobz `
+
+ * Tris Doan
diff --git a/web_m2m_inline/readme/CREDITS.md b/web_m2m_inline/readme/CREDITS.md
new file mode 100644
index 000000000000..88be33a23060
--- /dev/null
+++ b/web_m2m_inline/readme/CREDITS.md
@@ -0,0 +1,3 @@
+The development of this module has been financially supported by:
+
+- Camptocamp
diff --git a/web_m2m_inline/readme/DESCRIPTION.md b/web_m2m_inline/readme/DESCRIPTION.md
new file mode 100644
index 000000000000..a5686fd3ad7d
--- /dev/null
+++ b/web_m2m_inline/readme/DESCRIPTION.md
@@ -0,0 +1 @@
+This module extends the functionality of the Many2Many field in Odoo to support inline adjustments. It allows users to directly add or edit Many2Many field values within the form view.
diff --git a/web_m2m_inline/readme/USAGE.md b/web_m2m_inline/readme/USAGE.md
new file mode 100644
index 000000000000..205de04beefa
--- /dev/null
+++ b/web_m2m_inline/readme/USAGE.md
@@ -0,0 +1,9 @@
+To use this module, you need to add widget="m2m_inline":
+
+.. code-block:: XML
+
+
+
+
+
+
diff --git a/web_m2m_inline/static/description/icon.png b/web_m2m_inline/static/description/icon.png
new file mode 100644
index 000000000000..3a0328b516c4
Binary files /dev/null and b/web_m2m_inline/static/description/icon.png differ
diff --git a/web_m2m_inline/static/src/autocomplete.esm.js b/web_m2m_inline/static/src/autocomplete.esm.js
new file mode 100644
index 000000000000..5509bf796f12
--- /dev/null
+++ b/web_m2m_inline/static/src/autocomplete.esm.js
@@ -0,0 +1,30 @@
+/** @odoo-module **/
+
+import {_t} from "@web/core/l10n/translation";
+import {Many2XAutocomplete} from "@web/views/fields/relational_utils";
+
+export class CustomMany2XAutocomplete extends Many2XAutocomplete {
+ async loadOptionsSource(request) {
+ const res = await super.loadOptionsSource(request);
+ if (this.props.value) {
+ const inputVal = this.autoCompleteContainer.el.querySelector("input").value;
+ const record = await this.orm.call(this.props.resModel, "name_search", [], {
+ name: this.props.value,
+ operator: "ilike",
+ args: [],
+ limit: 1,
+ context: this.props.context,
+ });
+ res.push({
+ label: _t(`Edit ${record[0][1]}`),
+ classList: "o_m2o_dropdown_option o_m2o_dropdown_option_create_edit",
+ action: () =>
+ this._updateRecord(this.props.resModel, record[0], inputVal),
+ });
+ }
+ return res;
+ }
+ async _updateRecord(model, record, changes) {
+ return await this.orm.write(model, [record[0]], {name: changes});
+ }
+}
diff --git a/web_m2m_inline/static/src/list_renderer.esm.js b/web_m2m_inline/static/src/list_renderer.esm.js
new file mode 100644
index 000000000000..0d6e68c80754
--- /dev/null
+++ b/web_m2m_inline/static/src/list_renderer.esm.js
@@ -0,0 +1,96 @@
+/** @odoo-module **/
+
+import {Domain} from "@web/core/domain";
+import {useX2ManyCrud} from "@web/views/fields/relational_utils";
+import {useService} from "@web/core/utils/hooks";
+import {ListRenderer} from "@web/views/list/list_renderer";
+import {CustomMany2XAutocomplete} from "./autocomplete.esm";
+
+export class AutoCompleteListRenderer extends ListRenderer {
+ static components = {
+ ...ListRenderer,
+ CustomMany2XAutocomplete,
+ };
+
+ setup() {
+ super.setup();
+ this.orm = useService("orm");
+ const {saveRecord, removeRecord} = useX2ManyCrud(() => this.props.list, true);
+ this.update = (recordlist) => {
+ if (!recordlist || !Array.isArray(recordlist)) {
+ return;
+ }
+ if (this.selectedRecord) {
+ // Without removing, this record is kept the list
+ removeRecord(this.selectedRecord);
+ }
+ const resIds = recordlist.map((rec) => rec.id);
+ saveRecord(resIds);
+ return this.props.list.leaveEditMode();
+ };
+
+ if (this.props.canQuickCreate) {
+ this.quickCreate = async (name) => {
+ const created = await this.orm.call(
+ this.relation,
+ "name_create",
+ [name],
+ {
+ context: this.props.context,
+ }
+ );
+ saveRecord([created[0]]);
+ return this.props.list.leaveEditMode();
+ };
+ }
+ }
+
+ get showM2OSelectionField() {
+ return !this.props.readonly;
+ }
+
+ get relation() {
+ return this.props.list.records[0].resModel;
+ }
+
+ get string() {
+ return this.record.fields[this.column.name].string || "";
+ }
+
+ getDomain() {
+ const domain =
+ typeof this.props.domain === "function"
+ ? this.props.domain()
+ : this.props.domain;
+ const currentIds = this.props.list._currentIds.filter(
+ (id) => typeof id === "number"
+ );
+ return Domain.and([domain, Domain.not([["id", "in", currentIds]])]).toList(
+ this.props.context
+ );
+ }
+
+ /**
+ * Override to store selected record
+ */
+ async onCellClicked(record, column, ev) {
+ await super.onCellClicked(record, column, ev);
+ if (!record.isNew) {
+ this.selectedRecord = record;
+ }
+ }
+}
+
+AutoCompleteListRenderer.recordRowTemplate =
+ "c2c_governance.AutoCompleteListRenderer.recordRowTemplate";
+
+AutoCompleteListRenderer.props = [
+ ...ListRenderer.props,
+ "canCreate?",
+ "canQuickCreate?",
+ "canCreateEdit?",
+ "createDomain?",
+ "context?",
+ "domain?",
+ "readonly?",
+];
diff --git a/web_m2m_inline/static/src/list_renderer.xml b/web_m2m_inline/static/src/list_renderer.xml
new file mode 100644
index 000000000000..9d70a6b25d90
--- /dev/null
+++ b/web_m2m_inline/static/src/list_renderer.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
diff --git a/web_m2m_inline/static/src/m2m_inline.esm.js b/web_m2m_inline/static/src/m2m_inline.esm.js
new file mode 100644
index 000000000000..9b862662e1e6
--- /dev/null
+++ b/web_m2m_inline/static/src/m2m_inline.esm.js
@@ -0,0 +1,93 @@
+/** @odoo-module **/
+
+import {_t} from "@web/core/l10n/translation";
+import {evaluateBooleanExpr} from "@web/core/py_js/py";
+import {registry} from "@web/core/registry";
+import {X2ManyField, x2ManyField} from "@web/views/fields/x2many/x2many_field";
+import {AutoCompleteListRenderer} from "./list_renderer.esm";
+
+export class M2mInline extends X2ManyField {
+ static components = {
+ ...x2ManyField.components,
+ ListRenderer: AutoCompleteListRenderer,
+ };
+ static props = {
+ ...X2ManyField.props,
+ canCreate: {type: Boolean, optional: true},
+ canQuickCreate: {type: Boolean, optional: true},
+ canCreateEdit: {type: Boolean, optional: true},
+ };
+ static defaultProps = {
+ canCreate: true,
+ canQuickCreate: true,
+ canCreateEdit: true,
+ };
+
+ get isMany2Many() {
+ return false;
+ }
+
+ get rendererProps() {
+ const res = super.rendererProps;
+ const newProps = {
+ ...res,
+ canCreate: this.props.canCreate,
+ canQuickCreate: this.props.canQuickCreate,
+ canCreateEdit: this.props.canCreateEdit,
+ createDomain: this.props.createDomain,
+ context: this.props.context,
+ domain: this.props.domain,
+ readonly: this.props.readonly,
+ };
+ return newProps;
+ }
+}
+
+export const m2mInline = {
+ ...x2ManyField,
+ component: M2mInline,
+ supportedOptions: [
+ {
+ label: _t("Disable creation"),
+ name: "no_create",
+ type: "boolean",
+ help: _t(
+ "If checked, users won't be able to create records through the autocomplete dropdown at all."
+ ),
+ },
+ {
+ label: _t("Disable 'Create' option"),
+ name: "no_quick_create",
+ type: "boolean",
+ help: _t(
+ "If checked, users will not be able to create records based on the text input; they will still be able to create records via a popup form."
+ ),
+ },
+ ],
+ supportedTypes: ["many2many"],
+ extractProps(
+ {attrs, relatedFields, viewMode, views, widget, options, string},
+ dynamicInfo
+ ) {
+ const props = x2ManyField.extractProps(
+ {attrs, relatedFields, viewMode, views, widget, options, string},
+ dynamicInfo
+ );
+ const hasCreatePermission = attrs.can_create
+ ? evaluateBooleanExpr(attrs.can_create)
+ : true;
+ const noCreate = Boolean(options.no_create);
+ const canCreate = noCreate ? false : hasCreatePermission;
+ const noQuickCreate = Boolean(options.no_quick_create);
+
+ return {
+ ...props,
+ canCreate,
+ canQuickCreate: canCreate && !noQuickCreate,
+ context: dynamicInfo.context,
+ domain: dynamicInfo.domain,
+ string,
+ };
+ },
+};
+registry.category("fields").add("m2m_inline", m2mInline);
diff --git a/web_m2m_inline/static/src/m2m_inline.scss b/web_m2m_inline/static/src/m2m_inline.scss
new file mode 100644
index 000000000000..d040458a739d
--- /dev/null
+++ b/web_m2m_inline/static/src/m2m_inline.scss
@@ -0,0 +1,5 @@
+.o_field_m2m_inline {
+ input {
+ border: none; // disable border of Component Autocomplete
+ }
+}