# -*- coding: utf-8 -*- """ Test for the pseudo-form implementation (odoo.tests.common.Form), which should basically be a server-side implementation of form views (though probably not complete) intended for properly validating business "view" flows (onchanges, readonly, required, ...) and make it easier to generate sensible & coherent business objects. """ from operator import itemgetter from odoo.tests.common import TransactionCase, Form class TestBasic(TransactionCase): def test_defaults(self): """ Checks that we can load a default form view and perform trivial default_get & onchanges & computations """ f = Form(self.env['test_testing_utilities.a']) self.assertEqual(f.id, False, "check that our record is not in db (yet)") self.assertEqual(f.f2, 42) self.assertEqual(f.f3, 21) self.assertEqual(f.f4, 42) f.f1 = '4' self.assertEqual(f.f2, 42) self.assertEqual(f.f3, 21) self.assertEqual(f.f4, 10) f.f2 = 8 self.assertEqual(f.f3, 4) self.assertEqual(f.f4, 2) r = f.save() self.assertEqual( (r.f1, r.f2, r.f3, r.f4), ('4', 8, 4, 2), ) def test_required(self): f = Form(self.env['test_testing_utilities.a']) # f1 no default & no value => should fail with self.assertRaisesRegexp(AssertionError, 'f1 is a required field'): f.save() # set f1 and unset f2 => should work f.f1 = '1' f.f2 = False r = f.save() self.assertEqual( (r.f1, r.f2, r.f3, r.f4), ('1', 0, 0, 0) ) def test_required_bool(self): f = Form(self.env['test_testing_utilities.req_bool']) f.f_bool = False r = f.save() self.assertEqual(r.f_bool, 0) f2 = Form(self.env['test_testing_utilities.req_bool']) r2 = f2.save() self.assertEqual(r2.f_bool, 0) def test_readonly(self): """ Checks that fields with readonly modifiers (marked as readonly or computed w/o set) raise an error when set. """ f = Form(self.env['test_testing_utilities.readonly']) with self.assertRaises(AssertionError): f.f1 = '5' 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 """ f = Form(self.env['test_testing_utilities.a'], view='test_testing_utilities.non_normalized_attrs') # not readonly yet, should work f.f2 = 5 # make f2 readonly f.f1 = '63' f.f3 = 5 with self.assertRaises(AssertionError): f.f2 = 6 class TestM2O(TransactionCase): def test_default_and_onchange(self): """ Checks defaults & onchanges impacting m2o fields """ Sub = self.env['test_testing_utilities.m2o'] a = Sub.create({'name': "A"}) b = Sub.create({'name': "B"}) f = Form(self.env['test_testing_utilities.d']) self.assertFalse( f.f, "The default value gets overridden by the onchange" ) f.f2 = "B" self.assertEqual( f.f, b, "The new m2o value should match the second field by name" ) f.save() def test_set(self): """ Checks that we get/set recordsets for m2o & that set correctly triggers onchange """ r1 = self.env['test_testing_utilities.m2o'].create({'name': "A"}) r2 = self.env['test_testing_utilities.m2o'].create({'name': "B"}) f = Form(self.env['test_testing_utilities.c']) # check that basic manipulations work f.f2 = r1 self.assertEqual(f.f2, r1) self.assertEqual(f.name, 'A') f.f2 = r2 self.assertEqual(f.name, 'B') # can't set an int to an m2o field with self.assertRaises(AssertionError): f.f2 = r1.id self.assertEqual(f.f2, r2) self.assertEqual(f.name, 'B') # can't set a record of the wrong model temp = self.env['test_testing_utilities.readonly'].create({}) with self.assertRaises(AssertionError): f.f2 = temp self.assertEqual(f.f2, r2) self.assertEqual(f.name, 'B') r = f.save() self.assertEqual(r.f2, r2) class TestM2M(TransactionCase): def test_add(self): Sub = self.env['test_testing_utilities.sub2'] f = Form(self.env['test_testing_utilities.e']) r1 = Sub.create({'name': "Item"}) r2 = Sub.create({'name': "Item2"}) f.m2m.add(r1) f.m2m.add(r2) r = f.save() self.assertEqual( r.m2m, r1 | r2 ) def test_remove_by_index(self): Sub = self.env['test_testing_utilities.sub2'] f = Form(self.env['test_testing_utilities.e']) r1 = Sub.create({'name': "Item"}) r2 = Sub.create({'name': "Item2"}) f.m2m.add(r1) f.m2m.add(r2) f.m2m.remove(index=0) r = f.save() self.assertEqual( r.m2m, r2 ) def test_remove_by_id(self): Sub = self.env['test_testing_utilities.sub2'] f = Form(self.env['test_testing_utilities.e']) r1 = Sub.create({'name': "Item"}) r2 = Sub.create({'name': "Item2"}) f.m2m.add(r1) f.m2m.add(r2) f.m2m.remove(id=r1.id) r = f.save() self.assertEqual( r.m2m, r2 ) def test_on_m2m_change(self): Sub = self.env['test_testing_utilities.sub2'] f = Form(self.env['test_testing_utilities.e']) self.assertEqual(f.count, 0) f.m2m.add(Sub.create({'name': 'a'})) self.assertEqual(f.count, 1) f.m2m.add(Sub.create({'name': 'a'})) f.m2m.add(Sub.create({'name': 'a'})) f.m2m.add(Sub.create({'name': 'a'})) self.assertEqual(f.count, 4) f.m2m.remove(index=0) f.m2m.remove(index=0) f.m2m.remove(index=0) self.assertEqual(f.count, 1) def test_m2m_changed(self): Sub = self.env['test_testing_utilities.sub2'] a = Sub.create({'name': 'a'}) b = Sub.create({'name': 'b'}) c = Sub.create({'name': 'c'}) d = Sub.create({'name': 'd'}) f = Form(self.env['test_testing_utilities.f']) # check default_get self.assertEqual(f.m2m[:], a | b) f.m2o = c self.assertEqual(f.m2m[:], a | b | c) f.m2o = d self.assertEqual(f.m2m[:], a | b | c | d) def test_m2m_readonly(self): Sub = self.env['test_testing_utilities.sub3'] a = Sub.create({'name': 'a'}) b = Sub.create({'name': 'b'}) r = self.env['test_testing_utilities.g'].create({ 'm2m': [(6, 0, a.ids)] }) f = Form(r) with self.assertRaises(AssertionError): f.m2m.add(b) with self.assertRaises(AssertionError): f.m2m.remove(id=a.id) f.save() self.assertEqual(r.m2m, a) get = itemgetter('name', 'value', 'v') class TestO2M(TransactionCase): def test_basic_alterations(self): """ Tests that the o2m proxy allows adding, removing and editing o2m records """ f = Form(self.env['test_testing_utilities.parent'], view='test_testing_utilities.o2m_parent') f.subs.new().save() f.subs.new().save() f.subs.new().save() f.subs.remove(index=0) r = f.save() self.assertEqual( [get(s) for s in r.subs], [("2", 2, 2), ("2", 2, 2)] ) self.assertEqual(r.v, 5) with Form(r, view='test_testing_utilities.o2m_parent') as f: with f.subs.new() as sub: sub.value = 5 f.subs.new().save() with f.subs.edit(index=2) as sub: self.assertEqual(sub.v, 5) f.subs.remove(index=0) self.assertEqual( [get(s) for s in r.subs], [("2", 2, 2), ("5", 5, 5), ("2", 2, 2)] ) self.assertEqual(r.v, 10) with Form(r, view='test_testing_utilities.o2m_parent') as f, \ f.subs.edit(index=0) as sub,\ self.assertRaises(AssertionError): sub.name = "whop whop" def test_o2m_editable_list(self): """ Tests the o2m proxy when the list view is editable rather than delegating to a separate form view """ f = Form(self.env['test_testing_utilities.parent'], view='test_testing_utilities.o2m_parent_ed') custom_tree = self.env.ref('test_testing_utilities.editable_external').id subs_field = f._view['fields']['subs'] tree_view = subs_field['views']['tree'] self.assertEqual(tree_view['type'], 'tree') self.assertEqual( tree_view['view_id'], custom_tree, 'check that the tree view is the one referenced by tree_view_ref' ) self.assertIs(subs_field['views']['edition'], tree_view, "check that the edition view is the tree view") self.assertEqual( subs_field['views']['edition']['view_id'], custom_tree ) with f.subs.new() as s: s.value = 1 with f.subs.new() as s: s.value = 3 with f.subs.new() as s: s.value = 7 r = f.save() self.assertEqual(r.v, 12) self.assertEqual( [get(s) for s in r.subs], [('1', 1, 1), ('3', 3, 3), ('7', 7, 7)] ) def test_o2m_inline(self): """ Tests the o2m proxy when the list and form views are provided inline rather than fetched separately """ f = Form(self.env['test_testing_utilities.parent'], view='test_testing_utilities.o2m_parent_inline') with f.subs.new() as s: s.value = 42 r = f.save() self.assertEqual( [get(s) for s in r.subs], [("0", 42, 0)], "should not have set v (and thus not name)" ) def test_o2m_default(self): """ Tests that default_get can return defaults for the o2m """ f = Form(self.env['test_testing_utilities.default']) with f.subs.edit(index=0) as s: self.assertEqual(s.v, 5) self.assertEqual(s.value, False) r = f.save() self.assertEqual( [get(s) for s in r.subs], [("5", 0, 5)] ) def test_o2m_inner_default(self): """ Tests that creating an o2m record will get defaults for it """ f = Form(self.env['test_testing_utilities.default']) with f.subs.new() as s: self.assertEqual(s.value, 2) self.assertEqual(s.v, 2, "should have onchanged value to v") def test_o2m_onchange_parent(self): """ Tests that changing o2m content triggers onchange in the parent """ f = Form(self.env['test_testing_utilities.parent']) self.assertEqual(f.value, 1, "value should have its default") self.assertEqual(f.v, 1, "v should be equal to value") f.subs.new().save() self.assertEqual(f.v, 3, "should be sum of value & children v") def test_o2m_onchange_inner(self): """ Tests that editing a field of an o2m record triggers onchange in the o2m record and its parent """ f = Form(self.env['test_testing_utilities.parent']) # only apply the onchange on edition end (?) with f.subs.new() as sub: sub.value = 6 self.assertEqual(sub.v, 6) self.assertEqual(f.v, 1) self.assertEqual(f.v, 7) def test_o2m_parent_content(self): """ Tests that when editing a field of an o2m the data sent contains the parent data """ f = Form(self.env['test_testing_utilities.parent']) # only apply the onchange on edition end (?) with f.subs.new() as sub: sub.has_parent = True self.assertEqual(sub.has_parent, True) self.assertEqual(sub.value, 1) self.assertEqual(sub.v, 1) def test_readonly_o2m(self): """ Tests that o2m fields flagged as readonly (readonly="1" in the view) can't be written to """ r = self.env['test_testing_utilities.parent'].create({ 'subs': [(0, 0, {})] }) f = Form(r, view='test_testing_utilities.o2m_parent_readonly') with self.assertRaises(AssertionError): f.subs.new() with self.assertRaises(AssertionError): f.subs.edit(index=0) with self.assertRaises(AssertionError): f.subs.remove(index=0) def test_o2m_readonly_subfield(self): """ Tests that readonly is applied to the field of the o2m = not sent as part of the create / write values """ f = Form(self.env['o2m_readonly_subfield_parent']) with f.line_ids.new() as new_line: new_line.name = "ok" self.assertEqual(new_line.f, 2) r = f.save() self.assertEqual( (r.line_ids.name, r.line_ids.f), ('ok', 2) ) def test_o2m_dyn_onchange(self): f = Form(self.env['test_testing_utilities.onchange_parent'], view='test_testing_utilities.m2o_onchange_view') with f.line_ids.new() as new_line: new_line.dummy = 42 self.assertTrue(new_line.flag) with f.line_ids.edit(index=0) as new_line: self.assertTrue(new_line.flag) def test_o2m_remove(self): def commands(): return [c[0] for c in f._values['line_ids']] f = Form(self.env['test_testing_utilities.onchange_count']) self.assertEqual(f.count, 0) self.assertEqual(len(f.line_ids), 0) f.count = 5 self.assertEqual(f.count, 5) self.assertEqual(len(f.line_ids), 5) f.count = 2 self.assertEqual(f.count, 2) self.assertEqual(len(f.line_ids), 2) f.count = 4 r = f.save() previous = r.line_ids self.assertEqual(len(previous), 4) with Form(r) as f: f.count = 2 self.assertEqual(commands(), [0, 0, 2, 2, 2, 2], "Should contain 2 creations and 4 deletions") self.assertEqual(len(r.line_ids), 2) with Form(r) as f: f.line_ids.remove(0) self.assertEqual(commands(), [2, 1]) f.count = 1 self.assertEqual(commands(), [0, 2, 2], "should contain 1 '0' command and 2 deletions") self.assertEqual(len(r.line_ids), 1) def test_o2m_self_recursive(self): Form(self.env['test_testing_utilities.recursive'], view='test_testing_utilities.o2m_recursive_relation_view') class TestEdition(TransactionCase): """ These use the context manager form as we don't need the record post-save (we already have it) and it's easier to see what bits act on the form (inside `with`) versus outside. That let me catch a few mistakes. """ def test_trivial(self): r = self.env['test_testing_utilities.a'].create({ 'f1': '5', }) with Form(r) as f: self.assertEqual(f.id, r.id) self.assertEqual(f.f1, '5') self.assertEqual(f.f4, 8) f.f2 = 5 self.assertEqual(f.f3, 2) self.assertEqual(f.f4, 1) self.assertEqual(r.f2, 5) self.assertEqual(r.f3, 2) def test_m2o(self): Sub = self.env['test_testing_utilities.m2o'] a = Sub.create({'name': 'a'}) b = Sub.create({'name': 'b'}) c = Sub.create({'name': 'c'}) r = self.env['test_testing_utilities.d'].create({ 'f': c.id, 'f2': "OK", }) with Form(r) as f: # no default/onchange should have run so loading an incoherent # record should still be incoherent self.assertEqual(f.f, c) self.assertEqual(f.f2, 'OK') f.f2 = "b" self.assertEqual(f.f, b) f.f2 = "Whoops" self.assertEqual(f.f, Sub) f.f2 = "a" self.assertEqual(f.f, a) self.assertEqual(r.f2, "a") self.assertEqual(r.f, a) def test_m2m_empty(self): sub = self.env['test_testing_utilities.sub2'].create({'name': 'a'}) r = self.env['test_testing_utilities.f'].create({ 'm2m': [] }) with Form(r) as f: f.m2o = sub self.assertEqual(r.m2o, sub) self.assertEqual(r.m2m, sub) def test_m2m_nonempty(self): Sub = self.env['test_testing_utilities.sub2'] a = Sub.create({'name': 'a'}) b = Sub.create({'name': 'b'}) c = Sub.create({'name': 'c'}) r = self.env['test_testing_utilities.f'].create({ 'm2m': [(6, 0, (a | b | c).ids)] }) with Form(r) as f: self.assertEqual(f.m2m[:], (a | b | c)) f.m2m.remove(index=0) self.assertEqual(f.m2m[:], (b | c)) self.assertEqual(r.m2m, (b | c))