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
1 change: 1 addition & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
odoo-test-helper
13 changes: 9 additions & 4 deletions web_m2x_options_manager/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,16 @@ Usage

Go to Settings > Technical > Models.

Choose the model you wish to edit, and open its form view. Go to the
"Create/Edit Options" tab, and add the fields you want to manage.
Choose the model you wish to edit, and open its form view. Go to the "Create/Edit Options" tab,
and add the fields you want to manage in 2 different sections:

Button "Fill" will add every missing field to the options.
Button "Empty" will remove every option.
* the first list view allows you to handle fields for the selected model
* the second list view allows you to handle fields where the selected model is the comodel

For both sections:

* button "Fill" will add every missing field to the options
* button "Empty" will remove every option

Bug Tracker
===========
Expand Down
4 changes: 1 addition & 3 deletions web_m2x_options_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import models
from .hooks import pre_init_hook
11 changes: 9 additions & 2 deletions web_m2x_options_manager/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@
"summary": 'Adds an interface to manage the "Create" and'
' "Create and Edit" options for specific models and'
" fields.",
"version": "15.0.1.0.0",
"version": "15.0.2.0.0",
"author": "Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"category": "Web",
"data": [
"security/ir.model.access.csv",
"views/ir_model.xml",
"views/m2x_create_edit_option.xml",
],
"demo": [
"demo/res_partner_demo_view.xml",
],
"depends": ["base", "web_m2x_options"],
"depends": [
# OCA/server-tools
"base_view_inheritance_extension",
# OCA/web
"web_m2x_options",
],
"website": "https://github.com/OCA/web",
"installable": True,
"pre_init_hook": "pre_init_hook",
}
21 changes: 11 additions & 10 deletions web_m2x_options_manager/demo/res_partner_demo_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@
<form>
<sheet>
<group>
<!-- Many2one -->
<!-- Many2one w/o options -->
<field name="title" />
<!-- Many2many -->
<field name="category_id" options="{'create': False}" />
<!-- One2many -->
<field name="user_ids">
<tree>
<!-- Many2one within tree -->
<field name="company_id" options="{'create': False}" />
</tree>
</field>
<!-- Many2one w/ options -->
<field
name="parent_id"
options="{'create': False, 'create_edit': False, 'm2o_dialog': False}"
/>
<!-- Many2many w/ options -->
<field
name="category_id"
options="{'create': False, 'create_edit': False, 'm2o_dialog': False}"
/>
</group>
</sheet>
</form>
Expand Down
11 changes: 11 additions & 0 deletions web_m2x_options_manager/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright 2025 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from .tools import prepare_column_can_have_options, prepare_column_comodel_id


def pre_init_hook(cr):
# Pre-create and pre-fill these columns for perf reasons (might take a while to
# let Odoo do it via the ORM for huge DBs)
prepare_column_can_have_options(cr)
prepare_column_comodel_id(cr)
6 changes: 3 additions & 3 deletions web_m2x_options_manager/i18n/es.po
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ msgid "Create & Edit Option"
msgstr "Opción Crear y Editar"

#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__option_create_edit_wizard
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__option_m2o_dialog
msgid "Create & Edit Wizard"
msgstr "Asistente de Creación y Edición"

Expand All @@ -59,7 +59,7 @@ msgid "Created on"
msgstr "Creado el"

#. module: web_m2x_options_manager
#: model:ir.model.fields,help:web_m2x_options_manager.field_m2x_create_edit_option__option_create_edit_wizard
#: model:ir.model.fields,help:web_m2x_options_manager.field_m2x_create_edit_option__option_m2o_dialog
msgid ""
"Defines behaviour for 'Create & Edit' Wizard\n"
"Set to False to prevent 'Create & Edit' Wizard to pop up"
Expand Down Expand Up @@ -166,7 +166,7 @@ msgid "Last Updated on"
msgstr "Última Actualización el"

#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_model__m2x_create_edit_option_ids
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_model__m2x_option_ids
msgid "M2X Create Edit Option"
msgstr "M2X Crear Editar Opción"

Expand Down
6 changes: 3 additions & 3 deletions web_m2x_options_manager/i18n/web_m2x_options_manager.pot
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ msgid "Create & Edit Option"
msgstr ""

#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__option_create_edit_wizard
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__option_m2o_dialog
msgid "Create & Edit Wizard"
msgstr ""

