# -*- coding: utf-8 -*- import openerp.modules.registry import openerp from openerp.tests import common from openerp.tools.misc import mute_logger def ok(n): """ Successful import of ``n`` records :param int n: number of records which should have been imported """ return n, 0, 0, 0 def error(row, message, record=None, **kwargs): """ Failed import of the record ``record`` at line ``row``, with the error message ``message`` :param str message: :param dict record: """ return ( -1, dict(record or {}, **kwargs), "Line %d : %s" % (row, message), '') def values(seq, field='value'): return [item[field] for item in seq] class ImporterCase(common.TransactionCase): model_name = False def __init__(self, *args, **kwargs): super(ImporterCase, self).__init__(*args, **kwargs) self.model = None def setUp(self): super(ImporterCase, self).setUp() self.model = self.registry(self.model_name) def import_(self, fields, rows, context=None): return self.model.import_data( self.cr, openerp.SUPERUSER_ID, fields, rows, context=context) def read(self, fields=('value',), domain=(), context=None): return self.model.read( self.cr, openerp.SUPERUSER_ID, self.model.search(self.cr, openerp.SUPERUSER_ID, domain, context=context), fields=fields, context=context) def browse(self, domain=(), context=None): return self.model.browse( self.cr, openerp.SUPERUSER_ID, self.model.search(self.cr, openerp.SUPERUSER_ID, domain, context=context), context=context) def xid(self, record): ModelData = self.registry('ir.model.data') ids = ModelData.search( self.cr, openerp.SUPERUSER_ID, [('model', '=', record._table_name), ('res_id', '=', record.id)]) if ids: d = ModelData.read( self.cr, openerp.SUPERUSER_ID, ids, ['name', 'module'])[0] if d['module']: return '%s.%s' % (d['module'], d['name']) return d['name'] name = dict(record.name_get())[record.id] # fix dotted name_get results, otherwise xid lookups blow up name = name.replace('.', '-') ModelData.create(self.cr, openerp.SUPERUSER_ID, { 'name': name, 'model': record._table_name, 'res_id': record.id, 'module': '__test__' }) return '__test__.' + name class test_ids_stuff(ImporterCase): model_name = 'export.integer' def test_create_with_id(self): self.assertEqual( self.import_(['.id', 'value'], [['42', '36']]), error(1, u"Unknown database identifier '42'")) def test_create_with_xid(self): self.assertEqual( self.import_(['id', 'value'], [['somexmlid', '42']]), ok(1)) self.assertEqual( 'somexmlid', self.xid(self.browse()[0])) def test_update_with_id(self): id = self.model.create(self.cr, openerp.SUPERUSER_ID, {'value': 36}) self.assertEqual( 36, self.model.browse(self.cr, openerp.SUPERUSER_ID, id).value) self.assertEqual( self.import_(['.id', 'value'], [[str(id), '42']]), ok(1)) self.assertEqual( [42], # updated value to imported values(self.read())) def test_update_with_xid(self): self.import_(['id', 'value'], [['somexmlid', '36']]) self.assertEqual([36], values(self.read())) self.import_(['id', 'value'], [['somexmlid', '1234567']]) self.assertEqual([1234567], values(self.read())) class test_boolean_field(ImporterCase): model_name = 'export.boolean' def test_empty(self): self.assertEqual( self.import_(['value'], []), ok(0)) def test_exported(self): self.assertEqual( self.import_(['value'], [ ['False'], ['True'], ]), ok(2)) records = self.read() self.assertEqual([ False, True, ], values(records)) def test_falses(self): self.assertEqual( self.import_(['value'], [ [u'0'], [u'no'], [u'false'], [u'FALSE'], [u''], ]), ok(5)) self.assertEqual([ False, False, False, False, False, ], values(self.read())) def test_trues(self): self.assertEqual( self.import_(['value'], [ ['off'], ['None'], ['nil'], ['()'], ['f'], ['#f'], # Problem: OpenOffice (and probably excel) output localized booleans ['VRAI'], [u'OFF'], ]), ok(8)) self.assertEqual( [True] * 8, values(self.read())) class test_integer_field(ImporterCase): model_name = 'export.integer' def test_none(self): self.assertEqual( self.import_(['value'], []), ok(0)) def test_empty(self): self.assertEqual( self.import_(['value'], [['']]), ok(1)) self.assertEqual( [False], values(self.read())) def test_zero(self): self.assertEqual( self.import_(['value'], [['0']]), ok(1)) self.assertEqual( self.import_(['value'], [['-0']]), ok(1)) self.assertEqual([False, False], values(self.read())) def test_positives(self): self.assertEqual( self.import_(['value'], [ ['1'], ['42'], [str(2**31-1)], ['12345678'] ]), ok(4)) self.assertEqual([ 1, 42, 2**31-1, 12345678 ], values(self.read())) def test_negatives(self): self.assertEqual( self.import_(['value'], [ ['-1'], ['-42'], [str(-(2**31 - 1))], [str(-(2**31))], ['-12345678'] ]), ok(5)) self.assertEqual([ -1, -42, -(2**31 - 1), -(2**31), -12345678 ], values(self.read())) @mute_logger('openerp.sql_db') def test_out_of_range(self): self.assertEqual( self.import_(['value'], [[str(2**31)]]), error(1, "integer out of range\n")) # auto-rollbacks if error is in process_liness, but not during # ir.model.data write. Can differentiate because former ends lines # error lines with "!" self.cr.rollback() self.assertEqual( self.import_(['value'], [[str(-2**32)]]), error(1, "integer out of range\n")) def test_nonsense(self): self.assertEqual( self.import_(['value'], [['zorglub']]), error(1, u"'zorglub' does not seem to be an integer for field 'unknown'")) class test_float_field(ImporterCase): model_name = 'export.float' def test_none(self): self.assertEqual( self.import_(['value'], []), ok(0)) def test_empty(self): self.assertEqual( self.import_(['value'], [['']]), ok(1)) self.assertEqual( [False], values(self.read())) def test_zero(self): self.assertEqual( self.import_(['value'], [['0']]), ok(1)) self.assertEqual( self.import_(['value'], [['-0']]), ok(1)) self.assertEqual([False, False], values(self.read())) def test_positives(self): self.assertEqual( self.import_(['value'], [ ['1'], ['42'], [str(2**31-1)], ['12345678'], [str(2**33)], ['0.000001'], ]), ok(6)) self.assertEqual([ 1, 42, 2**31-1, 12345678, 2.0**33, .000001 ], values(self.read())) def test_negatives(self): self.assertEqual( self.import_(['value'], [ ['-1'], ['-42'], [str(-2**31 + 1)], [str(-2**31)], ['-12345678'], [str(-2**33)], ['-0.000001'], ]), ok(7)) self.assertEqual([ -1, -42, -(2**31 - 1), -(2**31), -12345678, -2.0**33, -.000001 ], values(self.read())) def test_nonsense(self): self.assertEqual( self.import_(['value'], [['foobar']]), error(1, u"'foobar' does not seem to be a number for field 'unknown'")) class test_string_field(ImporterCase): model_name = 'export.string.bounded' def test_empty(self): self.assertEqual( self.import_(['value'], [['']]), ok(1)) self.assertEqual([False], values(self.read())) def test_imported(self): self.assertEqual( self.import_(['value'], [ [u'foobar'], [u'foobarbaz'], [u'Með suð í eyrum við spilum endalaust'], [u"People 'get' types. They use them all the time. Telling " u"someone he can't pound a nail with a banana doesn't much " u"surprise him."] ]), ok(4)) self.assertEqual([ u"foobar", u"foobarbaz", u"Með suð í eyrum ", u"People 'get' typ", ], values(self.read())) class test_unbound_string_field(ImporterCase): model_name = 'export.string' def test_imported(self): self.assertEqual( self.import_(['value'], [ [u'í dag viðrar vel til loftárása'], # ackbar.jpg [u"If they ask you about fun, you tell them – fun is a filthy" u" parasite"] ]), ok(2)) self.assertEqual([ u"í dag viðrar vel til loftárása", u"If they ask you about fun, you tell them – fun is a filthy parasite" ], values(self.read())) class test_text(ImporterCase): model_name = 'export.text' def test_empty(self): self.assertEqual( self.import_(['value'], [['']]), ok(1)) self.assertEqual([False], values(self.read())) def test_imported(self): s = (u"Breiðskífa er notað um útgefna hljómplötu sem inniheldur " u"stúdíóupptökur frá einum flytjanda. Breiðskífur eru oftast " u"milli 25-80 mínútur og er lengd þeirra oft miðuð við 33⅓ " u"snúninga 12 tommu vínylplötur (sem geta verið allt að 30 mín " u"hvor hlið).\n\nBreiðskífur eru stundum tvöfaldar og eru þær þá" u" gefnar út á tveimur geisladiskum eða tveimur vínylplötum.") self.assertEqual( self.import_(['value'], [[s]]), ok(1)) self.assertEqual([s], values(self.read())) class test_selection(ImporterCase): model_name = 'export.selection' translations_fr = [ ("Qux", "toto"), ("Bar", "titi"), ("Foo", "tete"), ] def test_imported(self): self.assertEqual( self.import_(['value'], [ ['Qux'], ['Bar'], ['Foo'], ['2'], ]), ok(4)) self.assertEqual([3, 2, 1, 2], values(self.read())) def test_imported_translated(self): self.registry('res.lang').create(self.cr, openerp.SUPERUSER_ID, { 'name': u'Français', 'code': 'fr_FR', 'translatable': True, 'date_format': '%d.%m.%Y', 'decimal_point': ',', 'thousands_sep': ' ', }) Translations = self.registry('ir.translation') for source, value in self.translations_fr: Translations.create(self.cr, openerp.SUPERUSER_ID, { 'name': 'export.selection,value', 'lang': 'fr_FR', 'type': 'selection', 'src': source, 'value': value }) self.assertEqual( self.import_(['value'], [ ['toto'], ['tete'], ['titi'], ], context={'lang': 'fr_FR'}), ok(3)) self.assertEqual([3, 1, 2], values(self.read())) self.assertEqual( self.import_(['value'], [['Foo']], context={'lang': 'fr_FR'}), ok(1)) def test_invalid(self): self.assertEqual( self.import_(['value'], [['Baz']]), error(1, u"Value 'Baz' not found in selection field 'unknown'")) self.cr.rollback() self.assertEqual( self.import_(['value'], [[42]]), error(1, u"Value '42' not found in selection field 'unknown'")) class test_selection_function(ImporterCase): model_name = 'export.selection.function' translations_fr = [ ("Corge", "toto"), ("Grault", "titi"), ("Wheee", "tete"), ("Moog", "tutu"), ] def test_imported(self): """ import uses fields_get, so translates import label (may or may not be good news) *and* serializes the selection function to reverse it: import does not actually know that the selection field uses a function """ # NOTE: conflict between a value and a label => ? self.assertEqual( self.import_(['value'], [ ['3'], ["Grault"], ]), ok(2)) self.assertEqual( ['3', '1'], values(self.read())) def test_translated(self): """ Expects output of selection function returns translated labels """ self.registry('res.lang').create(self.cr, openerp.SUPERUSER_ID, { 'name': u'Français', 'code': 'fr_FR', 'translatable': True, 'date_format': '%d.%m.%Y', 'decimal_point': ',', 'thousands_sep': ' ', }) Translations = self.registry('ir.translation') for source, value in self.translations_fr: Translations.create(self.cr, openerp.SUPERUSER_ID, { 'name': 'export.selection,value', 'lang': 'fr_FR', 'type': 'selection', 'src': source, 'value': value }) self.assertEqual( self.import_(['value'], [ ['toto'], ['tete'], ], context={'lang': 'fr_FR'}), ok(2)) self.assertEqual( self.import_(['value'], [['Wheee']], context={'lang': 'fr_FR'}), ok(1)) class test_m2o(ImporterCase): model_name = 'export.many2one' def test_by_name(self): # create integer objects integer_id1 = self.registry('export.integer').create( self.cr, openerp.SUPERUSER_ID, {'value': 42}) integer_id2 = self.registry('export.integer').create( self.cr, openerp.SUPERUSER_ID, {'value': 36}) # get its name name1 = dict(self.registry('export.integer').name_get( self.cr, openerp.SUPERUSER_ID,[integer_id1]))[integer_id1] name2 = dict(self.registry('export.integer').name_get( self.cr, openerp.SUPERUSER_ID,[integer_id2]))[integer_id2] self.assertEqual( self.import_(['value'], [ # import by name_get [name1], [name1], [name2], ]), ok(3)) # correct ids assigned to corresponding records self.assertEqual([ (integer_id1, name1), (integer_id1, name1), (integer_id2, name2),], values(self.read())) def test_by_xid(self): ExportInteger = self.registry('export.integer') integer_id = ExportInteger.create( self.cr, openerp.SUPERUSER_ID, {'value': 42}) xid = self.xid(ExportInteger.browse( self.cr, openerp.SUPERUSER_ID, [integer_id])[0]) self.assertEqual( self.import_(['value/id'], [[xid]]), ok(1)) b = self.browse() self.assertEqual(42, b[0].value.value) def test_by_id(self): integer_id = self.registry('export.integer').create( self.cr, openerp.SUPERUSER_ID, {'value': 42}) self.assertEqual( self.import_(['value/.id'], [[integer_id]]), ok(1)) b = self.browse() self.assertEqual(42, b[0].value.value) def test_by_names(self): integer_id1 = self.registry('export.integer').create( self.cr, openerp.SUPERUSER_ID, {'value': 42}) integer_id2 = self.registry('export.integer').create( self.cr, openerp.SUPERUSER_ID, {'value': 42}) name1 = dict(self.registry('export.integer').name_get( self.cr, openerp.SUPERUSER_ID,[integer_id1]))[integer_id1] name2 = dict(self.registry('export.integer').name_get( self.cr, openerp.SUPERUSER_ID,[integer_id2]))[integer_id2] # names should be the same self.assertEqual(name1, name2) self.assertEqual( self.import_(['value'], [[name2]]), ok(1)) self.assertEqual([ (integer_id1, name1) ], values(self.read())) def test_fail_by_implicit_id(self): """ Can't implicitly import records by id """ # create integer objects integer_id1 = self.registry('export.integer').create( self.cr, openerp.SUPERUSER_ID, {'value': 42}) integer_id2 = self.registry('export.integer').create( self.cr, openerp.SUPERUSER_ID, {'value': 36}) self.assertEqual( self.import_(['value'], [ # import by id, without specifying it [integer_id1], [integer_id2], [integer_id1], ]), error(1, u"No matching record found for name '%s' in field 'unknown'" % integer_id1)) def test_sub_field(self): """ Does not implicitly create the record, does not warn that you can't import m2o subfields (at all)... """ self.assertEqual( self.import_(['value/value'], [['42']]), error(1, u"Can not create Many-To-One records indirectly, import the field separately")) def test_fail_noids(self): self.assertEqual( self.import_(['value'], [['nameisnoexist:3']]), error(1, u"No matching record found for name 'nameisnoexist:3' in field 'unknown'")) self.cr.rollback() self.assertEqual( self.import_(['value/id'], [['noxidhere']]), error(1, u"No matching record found for external id 'noxidhere' in field 'unknown'")) self.cr.rollback() self.assertEqual( self.import_(['value/.id'], [[66]]), error(1, u"No matching record found for database id '66' in field 'unknown'")) class test_m2m(ImporterCase): model_name = 'export.many2many' # apparently, one and only thing which works is a # csv_internal_sep-separated list of ids, xids, or names (depending if # m2m/.id, m2m/id or m2m[/anythingelse] def test_ids(self): id1 = self.registry('export.many2many.other').create( self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'}) id2 = self.registry('export.many2many.other').create( self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'}) id3 = self.registry('export.many2many.other').create( self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'}) id4 = self.registry('export.many2many.other').create( self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'}) id5 = self.registry('export.many2many.other').create( self.cr, openerp.SUPERUSER_ID, {'value': 99, 'str': 'record4'}) self.assertEqual( self.import_(['value/.id'], [ ['%d,%d' % (id1, id2)], ['%d,%d,%d' % (id1, id3, id4)], ['%d,%d,%d' % (id1, id2, id3)], ['%d' % id5] ]), ok(4)) ids = lambda records: [record.id for record in records] b = self.browse() self.assertEqual(ids(b[0].value), [id1, id2]) self.assertEqual(values(b[0].value), [3, 44]) self.assertEqual(ids(b[2].value), [id1, id2, id3]) self.assertEqual(values(b[2].value), [3, 44, 84]) def test_noids(self): self.assertEqual( self.import_(['value/.id'], [['42']]), error(1, u"No matching record found for database id '42' in field 'unknown'")) def test_xids(self): M2O_o = self.registry('export.many2many.other') id1 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'}) id2 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'}) id3 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'}) id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'}) records = M2O_o.browse(self.cr, openerp.SUPERUSER_ID, [id1, id2, id3, id4]) self.assertEqual( self.import_(['value/id'], [ ['%s,%s' % (self.xid(records[0]), self.xid(records[1]))], ['%s' % self.xid(records[3])], ['%s,%s' % (self.xid(records[2]), self.xid(records[1]))], ]), ok(3)) b = self.browse() self.assertEqual(values(b[0].value), [3, 44]) self.assertEqual(values(b[2].value), [44, 84]) def test_noxids(self): self.assertEqual( self.import_(['value/id'], [['noxidforthat']]), error(1, u"No matching record found for external id 'noxidforthat' in field 'unknown'")) def test_names(self): M2O_o = self.registry('export.many2many.other') id1 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'}) id2 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'}) id3 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'}) id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'}) records = M2O_o.browse(self.cr, openerp.SUPERUSER_ID, [id1, id2, id3, id4]) name = lambda record: dict(record.name_get())[record.id] self.assertEqual( self.import_(['value'], [ ['%s,%s' % (name(records[1]), name(records[2]))], ['%s,%s,%s' % (name(records[0]), name(records[1]), name(records[2]))], ['%s,%s' % (name(records[0]), name(records[3]))], ]), ok(3)) b = self.browse() self.assertEqual(values(b[1].value), [3, 44, 84]) self.assertEqual(values(b[2].value), [3, 9]) def test_nonames(self): self.assertEqual( self.import_(['value'], [['wherethem2mhavenonames']]), error(1, u"No matching record found for name 'wherethem2mhavenonames' in field 'unknown'")) def test_import_to_existing(self): M2O_o = self.registry('export.many2many.other') id1 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'}) id2 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'}) id3 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'}) id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'}) xid = 'myxid' self.assertEqual( self.import_(['id', 'value/.id'], [[xid, '%d,%d' % (id1, id2)]]), ok(1)) self.assertEqual( self.import_(['id', 'value/.id'], [[xid, '%d,%d' % (id3, id4)]]), ok(1)) b = self.browse() self.assertEqual(len(b), 1) # TODO: replacement of existing m2m values is correct? self.assertEqual(values(b[0].value), [84, 9]) class test_o2m(ImporterCase): model_name = 'export.one2many' def test_name_get(self): s = u'Java is a DSL for taking large XML files and converting them to' \ u' stack traces' self.assertEqual( self.import_( ['const', 'value'], [['5', s]]), error(1, u"No matching record found for name '%s' in field 'unknown'" % s)) def test_single(self): self.assertEqual( self.import_(['const', 'value/value'], [ ['5', '63'] ]), ok(1)) (b,) = self.browse() self.assertEqual(b.const, 5) self.assertEqual(values(b.value), [63]) def test_multicore(self): self.assertEqual( self.import_(['const', 'value/value'], [ ['5', '63'], ['6', '64'], ]), ok(2)) b1, b2 = self.browse() self.assertEqual(b1.const, 5) self.assertEqual(values(b1.value), [63]) self.assertEqual(b2.const, 6) self.assertEqual(values(b2.value), [64]) def test_multisub(self): self.assertEqual( self.import_(['const', 'value/value'], [ ['5', '63'], ['', '64'], ['', '65'], ['', '66'], ]), ok(4)) (b,) = self.browse() self.assertEqual(values(b.value), [63, 64, 65, 66]) def test_multi_subfields(self): self.assertEqual( self.import_(['value/str', 'const', 'value/value'], [ ['this', '5', '63'], ['is', '', '64'], ['the', '', '65'], ['rhythm', '', '66'], ]), ok(4)) (b,) = self.browse() self.assertEqual(values(b.value), [63, 64, 65, 66]) self.assertEqual( values(b.value, 'str'), 'this is the rhythm'.split()) def test_link_inline(self): id1 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, { 'str': 'Bf', 'value': 109 }) id2 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, { 'str': 'Me', 'value': 262 }) try: self.import_(['const', 'value/.id'], [ ['42', '%d,%d' % (id1, id2)] ]) except ValueError, e: # should be Exception(Database ID doesn't exist: export.one2many.child : $id1,$id2) self.assertIs(type(e), ValueError) self.assertEqual( e.args[0], "invalid literal for int() with base 10: '%d,%d'" % (id1, id2)) def test_link(self): id1 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, { 'str': 'Bf', 'value': 109 }) id2 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, { 'str': 'Me', 'value': 262 }) self.assertEqual( self.import_(['const', 'value/.id'], [ ['42', str(id1)], ['', str(id2)], ]), ok(2)) [b] = self.browse() self.assertEqual(b.const, 42) # automatically forces link between core record and o2ms self.assertEqual(values(b.value), [109, 262]) self.assertEqual(values(b.value, field='parent_id'), [b, b]) def test_link_2(self): O2M_c = self.registry('export.one2many.child') id1 = O2M_c.create(self.cr, openerp.SUPERUSER_ID, { 'str': 'Bf', 'value': 109 }) id2 = O2M_c.create(self.cr, openerp.SUPERUSER_ID, { 'str': 'Me', 'value': 262 }) self.assertEqual( self.import_(['const', 'value/.id', 'value/value'], [ ['42', str(id1), '1'], ['', str(id2), '2'], ]), ok(2)) [b] = self.browse() self.assertEqual(b.const, 42) self.assertEqual(values(b.value), [1, 2]) self.assertEqual(values(b.value, field='parent_id'), [b, b]) class test_o2m_multiple(ImporterCase): model_name = 'export.one2many.multiple' def test_multi_mixed(self): self.assertEqual( self.import_(['const', 'child1/value', 'child2/value'], [ ['5', '11', '21'], ['', '12', '22'], ['', '13', '23'], ['', '14', ''], ]), ok(4)) [b] = self.browse() self.assertEqual(values(b.child1), [11, 12, 13, 14]) self.assertEqual(values(b.child2), [21, 22, 23]) def test_multi(self): self.assertEqual( self.import_(['const', 'child1/value', 'child2/value'], [ ['5', '11', '21'], ['', '12', ''], ['', '13', ''], ['', '14', ''], ['', '', '22'], ['', '', '23'], ]), ok(6)) [b] = self.browse() self.assertEqual(values(b.child1), [11, 12, 13, 14]) self.assertEqual(values(b.child2), [21, 22, 23]) def test_multi_fullsplit(self): self.assertEqual( self.import_(['const', 'child1/value', 'child2/value'], [ ['5', '11', ''], ['', '12', ''], ['', '13', ''], ['', '14', ''], ['', '', '21'], ['', '', '22'], ['', '', '23'], ]), ok(7)) [b] = self.browse() self.assertEqual(b.const, 5) self.assertEqual(values(b.child1), [11, 12, 13, 14]) self.assertEqual(values(b.child2), [21, 22, 23]) # function, related, reference: written to db as-is... # => function uses @type for value coercion/conversion