[MERGE] forward port of branch saas-3 up to revid 5093 chs@openerp.com-20140318114540...
authorChristophe Simonis <chs@openerp.com>
Tue, 18 Mar 2014 12:41:12 +0000 (13:41 +0100)
committerChristophe Simonis <chs@openerp.com>
Tue, 18 Mar 2014 12:41:12 +0000 (13:41 +0100)
bzr revid: chs@openerp.com-20140318124112-cj65l3wysk2rhwlw

1  2 
openerp/addons/base/tests/test_base.py
openerp/addons/base/tests/test_ir_sequence.py
openerp/addons/test_impex/tests/test_export.py
openerp/http.py
openerp/osv/orm.py

index bc428d7,0000000..e925fff
mode 100644,000000..100644
--- /dev/null
@@@ -1,221 -1,0 +1,222 @@@
 +# -*- coding: utf-8 -*-
 +# Run with one of these commands:
 +#    > OPENERP_ADDONS_PATH='../../addons/trunk' OPENERP_PORT=8069 \
 +#      OPENERP_DATABASE=yy PYTHONPATH=. python tests/test_ir_sequence.py
 +#    > OPENERP_ADDONS_PATH='../../addons/trunk' OPENERP_PORT=8069 \
 +#      OPENERP_DATABASE=yy nosetests tests/test_ir_sequence.py
 +#    > OPENERP_ADDONS_PATH='../../../addons/trunk' OPENERP_PORT=8069 \
 +#      OPENERP_DATABASE=yy PYTHONPATH=../:. unit2 test_ir_sequence
 +# This assume an existing database.
 +import psycopg2
++import psycopg2.errorcodes
 +import unittest2
 +
 +import openerp
 +from openerp.tests import common
 +
 +DB = common.DB
 +ADMIN_USER_ID = common.ADMIN_USER_ID
 +
 +def registry(model):
 +    return openerp.modules.registry.RegistryManager.get(DB)[model]
 +
 +def cursor():
 +    return openerp.modules.registry.RegistryManager.get(DB).db.cursor()
 +
 +
 +def drop_sequence(code):
 +    cr = cursor()
 +    for model in ['ir.sequence', 'ir.sequence.type']:
 +        s = registry(model)
 +        ids = s.search(cr, ADMIN_USER_ID, [('code', '=', code)])
 +        s.unlink(cr, ADMIN_USER_ID, ids)
 +    cr.commit()
 +    cr.close()
 +
 +class test_ir_sequence_standard(unittest2.TestCase):
 +    """ A few tests for a 'Standard' (i.e. PostgreSQL) sequence. """
 +
 +    def test_ir_sequence_create(self):
 +        """ Try to create a sequence object. """
 +        cr = cursor()
 +        d = dict(code='test_sequence_type', name='Test sequence type')
 +        c = registry('ir.sequence.type').create(cr, ADMIN_USER_ID, d, {})
 +        assert c
 +        d = dict(code='test_sequence_type', name='Test sequence')
 +        c = registry('ir.sequence').create(cr, ADMIN_USER_ID, d, {})
 +        assert c
 +        cr.commit()
 +        cr.close()
 +
 +    def test_ir_sequence_search(self):
 +        """ Try a search. """
 +        cr = cursor()
 +        ids = registry('ir.sequence').search(cr, ADMIN_USER_ID, [], {})
 +        assert ids
 +        cr.commit()
 +        cr.close()
 +
 +    def test_ir_sequence_draw(self):
 +        """ Try to draw a number. """
 +        cr = cursor()
 +        n = registry('ir.sequence').next_by_code(cr, ADMIN_USER_ID, 'test_sequence_type', {})
 +        assert n
 +        cr.commit()
 +        cr.close()
 +
 +    def test_ir_sequence_draw_twice(self):
 +        """ Try to draw a number from two transactions. """
 +        cr0 = cursor()
 +        cr1 = cursor()
 +        n0 = registry('ir.sequence').next_by_code(cr0, ADMIN_USER_ID, 'test_sequence_type', {})
 +        assert n0
 +        n1 = registry('ir.sequence').next_by_code(cr1, ADMIN_USER_ID, 'test_sequence_type', {})
 +        assert n1
 +        cr0.commit()
 +        cr1.commit()
 +        cr0.close()
 +        cr1.close()
 +
 +    @classmethod
 +    def tearDownClass(cls):
 +        drop_sequence('test_sequence_type')
 +
 +class test_ir_sequence_no_gap(unittest2.TestCase):
 +    """ Copy of the previous tests for a 'No gap' sequence. """
 +
 +    def test_ir_sequence_create_no_gap(self):
 +        """ Try to create a sequence object. """
 +        cr = cursor()
 +        d = dict(code='test_sequence_type_2', name='Test sequence type')
 +        c = registry('ir.sequence.type').create(cr, ADMIN_USER_ID, d, {})
 +        assert c
 +        d = dict(code='test_sequence_type_2', name='Test sequence',
 +            implementation='no_gap')
 +        c = registry('ir.sequence').create(cr, ADMIN_USER_ID, d, {})
 +        assert c
 +        cr.commit()
 +        cr.close()
 +
 +    def test_ir_sequence_draw_no_gap(self):
 +        """ Try to draw a number. """
 +        cr = cursor()
 +        n = registry('ir.sequence').next_by_code(cr, ADMIN_USER_ID, 'test_sequence_type_2', {})
 +        assert n
 +        cr.commit()
 +        cr.close()
 +
 +    def test_ir_sequence_draw_twice_no_gap(self):
 +        """ Try to draw a number from two transactions.
 +        This is expected to not work.
 +        """
 +        cr0 = cursor()
 +        cr1 = cursor()
 +        cr1._default_log_exceptions = False # Prevent logging a traceback