Expand All @@ -56,7 +56,7 @@ msgid "Created on"
msgstr ""

#. module: web_m2x_options_manager
#: model:ir.model.fields,help:web_m2x_options_manager.field_m2x_create_edit_option__option_create_edit_wizard
#: model:ir.model.fields,help:web_m2x_options_manager.field_m2x_create_edit_option__option_m2o_dialog
msgid ""
"Defines behaviour for 'Create & Edit' Wizard\n"
"Set to False to prevent 'Create & Edit' Wizard to pop up"
Expand Down Expand Up @@ -149,7 +149,7 @@ msgid "Last Updated on"
msgstr ""

#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_model__m2x_create_edit_option_ids
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_model__m2x_option_ids
msgid "M2X Create Edit Option"
msgstr ""

Expand Down
41 changes: 41 additions & 0 deletions web_m2x_options_manager/migrations/15.0.2.0.0/pre-mig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2025 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo.tools.sql import column_exists, create_column, drop_constraint

# pylint: disable=odoo-addons-relative-import
from odoo.addons.web_m2x_options_manager.tools import (
prepare_column_can_have_options,
prepare_column_comodel_id,
)


def migrate(cr, version):
if not version:
return

# Migrate values from ``option_create_edit_wizard`` to ``option_m2o_dialog``
if not column_exists(cr, "m2x_create_edit_option", "option_m2o_dialog"):
create_column(cr, "m2x_create_edit_option", "option_m2o_dialog", "varchar")
cr.execute(
"""
UPDATE m2x_create_edit_option
SET option_m2o_dialog =
CASE
WHEN not option_create_edit_wizard THEN 'set_false'
ELSE 'null'
END
"""
)

# Pre-create and pre-fill these columns for perf reasons (might take a while to
# let Odoo do it via the ORM for huge DBs)
prepare_column_can_have_options(cr)
prepare_column_comodel_id(cr)

# Replaced by SQL constraint ``m2x_create_edit_option_field_uniqueness``
drop_constraint(
cr,
tablename="m2x_create_edit_option",
constraintname="m2x_create_edit_option_model_field_uniqueness",
)
4 changes: 1 addition & 3 deletions web_m2x_options_manager/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import ir_model
from . import ir_model_fields
from . import ir_ui_view
from . import m2x_create_edit_option
92 changes: 52 additions & 40 deletions web_m2x_options_manager/models/ir_model.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,64 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import api, fields, models
from odoo import fields, models


class IrModel(models.Model):
_inherit = "ir.model"

