pull/2975/merge
Lê Châu 2025-04-23 17:03:47 +02:00 committed by GitHub
commit 1f8251cf96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1236 additions and 0 deletions

View File

@ -1 +1,2 @@
odoo-test-helper
odoo-addon-web_m2x_options @ git+https://github.com/OCA/web.git@refs/pull/2961/head#subdirectory=web_m2x_options

View File

@ -0,0 +1,99 @@
=======================
Web M2X Options Manager
=======================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:e2c7c70fbcb74be8ffaed3747c322112463936bb6fbb5a48c42d659a5f8ddce7
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/18.0/web_m2x_options_manager
: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_m2x_options_manager
: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|
Allows managing the "Create..." and "Create and Edit..." options for
Many2one and Many2many fields directly from the ir.model form view.
**Table of contents**
.. contents::
:local:
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.
Button "Fill" will add every missing field to the options. Button
"Empty" will remove every option.
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_m2x_options_manager%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
-------
* Camptocamp
Contributors
------------
- `Camptocamp <https://www.camptocamp.com>`__:
- Silvio Gregorini
- Duong (Tran Quoc) <duongtq@trobz.com>
- Chau Le <chaulb@trobz.com>
Other credits
-------------
The migration of this module from 17.0 to 18.0 was 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 <https://github.com/OCA/web/tree/18.0/web_m2x_options_manager>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -0,0 +1,4 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models

View File

@ -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": "18.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,
}

View File

@ -0,0 +1,27 @@
<?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">
<list>
<!-- Many2one within list -->
<field name="company_id" options="{'create': False}" />
</list>
</field>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,210 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * web_m2x_options_manager
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: web_m2x_options_manager
#: code:addons/web_m2x_options_manager/models/m2x_create_edit_option.py:0
#, python-format
msgid "'%s' is not a valid field for model '%s'!"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields.selection,name:web_m2x_options_manager.selection__m2x_create_edit_option__option_create__set_true
#: model:ir.model.fields.selection,name:web_m2x_options_manager.selection__m2x_create_edit_option__option_create_edit__set_true
msgid "Add"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__option_create_edit
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
msgid "Create & Edit Wizard"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__option_create
msgid "Create Option"
msgstr ""
#. module: web_m2x_options_manager
#: model_terms:ir.ui.view,arch_db:web_m2x_options_manager.view_model_form_inherit
msgid "Create/Edit Options"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__create_uid
msgid "Created by"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__create_date
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
msgid ""
"Defines behaviour for 'Create & Edit' Wizard\n"
"Set to False to prevent 'Create & Edit' Wizard to pop up"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,help:web_m2x_options_manager.field_m2x_create_edit_option__option_create_edit
msgid ""
"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"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,help:web_m2x_options_manager.field_m2x_create_edit_option__option_create
msgid ""
"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"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_model__display_name
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_model_fields__display_name
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_ui_view__display_name
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__display_name
msgid "Display Name"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields.selection,name:web_m2x_options_manager.selection__m2x_create_edit_option__option_create__none
#: model:ir.model.fields.selection,name:web_m2x_options_manager.selection__m2x_create_edit_option__option_create_edit__none
msgid "Do nothing"
msgstr ""
#. module: web_m2x_options_manager
#: model_terms:ir.ui.view,arch_db:web_m2x_options_manager.view_model_form_inherit
msgid "Empty"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__field_id
msgid "Field"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__field_name
msgid "Field Name"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model,name:web_m2x_options_manager.model_ir_model_fields
msgid "Fields"
msgstr ""
#. module: web_m2x_options_manager
#: model_terms:ir.ui.view,arch_db:web_m2x_options_manager.view_model_form_inherit
msgid "Fields Description"
msgstr ""
#. module: web_m2x_options_manager
#: model_terms:ir.ui.view,arch_db:web_m2x_options_manager.view_model_form_inherit
msgid "Fill"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields.selection,name:web_m2x_options_manager.selection__m2x_create_edit_option__option_create__force_true
#: model:ir.model.fields.selection,name:web_m2x_options_manager.selection__m2x_create_edit_option__option_create_edit__force_true
msgid "Force Add"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields.selection,name:web_m2x_options_manager.selection__m2x_create_edit_option__option_create__force_false
#: model:ir.model.fields.selection,name:web_m2x_options_manager.selection__m2x_create_edit_option__option_create_edit__force_false
msgid "Force Remove"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_model__id
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_model_fields__id
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_ui_view__id
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__id
msgid "ID"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_model____last_update
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_model_fields____last_update
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_ir_ui_view____last_update
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option____last_update
msgid "Last Modified on"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__write_uid
msgid "Last Updated by"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__write_date
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
msgid "M2X Create Edit Option"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model,name:web_m2x_options_manager.model_m2x_create_edit_option
msgid "Manage Options 'Create/Edit' For Fields"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__model_id
msgid "Model"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields,field_description:web_m2x_options_manager.field_m2x_create_edit_option__model_name
msgid "Model Name"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model,name:web_m2x_options_manager.model_ir_model
msgid "Models"
msgstr ""
#. module: web_m2x_options_manager
#: code:addons/web_m2x_options_manager/models/m2x_create_edit_option.py:0
#, python-format
msgid "Only Many2many and Many2one fields can be chosen!"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.constraint,message:web_m2x_options_manager.constraint_m2x_create_edit_option_model_field_uniqueness
msgid "Options must be unique for each model/field couple!"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model.fields.selection,name:web_m2x_options_manager.selection__m2x_create_edit_option__option_create__set_false
#: model:ir.model.fields.selection,name:web_m2x_options_manager.selection__m2x_create_edit_option__option_create_edit__set_false
msgid "Remove"
msgstr ""
#. module: web_m2x_options_manager
#: model:ir.model,name:web_m2x_options_manager.model_ir_ui_view
msgid "View"
msgstr ""