-         msg_re = '^could not obtain lock on row in relation "ir_sequence"$'
-         with self.assertRaisesRegexp(psycopg2.OperationalError, msg_re):
++        with self.assertRaises(psycopg2.OperationalError) as e:
 +            n0 = registry('ir.sequence').next_by_code(cr0, ADMIN_USER_ID, 'test_sequence_type_2', {})
 +            assert n0
 +            n1 = registry('ir.sequence').next_by_code(cr1, ADMIN_USER_ID, 'test_sequence_type_2', {})
++        self.assertEqual(e.exception.pgcode, psycopg2.errorcodes.LOCK_NOT_AVAILABLE, msg="postgresql returned an incorrect errcode")
 +        cr0.close()
 +        cr1.close()
 +
 +    @classmethod
 +    def tearDownClass(cls):
 +        drop_sequence('test_sequence_type_2')
 +
 +class test_ir_sequence_change_implementation(unittest2.TestCase):
 +    """ Create sequence objects and change their ``implementation`` field. """
 +
 +    def test_ir_sequence_1_create(self):
 +        """ Try to create a sequence object. """
 +        cr = cursor()
 +        d = dict(code='test_sequence_type_3', name='Test sequence type')
 +        c = registry('ir.sequence.type').create(cr, ADMIN_USER_ID, d, {})
 +        assert c
 +        d = dict(code='test_sequence_type_3', name='Test sequence')
 +        c = registry('ir.sequence').create(cr, ADMIN_USER_ID, d, {})
 +        assert c
 +        d = dict(code='test_sequence_type_4', name='Test sequence type')
 +        c = registry('ir.sequence.type').create(cr, ADMIN_USER_ID, d, {})
 +        assert c
 +        d = dict(code='test_sequence_type_4', name='Test sequence',
 +            implementation='no_gap')
 +        c = registry('ir.sequence').create(cr, ADMIN_USER_ID, d, {})
 +        assert c
 +        cr.commit()
 +        cr.close()
 +
 +    def test_ir_sequence_2_write(self):
 +        cr = cursor()
 +        ids = registry('ir.sequence').search(cr, ADMIN_USER_ID,
 +            [('code', 'in', ['test_sequence_type_3', 'test_sequence_type_4'])], {})
 +        registry('ir.sequence').write(cr, ADMIN_USER_ID, ids,
 +            {'implementation': 'standard'}, {})
 +        registry('ir.sequence').write(cr, ADMIN_USER_ID, ids,
 +            {'implementation': 'no_gap'}, {})
 +        cr.commit()
 +        cr.close()
 +
 +    def test_ir_sequence_3_unlink(self):
 +        cr = cursor()
 +        ids = registry('ir.sequence').search(cr, ADMIN_USER_ID,
 +            [('code', 'in', ['test_sequence_type_3', 'test_sequence_type_4'])], {})
 +        registry('ir.sequence').unlink(cr, ADMIN_USER_ID, ids, {})
 +        cr.commit()
 +        cr.close()
 +
 +    @classmethod
 +    def tearDownClass(cls):
 +        drop_sequence('test_sequence_type_3')
 +        drop_sequence('test_sequence_type_4')
 +
 +class test_ir_sequence_generate(unittest2.TestCase):
 +    """ Create sequence objects and generate some values. """
 +
 +    def test_ir_sequence_create(self):
 +        """ Try to create a sequence object. """
 +        cr = cursor()
 +        d = dict(code='test_sequence_type_5', name='Test sequence type')
 +        c = registry('ir.sequence.type').create(cr, ADMIN_USER_ID, d, {})
 +        assert c
 +        d = dict(code='test_sequence_type_5', name='Test sequence')
 +        c = registry('ir.sequence').create(cr, ADMIN_USER_ID, d, {})
 +        assert c
 +        cr.commit()
 +        cr.close()
 +
 +        cr = cursor()
 +        f = lambda *a: registry('ir.sequence').next_by_code(cr, ADMIN_USER_ID, 'test_sequence_type_5', {})
 +        assert all(str(x) == f() for x in xrange(1,10))
 +        cr.commit()
 +        cr.close()
 +
 +    def test_ir_sequence_create_no_gap(self):
 +        """ Try to create a sequence object. """
 +        cr = cursor()
 +        d = dict(code='test_sequence_type_6', name='Test sequence type')
 +        c = registry('ir.sequence.type').create(cr, ADMIN_USER_ID, d, {})
 +        assert c
 +        d = dict(code='test_sequence_type_6', name='Test sequence')
 +        c = registry('ir.sequence').create(cr, ADMIN_USER_ID, d, {})
 +        assert c
 +        cr.commit()
 +        cr.close()
 +
 +        cr = cursor()
 +        f = lambda *a: registry('ir.sequence').next_by_code(cr, ADMIN_USER_ID, 'test_sequence_type_6', {})
 +        assert all(str(x) == f() for x in xrange(1,10))
 +        cr.commit()
 +        cr.close()
 +
 +    @classmethod
 +    def tearDownClass(cls):
 +        drop_sequence('test_sequence_type_5')
 +        drop_sequence('test_sequence_type_6')
 +
 +
 +if __name__ == '__main__':
 +    unittest2.main()
 +
 +
 +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
