odoo-addon-web-m2x-options-manager 16.0.1.0.0.3__py3-none-any.whl → 17.0.1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/web_m2x_options_manager/README.rst +29 -18
- odoo/addons/web_m2x_options_manager/__init__.py +1 -3
- odoo/addons/web_m2x_options_manager/__manifest__.py +9 -2
- odoo/addons/web_m2x_options_manager/demo/res_partner_demo_view.xml +11 -10
- odoo/addons/web_m2x_options_manager/hooks.py +11 -0
- odoo/addons/web_m2x_options_manager/i18n/it.po +1 -16
- odoo/addons/web_m2x_options_manager/i18n/web_m2x_options_manager.pot +81 -33
- odoo/addons/web_m2x_options_manager/models/__init__.py +1 -3
- odoo/addons/web_m2x_options_manager/models/ir_model.py +46 -21
- odoo/addons/web_m2x_options_manager/models/ir_model_fields.py +53 -0
- odoo/addons/web_m2x_options_manager/models/ir_ui_view.py +9 -7
- odoo/addons/web_m2x_options_manager/models/m2x_create_edit_option.py +133 -76
- odoo/addons/web_m2x_options_manager/readme/CONTRIBUTORS.md +2 -0
- odoo/addons/web_m2x_options_manager/readme/USAGE.md +12 -0
- odoo/addons/web_m2x_options_manager/static/description/index.html +49 -32
- odoo/addons/web_m2x_options_manager/tests/__init__.py +2 -3
- odoo/addons/web_m2x_options_manager/tests/common.py +48 -0
- odoo/addons/web_m2x_options_manager/tests/test_ir_model.py +37 -0
- odoo/addons/web_m2x_options_manager/tests/test_ir_model_fields.py +99 -0
- odoo/addons/web_m2x_options_manager/tests/test_m2x_create_edit_option.py +67 -92
- odoo/addons/web_m2x_options_manager/tools.py +34 -0
- odoo/addons/web_m2x_options_manager/views/ir_model.xml +38 -18
- odoo/addons/web_m2x_options_manager/views/m2x_create_edit_option.xml +157 -0
- {odoo_addon_web_m2x_options_manager-16.0.1.0.0.3.dist-info → odoo_addon_web_m2x_options_manager-17.0.1.0.1.dist-info}/METADATA +37 -24
- odoo_addon_web_m2x_options_manager-17.0.1.0.1.dist-info/RECORD +30 -0
- {odoo_addon_web_m2x_options_manager-16.0.1.0.0.3.dist-info → odoo_addon_web_m2x_options_manager-17.0.1.0.1.dist-info}/WHEEL +1 -1
- odoo_addon_web_m2x_options_manager-17.0.1.0.1.dist-info/top_level.txt +1 -0
- odoo/addons/web_m2x_options_manager/readme/CONTRIBUTORS.rst +0 -4
- odoo/addons/web_m2x_options_manager/readme/USAGE.rst +0 -7
- odoo_addon_web_m2x_options_manager-16.0.1.0.0.3.dist-info/RECORD +0 -23
- odoo_addon_web_m2x_options_manager-16.0.1.0.0.3.dist-info/top_level.txt +0 -1
- /odoo/addons/web_m2x_options_manager/readme/{DESCRIPTION.rst → DESCRIPTION.md} +0 -0
|
@@ -8,35 +8,48 @@ from odoo.tools.safe_eval import safe_eval
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class M2xCreateEditOption(models.Model):
|
|
11
|
+
"""Technical model to define M2X option at single field level.
|
|
12
|
+
|
|
13
|
+
Each record is uniquely defined by its ``field_id``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
11
16
|
_name = "m2x.create.edit.option"
|
|
12
|
-
_description = "
|
|
17
|
+
_description = "Field 'Create & Edit' Options"
|
|
13
18
|
|
|
19
|
+
name = fields.Char(compute="_compute_name", store=True)
|
|
14
20
|
field_id = fields.Many2one(
|
|
15
21
|
"ir.model.fields",
|
|
16
|
-
domain=[("
|
|
22
|
+
domain=[("can_have_options", "=", True)],
|
|
17
23
|
ondelete="cascade",
|
|
18
24
|
required=True,
|
|
25
|
+
index=True,
|
|
19
26
|
string="Field",
|
|
20
27
|
)
|
|
21
|
-
|
|
22
28
|
field_name = fields.Char(
|
|
23
29
|
related="field_id.name",
|
|
24
30
|
store=True,
|
|
25
31
|
)
|
|
26
|
-
|
|
27
32
|
model_id = fields.Many2one(
|
|
28
33
|
"ir.model",
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
related="field_id.model_id",
|
|
35
|
+
store=True,
|
|
31
36
|
string="Model",
|
|
32
37
|
)
|
|
33
|
-
|
|
34
38
|
model_name = fields.Char(
|
|
35
|
-
|
|
36
|
-
inverse="_inverse_model_name",
|
|
39
|
+
related="field_id.model",
|
|
37
40
|
store=True,
|
|
38
41
|
)
|
|
39
|
-
|
|
42
|
+
comodel_id = fields.Many2one(
|
|
43
|
+
"ir.model",
|
|
44
|
+
related="field_id.comodel_id",
|
|
45
|
+
store=True,
|
|
46
|
+
string="Comodel",
|
|
47
|
+
)
|
|
48
|
+
comodel_name = fields.Char(
|
|
49
|
+
related="field_id.relation",
|
|
50
|
+
store=True,
|
|
51
|
+
string="Comodel Name",
|
|
52
|
+
)
|
|
40
53
|
option_create = fields.Selection(
|
|
41
54
|
[
|
|
42
55
|
("none", "Do nothing"),
|
|
@@ -55,7 +68,6 @@ class M2xCreateEditOption(models.Model):
|
|
|
55
68
|
required=True,
|
|
56
69
|
string="Create Option",
|
|
57
70
|
)
|
|
58
|
-
|
|
59
71
|
option_create_edit = fields.Selection(
|
|
60
72
|
[
|
|
61
73
|
("none", "Do nothing"),
|
|
@@ -75,87 +87,125 @@ class M2xCreateEditOption(models.Model):
|
|
|
75
87
|
string="Create & Edit Option",
|
|
76
88
|
)
|
|
77
89
|
|
|
78
|
-
option_create_edit_wizard = fields.Boolean(
|
|
79
|
-
default=True,
|
|
80
|
-
help="Defines behaviour for 'Create & Edit' Wizard\n"
|
|
81
|
-
"Set to False to prevent 'Create & Edit' Wizard to pop up",
|
|
82
|
-
string="Create & Edit Wizard",
|
|
83
|
-
)
|
|
84
|
-
|
|
85
90
|
_sql_constraints = [
|
|
86
91
|
(
|
|
87
|
-
"
|
|
88
|
-
"unique(field_id
|
|
89
|
-
"Options must be unique for each
|
|
92
|
+
"field_uniqueness",
|
|
93
|
+
"unique(field_id)",
|
|
94
|
+
"Options must be unique for each field!",
|
|
90
95
|
),
|
|
91
96
|
]
|
|
92
97
|
|
|
93
98
|
@api.model_create_multi
|
|
94
99
|
def create(self, vals_list):
|
|
95
|
-
# Clear cache to avoid misbehavior from cached
|
|
96
|
-
|
|
100
|
+
# Clear cache to avoid misbehavior from cached methods
|
|
101
|
+
self._clear_caches()
|
|
97
102
|
return super().create(vals_list)
|
|
98
103
|
|
|
99
104
|
def write(self, vals):
|
|
100
|
-
# Clear cache to avoid misbehavior from cached
|
|
101
|
-
|
|
105
|
+
# Clear cache to avoid misbehavior from cached methods
|
|
106
|
+
if set(vals).intersection(["field_id"] + self._get_option_fields()):
|
|
107
|
+
self._clear_caches()
|
|
102
108
|
return super().write(vals)
|
|
103
109
|
|
|
104
110
|
def unlink(self):
|
|
105
|
-
# Clear cache to avoid misbehavior from cached
|
|
106
|
-
|
|
111
|
+
# Clear cache to avoid misbehavior from cached methods
|
|
112
|
+
self._clear_caches()
|
|
107
113
|
return super().unlink()
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
for opt in self:
|
|
112
|
-
opt.model_name = opt.model_id.model
|
|
115
|
+
def _clear_caches(self, *cache_names):
|
|
116
|
+
"""Clear registry caches
|
|
113
117
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
By default, clears caches to avoid misbehavior from cached methods:
|
|
119
|
+
- ``m2x.create.edit.option._get_id()``
|
|
120
|
+
- ``ir.ui.view._get_view_cache()``
|
|
121
|
+
"""
|
|
122
|
+
self.env.registry.clear_cache(*self._clear_caches_get_names(*cache_names))
|
|
123
|
+
|
|
124
|
+
def _clear_caches_get_names(self, *cache_names) -> list[str]:
|
|
125
|
+
"""Retrieves registry caches names for clearance
|
|
121
126
|
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
By default, we want to clear caches:
|
|
128
|
+
- "default": where ``m2x.create.edit.option._get_id()`` results get stored
|
|
129
|
+
- "templates": where ``ir.ui.view._get_view_cache()`` results get stored
|
|
130
|
+
"""
|
|
131
|
+
return list(cache_names) + ["default", "templates"]
|
|
132
|
+
|
|
133
|
+
@api.depends("field_id")
|
|
134
|
+
def _compute_name(self):
|
|
124
135
|
for opt in self:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
model_name=opt.model_name,
|
|
130
|
-
)
|
|
131
|
-
raise ValidationError(msg)
|
|
136
|
+
try:
|
|
137
|
+
opt.name = str(self.env[opt.field_id.model]._fields[opt.field_id.name])
|
|
138
|
+
except KeyError:
|
|
139
|
+
opt.name = "Invalid field"
|
|
132
140
|
|
|
133
141
|
@api.constrains("field_id")
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
def _check_field_can_have_options(self):
|
|
143
|
+
for opt in self:
|
|
144
|
+
if opt.field_id and not opt.field_id.can_have_options:
|
|
145
|
+
raise ValidationError(
|
|
146
|
+
_(
|
|
147
|
+
"Field %(field)s cannot have M2X options",
|
|
148
|
+
field=opt.field_id.display_name,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
139
151
|
|
|
140
152
|
def _apply_options(self, node):
|
|
141
|
-
"""Applies options ``self`` to ``node``
|
|
153
|
+
"""Applies options ``self`` to ``node``
|
|
154
|
+
|
|
155
|
+
:param etree._Element node: view ``<field/>`` node to update
|
|
156
|
+
:rtype: None
|
|
157
|
+
"""
|
|
158
|
+
self.ensure_one()
|
|
159
|
+
node_options = self._read_node_options(node)
|
|
160
|
+
for key, (mode, value) in self._read_own_options().items():
|
|
161
|
+
if mode == "force" or key not in node_options:
|
|
162
|
+
node_options[key] = value
|
|
163
|
+
node.set("options", str(node_options))
|
|
164
|
+
|
|
165
|
+
def _read_node_options(self, node):
|
|
166
|
+
"""Helper method to read "options" attribute on ``node``
|
|
167
|
+
|
|
168
|
+
:param etree._Element node: view ``<field/>`` node to parse
|
|
169
|
+
:rtype: dict[str, Any]
|
|
170
|
+
"""
|
|
142
171
|
self.ensure_one()
|
|
143
172
|
options = node.attrib.get("options") or {}
|
|
144
173
|
if isinstance(options, str):
|
|
145
|
-
options = safe_eval(options,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
174
|
+
options = safe_eval(options, self._get_node_options_eval_context()) or {}
|
|
175
|
+
return dict(options)
|
|
176
|
+
|
|
177
|
+
def _get_node_options_eval_context(self):
|
|
178
|
+
"""Helper method to get eval context to read "options" attribute from a node
|
|
179
|
+
|
|
180
|
+
:rtype: dict
|
|
181
|
+
"""
|
|
182
|
+
self.ensure_one()
|
|
183
|
+
eval_ctx = dict(self.env.context or [])
|
|
184
|
+
eval_ctx.update({"context": dict(eval_ctx), "true": True, "false": False})
|
|
185
|
+
return eval_ctx
|
|
186
|
+
|
|
187
|
+
def _read_own_options(self):
|
|
188
|
+
"""Helper method to retrieve M2X options from ``self``
|
|
189
|
+
|
|
190
|
+
:return: a dictionary mapping each M2X option to its mode and value, eg:
|
|
191
|
+
{'create': ('force', 'true'), 'create_edit': ('set', 'false')}
|
|
192
|
+
:rtype: dict[str, tuple[str, Any]]
|
|
193
|
+
"""
|
|
194
|
+
self.ensure_one()
|
|
195
|
+
res = {}
|
|
196
|
+
for fname, fvalue in self.read(self._get_option_fields())[0].items():
|
|
197
|
+
if fname != "id" and fvalue != "none":
|
|
198
|
+
mode, value = tuple(fvalue.split("_"))
|
|
199
|
+
res[fname.replace("option_", "")] = (mode, value == "true")
|
|
200
|
+
return res
|
|
201
|
+
|
|
202
|
+
def _get_option_fields(self):
|
|
203
|
+
"""Helper method to retrieve field names to parse as M2X options
|
|
204
|
+
|
|
205
|
+
:return: list of field names to parse as M2X options
|
|
206
|
+
:rtype: list[str]
|
|
207
|
+
"""
|
|
208
|
+
return ["option_create", "option_create_edit"]
|
|
159
209
|
|
|
160
210
|
@api.model
|
|
161
211
|
def get(self, model_name, field_name):
|
|
@@ -163,22 +213,29 @@ class M2xCreateEditOption(models.Model):
|
|
|
163
213
|
|
|
164
214
|
:param str model_name: technical model name (i.e. "sale.order")
|
|
165
215
|
:param str field_name: technical field name (i.e. "partner_id")
|
|
216
|
+
:return: a ``m2x.create.edit.option`` record
|
|
217
|
+
:rtype: M2xCreateEditOption
|
|
166
218
|
"""
|
|
167
|
-
return self.browse(self.
|
|
219
|
+
return self.browse(self._get_id(model_name, field_name))
|
|
168
220
|
|
|
169
221
|
@api.model
|
|
170
|
-
@ormcache("model_name", "field_name")
|
|
171
|
-
def
|
|
222
|
+
@ormcache("model_name", "field_name", cache="default")
|
|
223
|
+
def _get_id(self, model_name, field_name):
|
|
172
224
|
"""Inner implementation of ``get``.
|
|
225
|
+
|
|
173
226
|
An ID is returned to allow caching (see :class:`ormcache`); :meth:`get`
|
|
174
227
|
will then convert it to a proper record.
|
|
175
228
|
|
|
176
229
|
:param str model_name: technical model name (i.e. "sale.order")
|
|
177
230
|
:param str field_name: technical field name (i.e. "partner_id")
|
|
231
|
+
:return: a ``m2x.create.edit.option`` record ID
|
|
232
|
+
:rtype: int
|
|
178
233
|
"""
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
234
|
+
opt_id = 0
|
|
235
|
+
field = self.env["ir.model.fields"]._get(model_name, field_name)
|
|
236
|
+
if field:
|
|
237
|
+
# SQL constraint grants record uniqueness (if existing)
|
|
238
|
+
opt = self.search([("field_id", "=", field.id)], limit=1)
|
|
239
|
+
if opt:
|
|
240
|
+
opt_id = opt.id
|
|
241
|
+
return opt_id
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Go to Settings > Technical > Models.
|
|
2
|
+
|
|
3
|
+
Choose the model you wish to edit, and open its form view. Go to the "Create/Edit Options" tab,
|
|
4
|
+
and add the fields you want to manage in 2 different sections:
|
|
5
|
+
|
|
6
|
+
* the first list view allows you to handle fields for the selected model
|
|
7
|
+
* the second list view allows you to handle fields where the selected model is the comodel
|
|
8
|
+
|
|
9
|
+
For both sections:
|
|
10
|
+
|
|
11
|
+
* button "Fill" will add every missing field to the options
|
|
12
|
+
* button "Empty" will remove every option
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8" ?>
|
|
2
1
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
3
2
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
4
3
|
<head>
|
|
5
4
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
6
|
-
<meta name="generator" content="Docutils:
|
|
7
|
-
<title>
|
|
5
|
+
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
|
6
|
+
<title>README.rst</title>
|
|
8
7
|
<style type="text/css">
|
|
9
8
|
|
|
10
9
|
/*
|
|
11
10
|
:Author: David Goodger (goodger@python.org)
|
|
12
|
-
:Id: $Id: html4css1.css
|
|
11
|
+
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
|
13
12
|
:Copyright: This stylesheet has been placed in the public domain.
|
|
14
13
|
|
|
15
14
|
Default cascading style sheet for the HTML output of Docutils.
|
|
15
|
+
Despite the name, some widely supported CSS2 features are used.
|
|
16
16
|
|
|
17
|
-
See
|
|
17
|
+
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
|
18
18
|
customize this style sheet.
|
|
19
19
|
*/
|
|
20
20
|
|
|
@@ -275,7 +275,7 @@ pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
|
|
275
275
|
margin-left: 2em ;
|
|
276
276
|
margin-right: 2em }
|
|
277
277
|
|
|
278
|
-
pre.code .ln { color:
|
|
278
|
+
pre.code .ln { color: gray; } /* line numbers */
|
|
279
279
|
pre.code, code { background-color: #eeeeee }
|
|
280
280
|
pre.code .comment, code .comment { color: #5C6576 }
|
|
281
281
|
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
|
@@ -301,7 +301,7 @@ span.option {
|
|
|
301
301
|
span.pre {
|
|
302
302
|
white-space: pre }
|
|
303
303
|
|
|
304
|
-
span.problematic {
|
|
304
|
+
span.problematic, pre.problematic {
|
|
305
305
|
color: red }
|
|
306
306
|
|
|
307
307
|
span.section-subtitle {
|
|
@@ -360,76 +360,93 @@ ul.auto-toc {
|
|
|
360
360
|
</style>
|
|
361
361
|
</head>
|
|
362
362
|
<body>
|
|
363
|
-
<div class="document"
|
|
364
|
-
<h1 class="title">Web M2X Options Manager</h1>
|
|
363
|
+
<div class="document">
|
|
365
364
|
|
|
365
|
+
|
|
366
|
+
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
|
|
367
|
+
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
|
|
368
|
+
</a>
|
|
369
|
+
<div class="section" id="web-m2x-options-manager">
|
|
370
|
+
<h1>Web M2X Options Manager</h1>
|
|
366
371
|
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
367
372
|
!! This file is generated by oca-gen-addon-readme !!
|
|
368
373
|
!! changes will be overwritten. !!
|
|
369
374
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
370
|
-
!! source digest: sha256:
|
|
375
|
+
!! source digest: sha256:16d18fbd77168c0ada84f3ae7987a1fc752238c88ee402749bb77bf71deefbda
|
|
371
376
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
|
372
|
-
<p><a class="reference external" 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" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/
|
|
373
|
-
<p>Allows managing the “Create…” and “Create and Edit…” options for
|
|
374
|
-
and <
|
|
377
|
+
<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/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/web/tree/17.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-17-0/web-17-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&target_branch=17.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
|
378
|
+
<p>Allows managing the “Create…” and “Create and Edit…” options for
|
|
379
|
+
<tt class="docutils literal">Many2one</tt> and <tt class="docutils literal">Many2many</tt> fields directly from the <tt class="docutils literal">ir.model</tt>
|
|
380
|
+
form view.</p>
|
|
375
381
|
<p><strong>Table of contents</strong></p>
|
|
376
382
|
<div class="contents local topic" id="contents">
|
|
377
383
|
<ul class="simple">
|
|
378
|
-
<li><a class="reference internal" href="#usage" id="
|
|
379
|
-
<li><a class="reference internal" href="#bug-tracker" id="
|
|
380
|
-
<li><a class="reference internal" href="#credits" id="
|
|
381
|
-
<li><a class="reference internal" href="#authors" id="
|
|
382
|
-
<li><a class="reference internal" href="#contributors" id="
|
|
383
|
-
<li><a class="reference internal" href="#maintainers" id="
|
|
384
|
+
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
|
|
385
|
+
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
|
|
386
|
+
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
|
|
387
|
+
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
|
|
388
|
+
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
|
|
389
|
+
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
|
|
384
390
|
</ul>
|
|
385
391
|
</li>
|
|
386
392
|
</ul>
|
|
387
393
|
</div>
|
|
388
394
|
<div class="section" id="usage">
|
|
389
|
-
<
|
|
395
|
+
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
|
|
390
396
|
<p>Go to Settings > Technical > Models.</p>
|
|
391
397
|
<p>Choose the model you wish to edit, and open its form view. Go to the
|
|
392
|
-
“Create/Edit Options” tab, and add the fields you want to manage
|
|
393
|
-
|
|
394
|
-
|
|
398
|
+
“Create/Edit Options” tab, and add the fields you want to manage in 2
|
|
399
|
+
different sections:</p>
|
|
400
|
+
<ul class="simple">
|
|
401
|
+
<li>the first list view allows you to handle fields for the selected model</li>
|
|
402
|
+
<li>the second list view allows you to handle fields where the selected
|
|
403
|
+
model is the comodel</li>
|
|
404
|
+
</ul>
|
|
405
|
+
<p>For both sections:</p>
|
|
406
|
+
<ul class="simple">
|
|
407
|
+
<li>button “Fill” will add every missing field to the options</li>
|
|
408
|
+
<li>button “Empty” will remove every option</li>
|
|
409
|
+
</ul>
|
|
395
410
|
</div>
|
|
396
411
|
<div class="section" id="bug-tracker">
|
|
397
|
-
<
|
|
412
|
+
<h2><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h2>
|
|
398
413
|
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>.
|
|
399
414
|
In case of trouble, please check there if your issue has already been reported.
|
|
400
415
|
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
|
401
|
-
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_m2x_options_manager%0Aversion:%
|
|
416
|
+
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_m2x_options_manager%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
|
402
417
|
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
|
403
418
|
</div>
|
|
404
419
|
<div class="section" id="credits">
|
|
405
|
-
<
|
|
420
|
+
<h2><a class="toc-backref" href="#toc-entry-3">Credits</a></h2>
|
|
406
421
|
<div class="section" id="authors">
|
|
407
|
-
<
|
|
422
|
+
<h3><a class="toc-backref" href="#toc-entry-4">Authors</a></h3>
|
|
408
423
|
<ul class="simple">
|
|
409
424
|
<li>Camptocamp</li>
|
|
410
425
|
</ul>
|
|
411
426
|
</div>
|
|
412
427
|
<div class="section" id="contributors">
|
|
413
|
-
<
|
|
428
|
+
<h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
|
|
414
429
|
<ul class="simple">
|
|
415
430
|
<li><a class="reference external" href="https://www.camptocamp.com">Camptocamp</a>:<ul>
|
|
416
431
|
<li>Silvio Gregorini</li>
|
|
417
432
|
</ul>
|
|
418
433
|
</li>
|
|
419
|
-
<li>Duong (Tran Quoc) <<a class="reference external" href="mailto:duongtq@trobz.com">duongtq@trobz.com</a>></li>
|
|
420
434
|
</ul>
|
|
421
435
|
</div>
|
|
422
436
|
<div class="section" id="maintainers">
|
|
423
|
-
<
|
|
437
|
+
<h3><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h3>
|
|
424
438
|
<p>This module is maintained by the OCA.</p>
|
|
425
|
-
<a class="reference external image-reference" href="https://odoo-community.org"
|
|
439
|
+
<a class="reference external image-reference" href="https://odoo-community.org">
|
|
440
|
+
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
|
441
|
+
</a>
|
|
426
442
|
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
|
427
443
|
mission is to support the collaborative development of Odoo features and
|
|
428
444
|
promote its widespread use.</p>
|
|
429
|
-
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/
|
|
445
|
+
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/17.0/web_m2x_options_manager">OCA/web</a> project on GitHub.</p>
|
|
430
446
|
<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>
|
|
431
447
|
</div>
|
|
432
448
|
</div>
|
|
433
449
|
</div>
|
|
450
|
+
</div>
|
|
434
451
|
</body>
|
|
435
452
|
</html>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Copyright 2025 Camptocamp SA
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from lxml import etree
|
|
5
|
+
|
|
6
|
+
from odoo.tests.common import TransactionCase
|
|
7
|
+
from odoo.tools.safe_eval import safe_eval
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Common(TransactionCase):
|
|
11
|
+
@classmethod
|
|
12
|
+
def _create_opt(cls, model_name, field_name, vals=None):
|
|
13
|
+
field = cls._get_field(model_name, field_name)
|
|
14
|
+
vals = dict(vals or [])
|
|
15
|
+
return cls.env["m2x.create.edit.option"].create(dict(field_id=field.id, **vals))
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def _get_field(cls, model_name, field_name):
|
|
19
|
+
return cls.env["ir.model.fields"]._get(model_name, field_name)
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def _get_model(cls, model_name):
|
|
23
|
+
return cls.env["ir.model"]._get(model_name)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def _eval_node_options(cls, node):
|
|
27
|
+
opt = node.attrib.get("options") or {}
|
|
28
|
+
if isinstance(opt, str):
|
|
29
|
+
return safe_eval(opt, cls._get_node_options_eval_context(), nocopy=True)
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def _get_node_options_eval_context(cls):
|
|
34
|
+
eval_ctx = dict(cls.env.context or [])
|
|
35
|
+
eval_ctx.update({"context": dict(eval_ctx), "true": True, "false": False})
|
|
36
|
+
return eval_ctx
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def _get_test_view(cls):
|
|
40
|
+
return cls.env.ref("web_m2x_options_manager.res_partner_demo_form_view")
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def _get_test_view_fields_view_get(cls):
|
|
44
|
+
return cls.env["res.partner"].get_view(cls._get_test_view().id)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def _get_test_view_parsed(cls):
|
|
48
|
+
return etree.XML(cls._get_test_view_fields_view_get()["arch"])
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright 2025 Camptocamp SA
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from odoo.tools import mute_logger
|
|
5
|
+
|
|
6
|
+
from .common import Common
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestIrModel(Common):
|
|
10
|
+
@mute_logger("odoo.models.unlink")
|
|
11
|
+
def test_model_buttons(self):
|
|
12
|
+
model = self._get_model("res.users")
|
|
13
|
+
(model.m2x_option_ids + model.m2x_comodels_option_ids).unlink()
|
|
14
|
+
|
|
15
|
+
# Model's fields workflow
|
|
16
|
+
# 1- fill: check options have been created
|
|
17
|
+
model.button_fill_m2x_options()
|
|
18
|
+
options = model.m2x_option_ids
|
|
19
|
+
self.assertTrue(options)
|
|
20
|
+
# 2- refill: check no option has been created (they all existed already)
|
|
21
|
+
model.button_fill_m2x_options()
|
|
22
|
+
self.assertFalse(model.m2x_option_ids - options)
|
|
23
|
+
# 3- empty: check no option exists anymore
|
|
24
|
+
model.button_empty_m2x_options()
|
|
25
|
+
self.assertFalse(model.m2x_option_ids)
|
|
26
|
+
|
|
27
|
+
# Model's inverse fields workflow
|
|
28
|
+
# 1- fill: check options have been created
|
|
29
|
+
model.button_fill_m2x_comodels_options()
|
|
30
|
+
comodels_options = model.m2x_comodels_option_ids
|
|
31
|
+
self.assertTrue(comodels_options)
|
|
32
|
+
# 2- refill: check no option has been created (they all existed already)
|
|
33
|
+
model.button_fill_m2x_comodels_options()
|
|
34
|
+
self.assertFalse(model.m2x_comodels_option_ids - comodels_options)
|
|
35
|
+
# 3- empty: check no option exists anymore
|
|
36
|
+
model.button_empty_m2x_comodels_options()
|
|
37
|
+
self.assertFalse(model.m2x_comodels_option_ids)
|