m2x_create_edit_option_ids = fields.One2many(
m2x_option_ids = fields.One2many(
"m2x.create.edit.option",
"model_id",
)
m2x_comodels_option_ids = fields.One2many(
"m2x.create.edit.option",
"comodel_id",
)
comodel_field_ids = fields.One2many("ir.model.fields", "comodel_id")

def button_empty_m2x_options(self):
self._empty_m2x_options(own=True)

def button_fill_m2x_options(self):
self._fill_m2x_options(own=True)

def button_empty_m2x_comodels_options(self):
self._empty_m2x_options(comodels=True)

def button_fill_m2x_comodels_options(self):
self._fill_m2x_options(comodels=True)

def _empty_m2x_options(self, own=False, comodels=False):
"""Removes every option for model ``self``'s fields

:param bool own: if True, deletes options for model's fields
:param bool comodels: if True, deletes options for fields where ``self`` is
the field's comodel
"""
to_delete = self.env["m2x.create.edit.option"]
if own:
to_delete += self.m2x_option_ids
if comodels:
to_delete += self.m2x_comodels_option_ids
if to_delete:
to_delete.unlink()

def _fill_m2x_options(self, own=False, comodels=False):
"""Adds every missing field option for model ``self`` (with default values)

def button_empty(self):
for ir_model in self:
ir_model._empty_m2x_create_edit_option()

def button_fill(self):
for ir_model in self:
ir_model._fill_m2x_create_edit_option()

def _empty_m2x_create_edit_option(self):
"""Removes every option for model ``self``"""
self.ensure_one()
self.m2x_create_edit_option_ids.unlink()

def _fill_m2x_create_edit_option(self):
"""Adds every missing field option for model ``self``"""
self.ensure_one()
existing = self.m2x_create_edit_option_ids.mapped("field_id")
valid = self.field_id.filtered(lambda f: f.ttype in ("many2many", "many2one"))
vals = [(0, 0, {"field_id": f.id}) for f in valid - existing]
self.write({"m2x_create_edit_option_ids": vals})


class IrModelFields(models.Model):
_inherit = "ir.model.fields"

@api.model
def name_search(self, name="", args=None, operator="ilike", limit=100):
res = super().name_search(name, args, operator, limit)
if not (name and self.env.context.get("search_by_technical_name")):
return res
domain = list(args or []) + [("name", operator, name)]
new_fids = self.search(domain, limit=limit).ids
for fid in [x[0] for x in res]:
if fid not in new_fids:
new_fids.append(fid)
if limit and limit > 0:
new_fids = new_fids[:limit]
return self.browse(new_fids).sudo().name_get()
:param bool own: if True, creates options for model's fields
:param bool comodels: if True, creates options for fields where ``self`` is
the field's comodel
"""
todo = set()
if own:
exist = self.m2x_option_ids.field_id
valid = self.field_id.filtered("can_have_options")
todo.update((valid - exist).ids)
if comodels:
exist = self.m2x_comodels_option_ids.field_id
valid = self.comodel_field_ids.filtered("can_have_options")
todo.update((valid - exist).ids)
if todo:
self.env["m2x.create.edit.option"].create([{"field_id": i} for i in todo])
53 changes: 53 additions & 0 deletions web_m2x_options_manager/models/ir_model_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2025 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import api, fields, models
from odoo.osv.expression import AND


class IrModelFields(models.Model):
_inherit = "ir.model.fields"

can_have_options = fields.Boolean(compute="_compute_can_have_options", store=True)
comodel_id = fields.Many2one(
"ir.model", compute="_compute_comodel_id", store=True, index=True
)

@api.depends("ttype")
def _compute_can_have_options(self):
for field in self:
field.can_have_options = field.ttype in ("many2many", "many2one")

@api.depends("relation")
def _compute_comodel_id(self):
empty = self.env["ir.model"]
getter = self.env["ir.model"]._get
for field in self:
if field.relation:
field.comodel_id = getter(field.relation)
else:
field.comodel_id = empty

@api.model
def name_search(self, name="", args=None, operator="ilike", limit=100):
# OVERRIDE: allow searching by field tech name if the correct context key is
# used; in this case, fields fetched by tech name are prepended to other fields
result = super().name_search(name, args, operator, limit)
if not (name and self.env.context.get("search_by_technical_name")):
return result
domain = AND([args or [], [("name", operator, name)]])
new_fields = self.search_read(domain, fields=["display_name"], limit=limit)
new_result = {f["id"]: f["display_name"] for f in new_fields}
while result and not (limit and 0 < limit <= len(new_result)):
field_id, field_display_name = result.pop(0)
if field_id not in new_result:
new_result[field_id] = field_display_name
return list(new_result.items())

@api.model
def _search(self, args, **kwargs):
# OVERRIDE: allow defining filtering custom domain on model/comodel when
# searching fields for O2M list views on ``m2x.create.edit.option``
if self.env.context.get("o2m_list_view_m2x_domain"):
args = AND([list(args or []), self.env.context["o2m_list_view_m2x_domain"]])
return super()._search(args, **kwargs)
17 changes: 9 additions & 8 deletions web_m2x_options_manager/models/ir_ui_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ class IrUiView(models.Model):
_inherit = "ir.ui.view"

def _postprocess_tag_field(self, node, name_manager, node_info):
# OVERRIDE: check ``m2x.create.edit.option`` config when processing a ``field``
# node in views
res = super()._postprocess_tag_field(node, name_manager, node_info)
if node.tag == "field":
mname = name_manager.model._name
fname = node.attrib["name"]
field = self.env[mname]._fields.get(fname)
if field and field.type in ("many2many", "many2one"):
rec = self.env["m2x.create.edit.option"].get(mname, field.name)
if rec:
rec._apply_options(node)
m2x_option = self.env["m2x.create.edit.option"].get(
name_manager.model._name,
# ``name`` is required in ``<field/>`` items
node.attrib["name"],
)
if m2x_option:
m2x_option._apply_options(node)
return res
Loading
Loading