Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[16.0][ADD] server_action_mass_edit: add support for o2m fields #711

Merged
merged 1 commit into from
Jan 28, 2024
Merged
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
4 changes: 4 additions & 0 deletions server_action_mass_edit/demo/mass_editing.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
<field name="field_id" ref="base.field_res_users__image_1920" />
<field name="widget_option">image</field>
</record>
<record id="mass_editing_user_line_11" model="ir.actions.server.mass.edit.line">
<field name="server_action_id" ref="mass_editing_user" />
<field name="field_id" ref="base.field_res_users__bank_ids" />
</record>

<!-- Mass Edit Partner Title -->
<record id="mass_editing_partner_title" model="ir.actions.server">
Expand Down
49 changes: 49 additions & 0 deletions server_action_mass_edit/tests/test_mass_editing.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,25 @@ def test_wiz_fields_view_get(self):
"Fields view get must return architecture with fields" "created dynamicaly",
)

# test the code path where we extract an embedded tree for o2m fields
self.env["ir.ui.view"].search(
[
("model", "in", ("res.partner.bank", "res.partner", "res.users")),
("id", "!=", self.env.ref("base.res_partner_view_form_private").id),
]
).unlink()
self.env.ref("base.res_partner_view_form_private").model = "res.users"
result = self.MassEditingWizard.with_context(
server_action_id=self.mass_editing_user.id,
active_ids=[],
).get_view()
arch = result.get("arch", "")
self.assertIn(
"<tree editable=",
arch,
"Fields view get must return architecture with embedded tree",
)

def test_wzd_clean_check_company_field_domain(self):
"""
Test company field domain replacement
Expand Down Expand Up @@ -262,6 +281,36 @@ def test_mass_edit_email(self):
self._create_wizard_and_apply_values(self.mass_editing_user, self.user, vals)
self.assertNotEqual(self.user.email, False, "User's Email should be set.")

def test_mass_edit_o2m_banks(self):
"""Test Case for MASS EDITING which will remove and add
Partner's bank o2m."""
# Set another bank (must replace existing one)
bank_vals = {"acc_number": "account number"}
self.user.write(
{
"bank_ids": [(6, 0, []), (0, 0, bank_vals)],
}
)
vals = {
"selection__bank_ids": "set_o2m",
"bank_ids": [(0, 0, dict(bank_vals, acc_number="new number"))],
}
self._create_wizard_and_apply_values(self.mass_editing_user, self.user, vals)
self.assertEqual(self.user.bank_ids.acc_number, "new number")
# Add bank (must keep existing one)
vals = {
"selection__bank_ids": "add_o2m",
"bank_ids": [(0, 0, dict(bank_vals, acc_number="new number2"))],
}
self._create_wizard_and_apply_values(self.mass_editing_user, self.user, vals)
self.assertEqual(
self.user.bank_ids.mapped("acc_number"), ["new number", "new number2"]
)
# Set empty list (must remove all banks)
vals = {"selection__bank_ids": "set_o2m"}
self._create_wizard_and_apply_values(self.mass_editing_user, self.user, vals)
self.assertFalse(self.user.bank_ids)

def test_mass_edit_m2m_categ(self):
"""Test Case for MASS EDITING which will remove and add
Partner's category m2m."""
Expand Down
60 changes: 55 additions & 5 deletions server_action_mass_edit/wizard/mass_editing_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
# Copyright (C) 2020 Iván Todorovich (https://twitter.com/ivantodorovich)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import json

from lxml import etree

from odoo import _, api, fields, models

from odoo.addons.base.models.ir_ui_view import (
transfer_modifiers_to_node,
transfer_node_to_modifiers,
)


class MassEditingWizard(models.TransientModel):
_name = "mass.editing.wizard"
Expand Down Expand Up @@ -99,6 +106,12 @@ def _prepare_fields(self, line, field, field_info):
("remove_m2m", _("Remove")),
("add", _("Add")),
]
if field.ttype == "one2many":
selection = [
("ignore", _("Don't touch")),
("set_o2m", _("Set")),
("add_o2m", _("Add")),
]
else:
selection = [
("ignore", _("Don't touch")),
Expand Down Expand Up @@ -142,7 +155,32 @@ def _insert_field_in_arch(self, line, field, main_xml_group):
field_vals = self._get_field_options(field)
if line.widget_option:
field_vals["widget"] = line.widget_option
etree.SubElement(div, "field", field_vals)
field_element = etree.SubElement(div, "field", field_vals)
if field.ttype == "one2many":
comodel = self.env[field.relation]
dummy, form_view = comodel._get_view(view_type="form")
dummy, tree_view = comodel._get_view(view_type="tree")
field_context = {}
if form_view:
field_context["form_view_ref"] = form_view.xml_id
if tree_view:
field_context["tree_view_ref"] = tree_view.xml_id
if field_context:
field_element.attrib["context"] = json.dumps(field_context)
else:
model_arch, dummy = self.env[field.model]._get_view(view_type="form")
embedded_tree = None
for node in model_arch.xpath(
"//field[@name='%s'][./tree]" % field.name
):
embedded_tree = node.xpath("./tree")[0]
break
if embedded_tree is not None:
for node in embedded_tree.xpath("./*"):
modifiers = {}
transfer_node_to_modifiers(node, modifiers)
transfer_modifiers_to_node(modifiers, node)
field_element.insert(0, embedded_tree)

def _get_field_options(self, field):
return {
Expand All @@ -163,6 +201,11 @@ def get_view(self, view_id=None, view_type="form", **options):
main_xml_group = arch.find('.//group[@name="group_field_list"]')
for line in server_action.mapped("mass_edit_line_ids"):
self._insert_field_in_arch(line, line.field_id, main_xml_group)
if line.field_id.ttype == "one2many":
comodel = self.env[line.field_id.relation]
result["models"] = dict(
result["models"], **{comodel._name: tuple(comodel.fields_get())}
)
result["arch"] = etree.tostring(arch, encoding="unicode")
return result

Expand All @@ -179,6 +222,7 @@ def fields_get(self, allfields=None, attributes=None):
field_info = self._clean_check_company_field_domain(
self.env[server_action.model_id.model], field, fields_info[field.name]
)
field_info["relation_field"] = False
if not line.apply_domain and "domain" in field_info:
field_info["domain"] = "[]"
res.update(self._prepare_fields(line, field, field_info))
Expand Down Expand Up @@ -209,9 +253,14 @@ def create(self, vals_list):
for key, val in vals.items():
if key.startswith("selection_"):
split_key = key.split("__", 1)[1]
if val == "set":
if val == "set" or val == "add_o2m":
values.update({split_key: vals.get(split_key, False)})

elif val == "set_o2m":
values.update(
{split_key: [(6, 0, [])] + vals.get(split_key, [])}
)

elif val == "remove":
values.update({split_key: False})

Expand All @@ -230,10 +279,11 @@ def create(self, vals_list):
for m2m_id in vals.get(split_key, False)[0][2]:
m2m_list.append((4, m2m_id))
values.update({split_key: m2m_list})

if values:
self.env[server_action.model_id.model].browse(active_ids).write(
values
)
self.env[server_action.model_id.model].browse(
active_ids
).with_context(mass_edit=True,).write(values)
return super().create([{}])

def _prepare_create_values(self, vals_list):
Expand Down
Loading