diff --git a/odoo/addons/test_testing_utilities/menu.xml b/odoo/addons/test_testing_utilities/menu.xml
index fa5263efeca..99e409708c2 100644
--- a/odoo/addons/test_testing_utilities/menu.xml
+++ b/odoo/addons/test_testing_utilities/menu.xml
@@ -101,6 +101,8 @@
+
+
diff --git a/odoo/addons/test_testing_utilities/models.py b/odoo/addons/test_testing_utilities/models.py
index 68dc9172123..98a9fea7e7f 100644
--- a/odoo/addons/test_testing_utilities/models.py
+++ b/odoo/addons/test_testing_utilities/models.py
@@ -10,10 +10,14 @@ class A(models.Model):
f2 = fields.Integer(default=42)
f3 = fields.Integer()
f4 = fields.Integer(compute='_compute_f4')
+ f5 = fields.Integer()
+ f6 = fields.Integer()
@api.onchange('f2')
def _on_change_f2(self):
self.f3 = int(self.f2 / 2)
+ self.f5 = self.f2
+ self.f6 = self.f2
@api.depends('f1', 'f2')
def _compute_f4(self):
diff --git a/odoo/addons/test_testing_utilities/tests/test_form_impl.py b/odoo/addons/test_testing_utilities/tests/test_form_impl.py
index a14d6b93974..e2f0d0d89d3 100644
--- a/odoo/addons/test_testing_utilities/tests/test_form_impl.py
+++ b/odoo/addons/test_testing_utilities/tests/test_form_impl.py
@@ -65,6 +65,19 @@ class TestBasic(TransactionCase):
with self.assertRaises(AssertionError):
f.f2 = 42
+ def test_readonly_save(self):
+ """ Should not save readonly fields unless they're force_save
+ """
+ f = Form(self.env['test_testing_utilities.a'], view='test_testing_utilities.non_normalized_attrs')
+
+ f.f1 = 1
+ f.f2 = 987
+ self.assertEqual(f.f5, 987)
+ self.assertEqual(f.f6, 987)
+ r = f.save()
+ self.assertEqual(r.f5, 0)
+ self.assertEqual(r.f6, 987)
+
def test_attrs(self):
""" Checks that attrs/modifiers with non-normalized domains work
"""
diff --git a/odoo/tests/common.py b/odoo/tests/common.py
index 412634176d3..56b76e01c84 100644
--- a/odoo/tests/common.py
+++ b/odoo/tests/common.py
@@ -1098,7 +1098,7 @@ class Form(object):
else:
view_id = view or False
fvg = recordp.fields_view_get(view_id, 'form')
- arch = etree.fromstring(fvg['arch'])
+ fvg['tree'] = etree.fromstring(fvg['arch'])
object.__setattr__(self, '_view', fvg)
# TODO: make this less crappy?
@@ -1107,7 +1107,7 @@ class Form(object):
if descr['type'] != 'one2many':
continue
- node = next(n for n in arch.iter('field') if n.get('name') == f)
+ node = self._get_node(f)
default_view = next(
(m for m in node.get('mode', 'tree').split(',') if m != 'form'),
'tree'
@@ -1130,10 +1130,12 @@ class Form(object):
# if the default view is a kanban or a non-editable list, the
# "edition controller" is the form view
edition = views['form']
+ edition['tree'] = etree.fromstring(edition['arch'])
if default_view == 'tree':
subarch = etree.fromstring(views['tree']['arch'])
if subarch.get('editable'):
edition = views['tree']
+ edition['tree'] = subarch
self._process_fvg(submodel, edition)
descr['views']['edition'] = edition
@@ -1153,6 +1155,14 @@ class Form(object):
else:
self._init_from_defaults(self._model)
+ def _get_node(self, f):
+ """ Find etree node for the field ``f`` in the current arch
+ """
+ return next(
+ n for n in self._view['tree'].iter('field')
+ if n.get('name') == f
+ )
+
def __str__(self):
return "<%s %s(%s)>" % (
type(self).__name__,
@@ -1171,7 +1181,7 @@ class Form(object):
# pre-resolve modifiers & bind to arch toplevel
modifiers = fvg['modifiers'] = {}
contexts = fvg['contexts'] = {}
- for f in etree.fromstring(fvg['arch']).iter('field'):
+ for f in fvg['tree'].iter('field'):
fname = f.get('name')
modifiers[fname] = {
modifier: domain if isinstance(domain, bool) else normalize_domain(domain)
@@ -1356,8 +1366,11 @@ class Form(object):
# skip unmodified fields
if f not in self._changed:
continue
+
if self._get_modifier(f, 'readonly'):
- continue
+ node = self._get_node(f)
+ if not node.get('force_save'):
+ continue
# TODO: filter out (1, _, {}) from o2m values
values[f] = v
return values