View File

@ -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

View File

@ -0,0 +1,39 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import 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"
_rec_names_search = ["name", "field_description"]

View File

@ -0,0 +1,19 @@
# 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_tag_field(self, node, name_manager, node_info):
res = super()._postprocess_tag_field(node, name_manager, node_info)
if node.tag == "field":
mname = name_manager.model._name
field = name_manager.model._fields.get(node.get("name"))
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

View File

@ -0,0 +1,184 @@
# 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,
)
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,
)
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",
)
option_create_edit_wizard = fields.Boolean(
default=True,
help="Defines behaviour for 'Create & Edit' Wizard\n"
"Set to False to prevent 'Create & Edit' Wizard to pop up",
string="Create & Edit Wizard",
)
_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()`
self.env.registry.clear_all_caches()
return super().create(vals_list)
def write(self, vals):
# Clear cache to avoid misbehavior from cached :meth:`_get()`
self.env.registry.clear_all_caches()
return super().write(vals)
def unlink(self):
# Clear cache to avoid misbehavior from cached :meth:`_get()`
self.env.registry.clear_all_caches()
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 = self.env._(
"'%(field_name)s' is not a valid field for model '%(model_name)s'!",
field_name=opt.field_name,
model_name=opt.model_name,
)
raise ValidationError(msg)
@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 = self.env._("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[f"option_{k}"]
if opt == "none":
continue
mode, val = opt.split("_")
if k not in options:
options[k] = val == "true"
if mode == "force":
options[f"no_{k}"] = val == "false"
if not self.option_create_edit_wizard:
options["no_quick_create"] = 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

View File

@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@ -0,0 +1,4 @@
- [Camptocamp](https://www.camptocamp.com):
- Silvio Gregorini
- Duong (Tran Quoc) \<<duongtq@trobz.com>\>
- Chau Le \<<chaulb@trobz.com>\>

View File

@ -0,0 +1 @@
The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp

View File

@ -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.

View File

@ -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.

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_m2x_create_edit_option_user access_m2x_create_edit_option_user model_m2x_create_edit_option base.group_user 1 0 0 0
3 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

View File

@ -0,0 +1,444 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Web M2X Options Manager</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="web-m2x-options-manager">
<h1 class="title">Web M2X Options Manager</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:e2c7c70fbcb74be8ffaed3747c322112463936bb6fbb5a48c42d659a5f8ddce7
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/web/tree/18.0/web_m2x_options_manager"><img alt="OCA/web" src="https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_m2x_options_manager"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/web&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>Allows managing the “Create…” and “Create and Edit…” options for
Many2one and Many2many fields directly from the ir.model form view.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="toc-entry-6">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>Go to Settings &gt; Technical &gt; Models.</p>
<p>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.</p>
<p>Button “Fill” will add every missing field to the options. Button
“Empty” will remove every option.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_m2x_options_manager%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
<ul class="simple">
<li><a class="reference external" href="https://www.camptocamp.com">Camptocamp</a>:<ul>
<li>Silvio Gregorini</li>
</ul>
</li>
<li>Duong (Tran Quoc) &lt;<a class="reference external" href="mailto:duongtq&#64;trobz.com">duongtq&#64;trobz.com</a>&gt;</li>
<li>Chau Le &lt;<a class="reference external" href="mailto:chaulb&#64;trobz.com">chaulb&#64;trobz.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
<h2><a class="toc-backref" href="#toc-entry-6">Other credits</a></h2>
<p>The migration of this module from 17.0 to 18.0 was financially supported
by Camptocamp</p>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/18.0/web_m2x_options_manager">OCA/web</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -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

View File

@ -0,0 +1,120 @@
# 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 TransactionCase
from odoo.tools.safe_eval import safe_eval
class TestM2xCreateEditOption(TransactionCase):
def setUp(self):
super().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",
"option_create_edit_wizard": 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",
"option_create_edit_wizard": 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",
"option_create_edit_wizard": False,
}
)
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"].get_view(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},
)
self.assertEqual(
(
title_node.attrib.get("can_create"),
title_node.attrib.get("can_write"),
),
("True", "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)
company_node = form_doc.xpath("//field[@name='company_id']")[0]
self.assertEqual(
safe_eval(company_node.attrib.get("options"), nocopy=True),
{
"create": False,
"no_create": False,
"create_edit": True,
"no_quick_create": True,
},
)
# Update options, check that node has been updated too
self.title_opt.option_create_edit = "force_false"
res = self.env["res.partner"].get_view(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, "no_create_edit": True},
)

View File

@ -0,0 +1,36 @@
<?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}"
>
<list editable="bottom">
<field name="model_name" column_invisible="1" />
<field
name="field_id"
context="{'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" />
<field name="option_create_edit_wizard" />
</list>
</field>
</page>
</xpath>
</field>
</record>
</odoo>