index 26219b3,0000000..995d694
mode 100644,000000..100644
--- /dev/null
@@@ -1,589 -1,0 +1,590 @@@
 +# -*- coding: utf-8 -*-
 +import itertools
 +import openerp.modules.registry
 +import openerp
 +
 +from openerp.tests import common
 +
 +
 +class CreatorCase(common.TransactionCase):
 +    model_name = False
 +
 +    def __init__(self, *args, **kwargs):
 +        super(CreatorCase, self).__init__(*args, **kwargs)
 +        self.model = None
 +
 +    def setUp(self):
 +        super(CreatorCase, self).setUp()
 +        self.model = self.registry(self.model_name)
 +    def make(self, value):
 +        id = self.model.create(self.cr, openerp.SUPERUSER_ID, {'value': value})
 +        return self.model.browse(self.cr, openerp.SUPERUSER_ID, [id])[0]
 +    def export(self, value, fields=('value',), context=None):
 +        record = self.make(value)
 +        return self.model._BaseModel__export_row(
 +            self.cr, openerp.SUPERUSER_ID, record,
 +            [f.split('/') for f in fields],
 +            context=context)
 +
 +class test_boolean_field(CreatorCase):
 +    model_name = 'export.boolean'
 +
 +    def test_true(self):
 +        self.assertEqual(
 +            self.export(True),
 +            [[u'True']])
 +    def test_false(self):
 +        """ ``False`` value to boolean fields is unique in being exported as a
 +        (unicode) string, not a boolean
 +        """
 +        self.assertEqual(
 +            self.export(False),
 +            [[u'False']])
 +
 +class test_integer_field(CreatorCase):
 +    model_name = 'export.integer'
 +
 +    def test_empty(self):
 +        self.assertEqual(self.model.search(self.cr, openerp.SUPERUSER_ID, []), [],
 +                         "Test model should have no records")
 +    def test_0(self):
 +        self.assertEqual(
 +            self.export(0),
 +            [[False]])
 +
 +    def test_basic_value(self):
 +        self.assertEqual(
 +            self.export(42),
 +            [[u'42']])
 +
 +    def test_negative(self):
 +        self.assertEqual(
 +            self.export(-32),
 +            [[u'-32']])
 +
 +    def test_huge(self):
 +        self.assertEqual(
 +            self.export(2**31-1),
 +            [[unicode(2**31-1)]])
 +
 +class test_float_field(CreatorCase):
 +    model_name = 'export.float'
 +
 +    def test_0(self):
 +        self.assertEqual(
 +            self.export(0.0),
 +            [[False]])
 +
 +    def test_epsilon(self):
 +        self.assertEqual(
 +            self.export(0.000000000027),
 +            [[u'2.7e-11']])
 +
 +    def test_negative(self):
 +        self.assertEqual(
 +            self.export(-2.42),
 +            [[u'-2.42']])
 +
 +    def test_positive(self):
 +        self.assertEqual(
 +            self.export(47.36),
 +            [[u'47.36']])
 +
 +    def test_big(self):
 +        self.assertEqual(
 +            self.export(87654321.4678),
 +            [[u'87654321.4678']])
 +
 +class test_decimal_field(CreatorCase):
 +    model_name = 'export.decimal'
 +
 +    def test_0(self):
 +        self.assertEqual(
 +            self.export(0.0),
 +            [[False]])
 +
 +    def test_epsilon(self):
 +        """ epsilon gets sliced to 0 due to precision
 +        """
 +        self.assertEqual(
 +            self.export(0.000000000027),
 +            [[False]])
 +
 +    def test_negative(self):
 +        self.assertEqual(
 +            self.export(-2.42),
 +            [[u'-2.42']])
 +
 +    def test_positive(self):
 +        self.assertEqual(
 +            self.export(47.36),
 +            [[u'47.36']])
 +
 +    def test_big(self):
 +        self.assertEqual(
 +            self.export(87654321.4678), [[u'87654321.468']])
 +
 +class test_string_field(CreatorCase):
 +    model_name = 'export.string.bounded'
 +
 +    def test_empty(self):
 +        self.assertEqual(
 +            self.export(""),
 +            [[False]])
 +    def test_within_bounds(self):
 +        self.assertEqual(
 +            self.export("foobar"),
 +            [[u"foobar"]])
 +    def test_out_of_bounds(self):
 +        self.assertEqual(
 +            self.export("C for Sinking, "
 +                        "Java for Drinking, "
 +                        "Smalltalk for Thinking. "
 +                        "...and Power to the Penguin!"),
 +            [[u"C for Sinking, J"]])
 +
 +class test_unbound_string_field(CreatorCase):
 +    model_name = 'export.string'
 +
 +    def test_empty(self):
 +        self.assertEqual(
 +            self.export(""),
 +            [[False]])
 +    def test_small(self):
 +        self.assertEqual(
 +            self.export("foobar"),
 +            [[u"foobar"]])
 +    def test_big(self):
 +        self.assertEqual(
 +            self.export("We flew down weekly to meet with IBM, but they "
 +                        "thought the way to measure software was the amount "
 +                        "of code we wrote, when really the better the "
 +                        "software, the fewer lines of code."),
 +            [[u"We flew down weekly to meet with IBM, but they thought the "
 +              u"way to measure software was the amount of code we wrote, "
 +              u"when really the better the software, the fewer lines of "
 +              u"code."]])
 +
 +class test_text(CreatorCase):
 +    model_name = 'export.text'
 +
 +    def test_empty(self):
 +        self.assertEqual(
 +            self.export(""),
 +            [[False]])
 +    def test_small(self):
 +        self.assertEqual(
 +            self.export("foobar"),
 +            [[u"foobar"]])
 +    def test_big(self):
 +        self.assertEqual(
 +            self.export("So, `bind' is `let' and monadic programming is"
 +                        " equivalent to programming in the A-normal form. That"
 +                        " is indeed all there is to monads"),
 +            [[u"So, `bind' is `let' and monadic programming is equivalent to"
 +              u" programming in the A-normal form. That is indeed all there"
 +              u" is to monads"]])
 +
 +class test_date(CreatorCase):
 +    model_name = 'export.date'
 +
 +    def test_empty(self):
 +        self.assertEqual(
 +            self.export(False),
 +            [[False]])
 +    def test_basic(self):
 +        self.assertEqual(
 +            self.export('2011-11-07'),
 +            [[u'2011-11-07']])
 +
 +class test_datetime(CreatorCase):
 +    model_name = 'export.datetime'
 +
 +    def test_empty(self):
 +        self.assertEqual(
 +            self.export(False),
 +            [[False]])
 +    def test_basic(self):
 +        self.assertEqual(
 +            self.export('2011-11-07 21:05:48'),
 +            [[u'2011-11-07 21:05:48']])
 +    def test_tz(self):
 +        """ Export ignores the timezone and always exports to UTC
 +
 +        .. note:: on the other hand, export uses user lang for name_get
 +        """
 +        # NOTE: ignores user timezone, always exports to UTC
 +        self.assertEqual(
 +            self.export('2011-11-07 21:05:48', context={'tz': 'Pacific/Norfolk'}),
 +            [[u'2011-11-07 21:05:48']])
 +
 +class test_selection(CreatorCase):
 +    model_name = 'export.selection'
 +    translations_fr = [
 +        ("Qux", "toto"),
 +        ("Bar", "titi"),
 +        ("Foo", "tete"),
 +    ]
 +
 +    def test_empty(self):
 +        self.assertEqual(
 +            self.export(False),
 +            [[False]])
 +
 +    def test_value(self):
 +        """ selections export the *label* for their value
 +        """
 +        self.assertEqual(
 +            self.export(2),
 +            [[u"Bar"]])
 +
 +    def test_localized_export(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.export(2, context={'lang': 'fr_FR'}),
 +            [[u'Bar']])
 +
 +class test_selection_function(CreatorCase):
 +    model_name = 'export.selection.function'
 +
 +    def test_empty(self):
 +        self.assertEqual(
 +            self.export(False),
 +            [[False]])
 +
 +    def test_value(self):
 +        # FIXME: selection functions export the *value* itself
 +        self.assertEqual(
 +            self.export(1),
 +            [[u'1']])
 +        self.assertEqual(
 +            self.export(3),
 +            [[u'3']])
 +        # fucking hell
 +        self.assertEqual(
 +            self.export(0),
 +            [[False]])
 +
 +class test_m2o(CreatorCase):
 +    model_name = 'export.many2one'
 +
 +    def test_empty(self):
 +        self.assertEqual(
 +            self.export(False),
 +            [[False]])
 +    def test_basic(self):
 +        """ Exported value is the name_get of the related object
 +        """
 +        integer_id = self.registry('export.integer').create(
 +            self.cr, openerp.SUPERUSER_ID, {'value': 42})
 +        name = dict(self.registry('export.integer').name_get(
 +            self.cr, openerp.SUPERUSER_ID,[integer_id]))[integer_id]
 +        self.assertEqual(
 +            self.export(integer_id),
 +            [[name]])
 +    def test_path(self):
 +        """ Can recursively export fields of m2o via path
 +        """
 +        integer_id = self.registry('export.integer').create(
 +            self.cr, openerp.SUPERUSER_ID, {'value': 42})
 +        self.assertEqual(
 +            self.export(integer_id, fields=['value/.id', 'value/value']),
 +            [[unicode(integer_id), u'42']])
 +    def test_external_id(self):
 +        integer_id = self.registry('export.integer').create(
 +            self.cr, openerp.SUPERUSER_ID, {'value': 42})
