forked from Techsystech/web
commit
182a61866d
|
@ -0,0 +1 @@
|
||||||
|
../../../../web_m2x_options_manager
|
|
@ -0,0 +1,6 @@
|
||||||
|
import setuptools
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
setup_requires=['setuptools-odoo'],
|
||||||
|
odoo_addon=True,
|
||||||
|
)
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Copyright 2021 Camptocamp SA
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import models
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Copyright 2021 Camptocamp SA
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Web M2X Options Manager",
|
||||||
|
"summary": 'Adds an interface to manage the "Create" and'
|
||||||
|
' "Create and Edit" options for specific models and'
|
||||||
|
" fields.",
|
||||||
|
"version": "14.0.1.0.0",
|
||||||
|
"author": "Camptocamp, Odoo Community Association (OCA)",
|
||||||
|
"license": "AGPL-3",
|
||||||
|
"category": "Web",
|
||||||
|
"data": [
|
||||||
|
"security/ir.model.access.csv",
|
||||||
|
"views/ir_model.xml",
|
||||||
|
],
|
||||||
|
"demo": [
|
||||||
|
"demo/res_partner_demo_view.xml",
|
||||||
|
],
|
||||||
|
"depends": ["base", "web_m2x_options"],
|
||||||
|
"website": "https://github.com/OCA/web",
|
||||||
|
"installable": True,
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="res_partner_demo_form_view" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.demo.form.view</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="priority">1000</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<!-- Many2one -->
|
||||||
|
<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>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Copyright 2021 Camptocamp SA
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import ir_model
|
||||||
|
from . import ir_ui_view
|
||||||
|
from . import m2x_create_edit_option
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Copyright 2021 Camptocamp SA
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class IrModel(models.Model):
|
||||||
|
_inherit = "ir.model"
|
||||||
|
|
||||||
|
m2x_create_edit_option_ids = fields.One2many(
|
||||||
|
"m2x.create.edit.option",
|
||||||
|
"model_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Copyright 2021 Camptocamp SA
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class IrUiView(models.Model):
|
||||||
|
_inherit = "ir.ui.view"
|
||||||
|
|
||||||
|
def postprocess(self, node, current_node_path, editable, name_manager):
|
||||||
|
res = super().postprocess(node, current_node_path, editable, name_manager)
|
||||||
|
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)
|
||||||
|
return res
|
|
@ -0,0 +1,170 @@
|
||||||
|
# Copyright 2021 Camptocamp SA
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.tools.cache import ormcache
|
||||||
|
from odoo.tools.safe_eval import safe_eval
|
||||||
|
|
||||||
|
|
||||||
|
class M2xCreateEditOption(models.Model):
|
||||||
|
_name = "m2x.create.edit.option"
|
||||||
|
_description = "Manage Options 'Create/Edit' For Fields"
|
||||||
|
|
||||||
|
field_id = fields.Many2one(
|
||||||
|
"ir.model.fields",
|
||||||
|
domain=[("ttype", "in", ("many2many", "many2one"))],
|
||||||
|
ondelete="cascade",
|
||||||
|
required=True,
|
||||||
|
string="Field",
|
||||||
|
)
|
||||||
|
|
||||||
|
field_name = fields.Char(
|
||||||
|
related="field_id.name",
|
||||||
|
store=True,
|
||||||
|
string="Field Name",
|
||||||
|
)
|
||||||
|
|
||||||
|
model_id = fields.Many2one(
|
||||||
|
"ir.model",
|
||||||
|
ondelete="cascade",
|
||||||
|
required=True,
|
||||||
|
string="Model",
|
||||||
|
)
|
||||||
|
|
||||||
|
model_name = fields.Char(
|
||||||
|
compute="_compute_model_name",
|
||||||
|
inverse="_inverse_model_name",
|
||||||
|
store=True,
|
||||||
|
string="Model Name",
|
||||||
|
)
|
||||||
|
|
||||||
|
option_create = fields.Selection(
|
||||||
|
[
|
||||||
|
("none", "Do nothing"),
|
||||||
|
("set_true", "Add"),
|
||||||
|
("force_true", "Force Add"),
|
||||||
|
("set_false", "Remove"),
|
||||||
|
("force_false", "Force Remove"),
|
||||||
|
],
|
||||||
|
default="set_false",
|
||||||
|
help="Defines behaviour for 'Create' option:\n"
|
||||||
|
"* Do nothing: nothing is done\n"
|
||||||
|
"* Add/Remove: option 'Create' is set to True/False only if not"
|
||||||
|
" already present in view definition\n"
|
||||||
|
"* Force Add/Remove: option 'Create' is always set to True/False,"
|
||||||
|
" overriding any pre-existing option",
|
||||||
|
required=True,
|
||||||
|
string="Create Option",
|
||||||
|
)
|
||||||
|
|
||||||
|
option_create_edit = fields.Selection(
|
||||||
|
[
|
||||||
|
("none", "Do nothing"),
|
||||||
|
("set_true", "Add"),
|
||||||
|
("force_true", "Force Add"),
|
||||||
|
("set_false", "Remove"),
|
||||||
|
("force_false", "Force Remove"),
|
||||||
|
],
|
||||||
|
default="set_false",
|
||||||
|
help="Defines behaviour for 'Create & Edit' option:\n"
|
||||||
|
"* Do nothing: nothing is done\n"
|
||||||
|
"* Add/Remove: option 'Create & Edit' is set to True/False only if not"
|
||||||
|
" already present in view definition\n"
|
||||||
|
"* Force Add/Remove: option 'Create & Edit' is always set to"
|
||||||
|
" True/False, overriding any pre-existing option",
|
||||||
|
required=True,
|
||||||
|
string="Create & Edit Option",
|
||||||
|
)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
(
|
||||||
|
"model_field_uniqueness",
|
||||||
|
"unique(field_id,model_id)",
|
||||||
|
"Options must be unique for each model/field couple!",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
# Clear cache to avoid misbehavior from cached :meth:`_get()`
|
||||||
|
type(self)._get.clear_cache(self.browse())
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
# Clear cache to avoid misbehavior from cached :meth:`_get()`
|
||||||
|
type(self)._get.clear_cache(self.browse())
|
||||||
|
return super().write(vals)
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
# Clear cache to avoid misbehavior from cached :meth:`_get()`
|
||||||
|
type(self)._get.clear_cache(self.browse())
|
||||||
|
return super().unlink()
|
||||||
|
|
||||||
|
@api.depends("model_id")
|
||||||
|
def _compute_model_name(self):
|
||||||
|
for opt in self:
|
||||||
|
opt.model_name = opt.model_id.model
|
||||||
|
|
||||||
|
def _inverse_model_name(self):
|
||||||
|
getter = self.env["ir.model"]._get
|
||||||
|
for opt in self:
|
||||||
|
# This also works as a constrain: if ``model_name`` is not a
|
||||||
|
# valid model name, then ``model_id`` will be emptied, but it's
|
||||||
|
# a required field!
|
||||||
|
opt.model_id = getter(opt.model_name)
|
||||||
|
|
||||||
|
@api.constrains("model_id", "field_id")
|
||||||
|
def _check_field_in_model(self):
|
||||||
|
for opt in self:
|
||||||
|
if opt.field_id.model_id != opt.model_id:
|
||||||
|
msg = _("'%s' is not a valid field for model '%s'!")
|
||||||
|
raise ValidationError(msg % (opt.field_name, opt.model_name))
|
||||||
|
|
||||||
|
@api.constrains("field_id")
|
||||||
|
def _check_field_type(self):
|
||||||
|
ttypes = ("many2many", "many2one")
|
||||||
|
if any(o.field_id.ttype not in ttypes for o in self):
|
||||||
|
msg = _("Only Many2many and Many2one fields can be chosen!")
|
||||||
|
raise ValidationError(msg)
|
||||||
|
|
||||||
|
def _apply_options(self, node):
|
||||||
|
"""Applies options ``self`` to ``node``"""
|
||||||
|
self.ensure_one()
|
||||||
|
options = node.attrib.get("options") or {}
|
||||||
|
if isinstance(options, str):
|
||||||
|
options = safe_eval(options, dict(self.env.context or [])) or {}
|
||||||
|
for k in ("create", "create_edit"):
|
||||||
|
opt = self["option_%s" % k]
|
||||||
|
if opt == "none":
|
||||||
|
continue
|
||||||
|
mode, val = opt.split("_")
|
||||||
|
if mode == "force" or k not in options:
|
||||||
|
options[k] = val == "true"
|
||||||
|
node.set("options", str(options))
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get(self, model_name, field_name):
|
||||||
|
"""Returns specific record for ``field_name`` in ``model_name``
|
||||||
|
|
||||||
|
:param str model_name: technical model name (i.e. "sale.order")
|
||||||
|
:param str field_name: technical field name (i.e. "partner_id")
|
||||||
|
"""
|
||||||
|
return self.browse(self._get(model_name, field_name))
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
@ormcache("model_name", "field_name")
|
||||||
|
def _get(self, model_name, field_name):
|
||||||
|
"""Inner implementation of ``get``.
|
||||||
|
An ID is returned to allow caching (see :class:`ormcache`); :meth:`get`
|
||||||
|
will then convert it to a proper record.
|
||||||
|
|
||||||
|
:param str model_name: technical model name (i.e. "sale.order")
|
||||||
|
:param str field_name: technical field name (i.e. "partner_id")
|
||||||
|
"""
|
||||||
|
dom = [
|
||||||
|
("model_name", "=", model_name),
|
||||||
|
("field_name", "=", field_name),
|
||||||
|
]
|
||||||
|
# `_check_field_model_uniqueness()` grants uniqueness if existing
|
||||||
|
return self.search(dom, limit=1).id
|
|
@ -0,0 +1,3 @@
|
||||||
|
* `Camptocamp <https://www.camptocamp.com>`__:
|
||||||
|
|
||||||
|
* Silvio Gregorini
|
|
@ -0,0 +1,2 @@
|
||||||
|
Allows managing the "Create..." and "Create and Edit..." options for `Many2one`
|
||||||
|
and `Many2many` fields directly from the `ir.model` form view.
|
|
@ -0,0 +1,7 @@
|
||||||
|
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.
|
||||||
|
|
||||||
|
Button "Fill" will add every missing field to the options.
|
||||||
|
Button "Empty" will remove every option.
|
|
@ -0,0 +1,3 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_m2x_create_edit_option_user,access_m2x_create_edit_option_user,model_m2x_create_edit_option,base.group_user,1,0,0,0
|
||||||
|
access_m2x_create_edit_option_system,access_m2x_create_edit_option_system,model_m2x_create_edit_option,base.group_system,1,1,1,1
|
|
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
|
@ -0,0 +1,4 @@
|
||||||
|
# Copyright 2021 Camptocamp SA
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import test_m2x_create_edit_option
|
|
@ -0,0 +1,109 @@
|
||||||
|
# Copyright 2021 Camptocamp SA
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.tests.common import SavepointCase
|
||||||
|
from odoo.tools.safe_eval import safe_eval
|
||||||
|
|
||||||
|
|
||||||
|
class TestM2xCreateEditOption(SavepointCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestM2xCreateEditOption, self).setUp()
|
||||||
|
ref = self.env.ref
|
||||||
|
# View to be used
|
||||||
|
self.view = ref("web_m2x_options_manager.res_partner_demo_form_view")
|
||||||
|
# res.partner model and fields
|
||||||
|
self.res_partner_model = ref("base.model_res_partner")
|
||||||
|
self.categ_field = ref("base.field_res_partner__category_id")
|
||||||
|
self.title_field = ref("base.field_res_partner__title")
|
||||||
|
self.users_field = ref("base.field_res_partner__user_ids")
|
||||||
|
# res.users model and fields
|
||||||
|
self.res_users_model = ref("base.model_res_users")
|
||||||
|
self.company_field = ref("base.field_res_users__company_id")
|
||||||
|
# Options setup
|
||||||
|
self.title_opt = self.env["m2x.create.edit.option"].create(
|
||||||
|
{
|
||||||
|
"field_id": self.title_field.id,
|
||||||
|
"model_id": self.res_partner_model.id,
|
||||||
|
"option_create": "set_true",
|
||||||
|
"option_create_edit": "set_true",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.categories_opt = self.env["m2x.create.edit.option"].create(
|
||||||
|
{
|
||||||
|
"field_id": self.categ_field.id,
|
||||||
|
"model_id": self.res_partner_model.id,
|
||||||
|
"option_create": "set_true",
|
||||||
|
"option_create_edit": "set_true",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.company_opt = self.env["m2x.create.edit.option"].create(
|
||||||
|
{
|
||||||
|
"field_id": self.company_field.id,
|
||||||
|
"model_id": self.res_users_model.id,
|
||||||
|
"option_create": "force_true",
|
||||||
|
"option_create_edit": "set_true",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
# Fails ``_check_field_in_model``: model is res.partner, field is
|
||||||
|
# res.users's company_id
|
||||||
|
self.env["m2x.create.edit.option"].create(
|
||||||
|
{
|
||||||
|
"field_id": self.company_field.id,
|
||||||
|
"model_id": self.res_partner_model.id,
|
||||||
|
"option_create": "set_true",
|
||||||
|
"option_create_edit": "set_true",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
# Fails ``_check_field_type``: users_field is a One2many
|
||||||
|
self.env["m2x.create.edit.option"].create(
|
||||||
|
{
|
||||||
|
"field_id": self.users_field.id,
|
||||||
|
"model_id": self.res_partner_model.id,
|
||||||
|
"option_create": "set_true",
|
||||||
|
"option_create_edit": "set_true",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_apply_options(self):
|
||||||
|
res = self.env["res.partner"].fields_view_get(self.view.id)
|
||||||
|
|
||||||
|
# Check fields on res.partner form view
|
||||||
|
form_arch = res["arch"]
|
||||||
|
form_doc = etree.XML(form_arch)
|
||||||
|
title_node = form_doc.xpath("//field[@name='title']")[0]
|
||||||
|
self.assertEqual(
|
||||||
|
safe_eval(title_node.attrib.get("options"), nocopy=True),
|
||||||
|
{"create": True, "create_edit": True},
|
||||||
|
)
|
||||||
|
categ_node = form_doc.xpath("//field[@name='category_id']")[0]
|
||||||
|
self.assertEqual(
|
||||||
|
safe_eval(categ_node.attrib.get("options"), nocopy=True),
|
||||||
|
{"create": False, "create_edit": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check fields on res.users tree view (contained in ``user_ids`` field)
|
||||||
|
tree_arch = res["fields"]["user_ids"]["views"]["tree"]["arch"]
|
||||||
|
tree_doc = etree.XML(tree_arch)
|
||||||
|
company_node = tree_doc.xpath("//field[@name='company_id']")[0]
|
||||||
|
self.assertEqual(
|
||||||
|
safe_eval(company_node.attrib.get("options"), nocopy=True),
|
||||||
|
{"create": True, "create_edit": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update options, check that node has been updated too
|
||||||
|
self.title_opt.option_create_edit = "force_false"
|
||||||
|
res = self.env["res.partner"].fields_view_get(self.view.id)
|
||||||
|
form_arch = res["arch"]
|
||||||
|
form_doc = etree.XML(form_arch)
|
||||||
|
title_node = form_doc.xpath("//field[@name='title']")[0]
|
||||||
|
self.assertEqual(
|
||||||
|
safe_eval(title_node.attrib.get("options"), nocopy=True),
|
||||||
|
{"create": True, "create_edit": False},
|
||||||
|
)
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_model_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">view.model.form.inherit</field>
|
||||||
|
<field name="model">ir.model</field>
|
||||||
|
<field name="inherit_id" ref="base.view_model_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//notebook/page[@name='fields']" position="after">
|
||||||
|
<page string="Create/Edit Options" name="create_edit_options">
|
||||||
|
<div>
|
||||||
|
<button name="button_fill" type="object" string="Fill" />
|
||||||
|
<button name="button_empty" type="object" string="Empty" />
|
||||||
|
</div>
|
||||||
|
<field
|
||||||
|
name="m2x_create_edit_option_ids"
|
||||||
|
nolabel="1"
|
||||||
|
context="{'default_model_name': model}"
|
||||||
|
>
|
||||||
|
<tree string="Fields Description" editable="bottom">
|
||||||
|
<field name="model_name" invisible="1" />
|
||||||
|
<field
|
||||||
|
name="field_id"
|
||||||
|
context="{'search_by_technical_name': True, 'display_technical_name': True}"
|
||||||
|
domain="[('ttype', 'in', ('many2many', 'many2one')), ('model_id.model', '=', model_name)]"
|
||||||
|
options="{'create': False, 'create_edit': False}"
|
||||||
|
/>
|
||||||
|
<field name="option_create" />
|
||||||
|
<field name="option_create_edit" />
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
Loading…
Reference in New Issue