-         # __export__.$class.$id
-         external_id = u'__export__.export_many2one_%d' % integer_id
++        # Expecting the m2o target model name in the external id,
++        # not this model's name
++        external_id = u'__export__.export_integer_%d' % integer_id
 +        self.assertEqual(
 +            self.export(integer_id, fields=['value/id']),
 +            [[external_id]])
 +
 +class test_o2m(CreatorCase):
 +    model_name = 'export.one2many'
 +    commands = [
 +        (0, False, {'value': 4, 'str': 'record1'}),
 +        (0, False, {'value': 42, 'str': 'record2'}),
 +        (0, False, {'value': 36, 'str': 'record3'}),
 +        (0, False, {'value': 4, 'str': 'record4'}),
 +        (0, False, {'value': 13, 'str': 'record5'}),
 +    ]
 +    names = [
 +        u'export.one2many.child:%d' % d['value']
 +        for c, _, d in commands
 +    ]
 +
 +    def test_empty(self):
 +        self.assertEqual(
 +            self.export(False),
 +            [[False]])
 +
 +    def test_single(self):
 +        self.assertEqual(
 +            self.export([(0, False, {'value': 42})]),
 +            # name_get result
 +            [[u'export.one2many.child:42']])
 +
 +    def test_single_subfield(self):
 +        self.assertEqual(
 +            self.export([(0, False, {'value': 42})],
 +                        fields=['value', 'value/value']),
 +            [[u'export.one2many.child:42', u'42']])
 +
 +    def test_integrate_one_in_parent(self):
 +        self.assertEqual(
 +            self.export([(0, False, {'value': 42})],
 +                        fields=['const', 'value/value']),
 +            [[u'4', u'42']])
 +
 +    def test_multiple_records(self):
 +        self.assertEqual(
 +            self.export(self.commands, fields=['const', 'value/value']),
 +            [
 +                [u'4', u'4'],
 +                [u'', u'42'],
 +                [u'', u'36'],
 +                [u'', u'4'],
 +                [u'', u'13'],
 +            ])
 +
 +    def test_multiple_records_name(self):
 +        self.assertEqual(
 +            self.export(self.commands, fields=['const', 'value']),
 +            [[
 +                u'4', u','.join(self.names)
 +            ]])
 +
 +    def test_multiple_records_id(self):
 +        export = self.export(self.commands, fields=['const', 'value/.id'])
 +        O2M_c = self.registry('export.one2many.child')
 +        ids = O2M_c.browse(self.cr, openerp.SUPERUSER_ID,
 +                           O2M_c.search(self.cr, openerp.SUPERUSER_ID, []))
 +        self.assertEqual(
 +            export,
 +            [
 +                ['4', str(ids[0].id)],
 +                ['', str(ids[1].id)],
 +                ['', str(ids[2].id)],
 +                ['', str(ids[3].id)],
 +                ['', str(ids[4].id)],
 +            ])
 +
 +    def test_multiple_records_with_name_before(self):
 +        self.assertEqual(
 +            self.export(self.commands, fields=['const', 'value', 'value/value']),
 +            [[ # exports sub-fields of very first o2m
 +                u'4', u','.join(self.names), u'4'
 +            ]])
 +
 +    def test_multiple_records_with_name_after(self):
 +        self.assertEqual(
 +            self.export(self.commands, fields=['const', 'value/value', 'value']),
 +            [ # completely ignores name_get request
 +                [u'4', u'4', ''],
 +                ['', u'42', ''],
 +                ['', u'36', ''],
 +                ['', u'4', ''],
 +                ['', u'13', ''],
 +            ])
 +
 +    def test_multiple_subfields_neighbour(self):
 +        self.assertEqual(
 +            self.export(self.commands, fields=['const', 'value/str','value/value']),
 +            [
 +                [u'4', u'record1', u'4'],
 +                ['', u'record2', u'42'],
 +                ['', u'record3', u'36'],
 +                ['', u'record4', u'4'],
 +                ['', u'record5', u'13'],
 +            ])
 +
 +    def test_multiple_subfields_separated(self):
 +        self.assertEqual(
 +            self.export(self.commands, fields=['value/str', 'const', 'value/value']),
 +            [
 +                [u'record1', u'4', u'4'],
 +                [u'record2', '', u'42'],
 +                [u'record3', '', u'36'],
 +                [u'record4', '', u'4'],
 +                [u'record5', '', u'13'],
 +            ])
 +
 +class test_o2m_multiple(CreatorCase):
 +    model_name = 'export.one2many.multiple'
 +
 +    def make(self, value=None, **values):
 +        if value is not None: values['value'] = value
 +        id = self.model.create(self.cr, openerp.SUPERUSER_ID, values)
 +        return self.model.browse(self.cr, openerp.SUPERUSER_ID, [id])[0]
 +    def export(self, value=None, fields=('child1', 'child2',), context=None, **values):
 +        record = self.make(value, **values)
 +        return self.model._BaseModel__export_row(
 +            self.cr, openerp.SUPERUSER_ID, record,
 +            [f.split('/') for f in fields],
 +            context=context)
 +
 +    def test_empty(self):
 +        self.assertEqual(
 +            self.export(child1=False, child2=False),
 +            [[False, False]])
 +
 +    def test_single_per_side(self):
 +        self.assertEqual(
 +            self.export(child1=False, child2=[(0, False, {'value': 42})]),
 +            [[False, u'export.one2many.child.2:42']])
 +
 +        self.assertEqual(
 +            self.export(child1=[(0, False, {'value': 43})], child2=False),
 +            [[u'export.one2many.child.1:43', False]])
 +
 +        self.assertEqual(
 +            self.export(child1=[(0, False, {'value': 43})],
 +                        child2=[(0, False, {'value': 42})]),
 +            [[u'export.one2many.child.1:43', u'export.one2many.child.2:42']])
 +
 +    def test_single_integrate_subfield(self):
 +        fields = ['const', 'child1/value', 'child2/value']
 +        self.assertEqual(
 +            self.export(child1=False, child2=[(0, False, {'value': 42})],
 +                        fields=fields),
 +            [[u'36', False, u'42']])
 +
 +        self.assertEqual(
 +            self.export(child1=[(0, False, {'value': 43})], child2=False,
 +                        fields=fields),
 +            [[u'36', u'43', False]])
 +
 +        self.assertEqual(
 +            self.export(child1=[(0, False, {'value': 43})],
 +                        child2=[(0, False, {'value': 42})],
 +                        fields=fields),
 +            [[u'36', u'43', u'42']])
 +
 +    def test_multiple(self):
 +        """ With two "concurrent" o2ms, exports the first line combined, then
 +        exports the rows for the first o2m, then the rows for the second o2m.
 +        """
 +        fields = ['const', 'child1/value', 'child2/value']
 +        child1 = [(0, False, {'value': v, 'str': 'record%.02d' % index})
 +                  for index, v in zip(itertools.count(), [4, 42, 36, 4, 13])]
 +        child2 = [(0, False, {'value': v, 'str': 'record%.02d' % index})
 +                  for index, v in zip(itertools.count(10), [8, 12, 8, 55, 33, 13])]
 +
 +        self.assertEqual(
 +            self.export(child1=child1, child2=False, fields=fields),
 +            [
 +                [u'36', u'4', False],
 +                ['', u'42', ''],
 +                ['', u'36', ''],
 +                ['', u'4', ''],
 +                ['', u'13', ''],
 +            ])
 +        self.assertEqual(
 +            self.export(child1=False, child2=child2, fields=fields),
 +            [
 +                [u'36', False, u'8'],
 +                ['', '', u'12'],
 +                ['', '', u'8'],
 +                ['', '', u'55'],
 +                ['', '', u'33'],
 +                ['', '', u'13'],
 +            ])
 +        self.assertEqual(
 +            self.export(child1=child1, child2=child2, fields=fields),
 +            [
 +                [u'36', u'4', u'8'],
 +                ['', u'42', ''],
 +                ['', u'36', ''],
 +                ['', u'4', ''],
 +                ['', u'13', ''],
 +                ['', '', u'12'],
 +                ['', '', u'8'],
 +                ['', '', u'55'],
 +                ['', '', u'33'],
 +                ['', '', u'13'],
 +            ])
 +
 +class test_m2m(CreatorCase):
 +    model_name = 'export.many2many'
 +    commands = [
 +        (0, False, {'value': 4, 'str': 'record000'}),
 +        (0, False, {'value': 42, 'str': 'record001'}),
 +        (0, False, {'value': 36, 'str': 'record010'}),
 +        (0, False, {'value': 4, 'str': 'record011'}),
 +        (0, False, {'value': 13, 'str': 'record100'}),
 +    ]
 +    names = [
 +        u'export.many2many.other:%d' % d['value']
 +        for c, _, d in commands
 +    ]
 +
 +    def test_empty(self):
 +        self.assertEqual(
 +            self.export(False),
 +            [[False]])
 +
 +    def test_single(self):
 +        self.assertEqual(
 +            self.export([(0, False, {'value': 42})]),
 +            # name_get result
 +            [[u'export.many2many.other:42']])
 +
 +    def test_single_subfield(self):
 +        self.assertEqual(
 +            self.export([(0, False, {'value': 42})],
 +                        fields=['value', 'value/value']),
 +            [[u'export.many2many.other:42', u'42']])
 +
 +    def test_integrate_one_in_parent(self):
 +        self.assertEqual(
 +            self.export([(0, False, {'value': 42})],
 +                        fields=['const', 'value/value']),
 +            [[u'4', u'42']])
 +
 +    def test_multiple_records(self):
 +        self.assertEqual(
 +            self.export(self.commands, fields=['const', 'value/value']),
 +            [
 +                [u'4', u'4'],
 +                [u'', u'42'],
 +                [u'', u'36'],
 +                [u'', u'4'],
 +                [u'', u'13'],
 +            ])
 +
 +    def test_multiple_records_name(self):
 +        self.assertEqual(
 +            self.export(self.commands, fields=['const', 'value']),
 +            [[ # FIXME: hardcoded comma, import uses config.csv_internal_sep
 +               # resolution: remove configurable csv_internal_sep
 +                u'4', u','.join(self.names)
 +            ]])
 +
 +    # essentially same as o2m, so boring
 +
 +class test_function(CreatorCase):
 +    model_name = 'export.function'
 +
 +    def test_value(self):
 +        """ Exports value normally returned by accessing the function field
 +        """
 +        self.assertEqual(
 +            self.export(42),
 +            [[u'3']])
diff --cc openerp/http.py
@@@ -133,6 -134,11 +133,10 @@@ class WebRequest(object)
          self.auth_method = None
          self._cr_cm = None
          self._cr = None
 -        self.func_request_type = None
+         # prevents transaction commit, use when you catch an exception during handling
+         self._failed = None
          # set db/uid trackers - they're cleaned up at the WSGI
          # dispatching phase in openerp.service.wsgi_server.application
          if self.db:
  
      def __exit__(self, exc_type, exc_value, traceback):
          _request_stack.pop()
 +
          if self._cr:
-             # Dont commit test cursors
 -            if exc_type is None and not self._failed:
 -                self._cr.commit()
 -            else:
 -                # just to be explicit - happens at close() anyway
 -                self._cr.rollback()
 -            self._cr.close()
++            # Dont close test cursors
 +            if not openerp.tests.common.release_test_cursor(self._cr):
-                 if exc_type is None:
++                if exc_type is None and not self._failed:
 +                    self._cr.commit()
++                else:
++                    # just to be explicit - happens at close() anyway
++                    self._cr.rollback()
 +                self._cr.close()
          # just to be sure no one tries to re-use the request
          self.disable_db = True
          self.uid = None
@@@ -368,10 -357,9 +375,11 @@@ class JsonRequest(WebRequest)
                  'message': "OpenERP Session Invalid",
                  'data': se
              }
+             self._failed = e # prevent tx commit
          except Exception, e:
 -            _logger.exception("Exception during JSON request handling.")
 +            # Mute test cursor error for runbot
 +            if not (openerp.tools.config['test_enable'] and isinstance(e, psycopg2.OperationalError)):
 +                _logger.exception("JSON-RPC Exception in %s.", self.httprequest.path)
              se = serialize_exception(e)
              error = {
                  'code': 200,
Simple merge