1# -*- coding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4#
5# test cases for new-style fields
6#
7import base64
8from collections import OrderedDict
9from datetime import date, datetime, time
10import io
11from PIL import Image
12import psycopg2
13
14from odoo import models, fields
15from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
16from odoo.exceptions import AccessError, UserError, ValidationError
17from odoo.tests import common
18from odoo.tools import mute_logger, float_repr
19from odoo.tools.date_utils import add, subtract, start_of, end_of
20from odoo.tools.image import image_data_uri
21
22
23class TestFields(TransactionCaseWithUserDemo):
24
25    def setUp(self):
26        super(TestFields, self).setUp()
27        self.env.ref('test_new_api.discussion_0').write({'participants': [(4, self.user_demo.id)]})
28        # YTI FIX ME: The cache shouldn't be inconsistent (rco is gonna fix it)
29        # self.env.ref('test_new_api.discussion_0').participants -> 1 user
30        # self.env.ref('test_new_api.discussion_0').invalidate_cache()
31        # self.env.ref('test_new_api.discussion_0').with_context(active_test=False).participants -> 2 users
32        self.env.ref('test_new_api.message_0_1').write({'author': self.user_demo.id})
33
34    def test_00_basics(self):
35        """ test accessing new fields """
36        # find a discussion
37        discussion = self.env.ref('test_new_api.discussion_0')
38
39        # read field as a record attribute or as a record item
40        self.assertIsInstance(discussion.name, str)
41        self.assertIsInstance(discussion['name'], str)
42        self.assertEqual(discussion['name'], discussion.name)
43
44        # read it with method read()
45        values = discussion.read(['name'])[0]
46        self.assertEqual(values['name'], discussion.name)
47
48    def test_01_basic_get_assertion(self):
49        """ test item getter """
50        # field access works on single record
51        record = self.env.ref('test_new_api.message_0_0')
52        self.assertEqual(len(record), 1)
53        ok = record.body
54
55        # field access fails on multiple records
56        records = self.env['test_new_api.message'].search([])
57        assert len(records) > 1
58        with self.assertRaises(ValueError):
59            faulty = records.body
60
61    def test_01_basic_set_assertion(self):
62        """ test item setter """
63        # field assignment works on single record
64        record = self.env.ref('test_new_api.message_0_0')
65        self.assertEqual(len(record), 1)
66        record.body = 'OK'
67
68        # field assignment on multiple records should assign value to all records
69        records = self.env['test_new_api.message'].search([])
70        records.body = 'Updated'
71        self.assertTrue(all(map(lambda record:record.body=='Updated', records)))
72
73        # field assigmenent does not cache the wrong value when write overridden
74        record.priority = 4
75        self.assertEqual(record.priority, 5)
76
77    def test_05_unknown_fields(self):
78        """ test ORM operations with unknown fields """
79        cat = self.env['test_new_api.category'].create({'name': 'Foo'})
80
81        with self.assertRaisesRegex(ValueError, 'Invalid field'):
82            cat.search([('zzz', '=', 42)])
83        with self.assertRaisesRegex(ValueError, 'Invalid field'):
84            cat.search([], order='zzz')
85
86        with self.assertRaisesRegex(ValueError, 'Invalid field'):
87            cat.read(['zzz'])
88
89        with self.assertRaisesRegex(ValueError, 'Invalid field'):
90            cat.read_group([('zzz', '=', 42)], fields=['color'], groupby=['parent'])
91        with self.assertRaisesRegex(ValueError, 'Invalid field'):
92            cat.read_group([], fields=['zzz'], groupby=['parent'])
93        with self.assertRaisesRegex(ValueError, 'Invalid field'):
94            cat.read_group([], fields=['zzz:sum'], groupby=['parent'])
95        with self.assertRaisesRegex(ValueError, 'Invalid field'):
96            cat.read_group([], fields=['color'], groupby=['zzz'])
97        with self.assertRaisesRegex(ValueError, 'Invalid field'):
98            cat.read_group([], fields=['color'], groupby=['parent'], orderby='zzz')
99        # exception: accept '__count' as field to aggregate
100        cat.read_group([], fields=['__count'], groupby=['parent'])
101
102        with self.assertRaisesRegex(ValueError, 'Invalid field'):
103            cat.create({'name': 'Foo', 'zzz': 42})
104
105        with self.assertRaisesRegex(ValueError, 'Invalid field'):
106            cat.write({'zzz': 42})
107
108        with self.assertRaisesRegex(ValueError, 'Invalid field'):
109            cat.new({'name': 'Foo', 'zzz': 42})
110
111    def test_10_computed(self):
112        """ check definition of computed fields """
113        # by default function fields are not stored, readonly, not copied
114        field = self.env['test_new_api.message']._fields['size']
115        self.assertFalse(field.store)
116        self.assertFalse(field.compute_sudo)
117        self.assertTrue(field.readonly)
118        self.assertFalse(field.copy)
119
120        field = self.env['test_new_api.message']._fields['name']
121        self.assertTrue(field.store)
122        self.assertTrue(field.compute_sudo)
123        self.assertTrue(field.readonly)
124        self.assertFalse(field.copy)
125
126        # stored editable computed fields are copied according to their type
127        field = self.env['test_new_api.compute.onchange']._fields['baz']
128        self.assertTrue(field.store)
129        self.assertTrue(field.compute_sudo)
130        self.assertFalse(field.readonly)
131        self.assertTrue(field.copy)
132
133        field = self.env['test_new_api.compute.onchange']._fields['line_ids']
134        self.assertTrue(field.store)
135        self.assertTrue(field.compute_sudo)
136        self.assertFalse(field.readonly)
137        self.assertFalse(field.copy)  # like a regular one2many field
138
139        field = self.env['test_new_api.compute.onchange']._fields['tag_ids']
140        self.assertTrue(field.store)
141        self.assertTrue(field.compute_sudo)
142        self.assertFalse(field.readonly)
143        self.assertTrue(field.copy)  # like a regular many2many field
144
145    def test_10_computed_custom(self):
146        """ check definition of custom computed fields """
147        # Flush demo user before creating a new ir.model.fields to avoid
148        # a deadlock
149        self.user_demo.flush()
150        self.env['ir.model.fields'].create({
151            'name': 'x_bool_false_computed',
152            'model_id': self.env.ref('test_new_api.model_test_new_api_message').id,
153            'field_description': 'A boolean computed to false',
154            'compute': "for r in self: r['x_bool_false_computed'] = False",
155            'store': False,
156            'ttype': 'boolean'
157        })
158        field = self.env['test_new_api.message']._fields['x_bool_false_computed']
159        self.assertFalse(field.depends)
160
161    def test_10_computed_custom_invalid_transitive_depends(self):
162        self.patch(type(self.env["ir.model.fields"]), "_check_depends", lambda self: True)
163        self.env["ir.model.fields"].create(
164            {
165                "name": "x_computed_custom_valid_depends",
166                "model_id": self.env.ref("test_new_api.model_test_new_api_foo").id,
167                "state": "manual",
168                "field_description": "A compute with a valid depends",
169                "compute": "for r in self: r['x_computed_custom_valid_depends'] = False",
170                "depends": "value1",
171                "store": False,
172                "ttype": "boolean",
173            }
174        )
175        self.env["ir.model.fields"].create(
176            {
177                "name": "x_computed_custom_valid_transitive_depends",
178                "model_id": self.env.ref("test_new_api.model_test_new_api_foo").id,
179                "state": "manual",
180                "field_description": "A compute with a valid transitive depends",
181                "compute": "for r in self: r['x_computed_custom_valid_transitive_depends'] = False",
182                "depends": "x_computed_custom_valid_depends",
183                "store": False,
184                "ttype": "boolean",
185            }
186        )
187        self.env["ir.model.fields"].create(
188            {
189                "name": "x_computed_custom_invalid_depends",
190                "model_id": self.env.ref("test_new_api.model_test_new_api_foo").id,
191                "state": "manual",
192                "field_description": "A compute with an invalid depends",
193                "compute": "for r in self: r['x_computed_custom_invalid_depends'] = False",
194                "depends": "bar",
195                "store": False,
196                "ttype": "boolean",
197            }
198        )
199        self.env["ir.model.fields"].create(
200            {
201                "name": "x_computed_custom_invalid_transitive_depends",
202                "model_id": self.env.ref("test_new_api.model_test_new_api_foo").id,
203                "state": "manual",
204                "field_description": "A compute with an invalid transitive depends",
205                "compute": "for r in self: r['x_computed_custom_invalid_transitive_depends'] = False",
206                "depends": "x_computed_custom_invalid_depends",
207                "store": False,
208                "ttype": "boolean",
209            }
210        )
211        fields = self.env["test_new_api.foo"]._fields
212        triggers = self.env.registry.field_triggers
213        value1 = fields["value1"]
214        valid_depends = fields["x_computed_custom_valid_depends"]
215        valid_transitive_depends = fields["x_computed_custom_valid_transitive_depends"]
216        invalid_depends = fields["x_computed_custom_invalid_depends"]
217        invalid_transitive_depends = fields["x_computed_custom_invalid_transitive_depends"]
218        # `x_computed_custom_valid_depends` in the triggers of the field `value1`
219        self.assertTrue(valid_depends in triggers[value1][None])
220        # `x_computed_custom_valid_transitive_depends` in the triggers `x_computed_custom_valid_depends` and `value1`
221        self.assertTrue(valid_transitive_depends in triggers[valid_depends][None])
222        self.assertTrue(valid_transitive_depends in triggers[value1][None])
223        # `x_computed_custom_invalid_depends` not in any triggers, as it was invalid and was skipped
224        self.assertEqual(
225            sum(invalid_depends in field_triggers.get(None, []) for field_triggers in triggers.values()), 0
226        )
227        # `x_computed_custom_invalid_transitive_depends` in the triggers of `x_computed_custom_invalid_depends` only
228        self.assertTrue(invalid_transitive_depends in triggers[invalid_depends][None])
229        self.assertEqual(
230            sum(invalid_transitive_depends in field_triggers.get(None, []) for field_triggers in triggers.values()), 1
231        )
232
233    @mute_logger('odoo.fields')
234    def test_10_computed_stored_x_name(self):
235        # create a custom model with two fields
236        self.env["ir.model"].create({
237            "name": "x_test_10_compute_store_x_name",
238            "model": "x_test_10_compute_store_x_name",
239            "field_id": [
240                (0, 0, {'name': 'x_name', 'ttype': 'char'}),
241                (0, 0, {'name': 'x_stuff_id', 'ttype': 'many2one', 'relation': 'ir.model'}),
242            ],
243        })
244        # set 'x_stuff_id' refer to a model not loaded yet
245        self.cr.execute("""
246            UPDATE ir_model_fields
247            SET relation = 'not.loaded'
248            WHERE model = 'x_test_10_compute_store_x_name' AND name = 'x_stuff_id'
249        """)
250        # set 'x_name' be computed and depend on 'x_stuff_id'
251        self.cr.execute("""
252            UPDATE ir_model_fields
253            SET compute = 'pass', depends = 'x_stuff_id.x_custom_1'
254            WHERE model = 'x_test_10_compute_store_x_name' AND name = 'x_name'
255        """)
256        # setting up models should not crash
257        self.registry.setup_models(self.cr)
258
259    def test_10_display_name(self):
260        """ test definition of automatic field 'display_name' """
261        field = type(self.env['test_new_api.discussion']).display_name
262        self.assertTrue(field.automatic)
263        self.assertTrue(field.compute)
264        self.assertEqual(field.depends, ('name',))
265
266    def test_10_non_stored(self):
267        """ test non-stored fields """
268        # a field declared with store=False should not have a column
269        field = self.env['test_new_api.category']._fields['dummy']
270        self.assertFalse(field.store)
271        self.assertFalse(field.compute)
272        self.assertFalse(field.inverse)
273
274        # find messages
275        for message in self.env['test_new_api.message'].search([]):
276            # check definition of field
277            self.assertEqual(message.size, len(message.body or ''))
278
279            # check recomputation after record is modified
280            size = message.size
281            message.write({'body': (message.body or '') + "!!!"})
282            self.assertEqual(message.size, size + 3)
283
284        # create a message, assign body, and check size in several environments
285        message1 = self.env['test_new_api.message'].create({})
286        message2 = message1.with_user(self.user_demo)
287        self.assertEqual(message1.size, 0)
288        self.assertEqual(message2.size, 0)
289
290        message1.write({'body': "XXX"})
291        self.assertEqual(message1.size, 3)
292        self.assertEqual(message2.size, 3)
293
294        # special case: computed field without dependency must be computed
295        record = self.env['test_new_api.mixed'].create({})
296        self.assertTrue(record.now)
297
298    def test_11_stored(self):
299        """ test stored fields """
300        def check_stored(disc):
301            """ Check the stored computed field on disc.messages """
302            for msg in disc.messages:
303                self.assertEqual(msg.name, "[%s] %s" % (disc.name, msg.author.name))
304
305        # find the demo discussion, and check messages
306        discussion1 = self.env.ref('test_new_api.discussion_0')
307        self.assertTrue(discussion1.messages)
308        check_stored(discussion1)
309
310        # modify discussion name, and check again messages
311        discussion1.name = 'Talking about stuff...'
312        check_stored(discussion1)
313
314        # switch message from discussion, and check again
315
316        # See YTI FIXME
317        discussion1.invalidate_cache()
318
319        discussion2 = discussion1.copy({'name': 'Another discussion'})
320        message2 = discussion1.messages[0]
321        message2.discussion = discussion2
322        check_stored(discussion2)
323
324        # create a new discussion with messages, and check their name
325        user_root = self.env.ref('base.user_root')
326        user_demo = self.user_demo
327        discussion3 = self.env['test_new_api.discussion'].create({
328            'name': 'Stuff',
329            'participants': [(4, user_root.id), (4, user_demo.id)],
330            'messages': [
331                (0, 0, {'author': user_root.id, 'body': 'one'}),
332                (0, 0, {'author': user_demo.id, 'body': 'two'}),
333                (0, 0, {'author': user_root.id, 'body': 'three'}),
334            ],
335        })
336        check_stored(discussion3)
337
338        # modify the discussion messages: edit the 2nd one, remove the last one
339        # (keep modifications in that order, as they reproduce a former bug!)
340        discussion3.write({
341            'messages': [
342                (4, discussion3.messages[0].id),
343                (1, discussion3.messages[1].id, {'author': user_root.id}),
344                (2, discussion3.messages[2].id),
345            ],
346        })
347        check_stored(discussion3)
348
349    def test_11_stored_protected(self):
350        """ test protection against recomputation """
351        model = self.env['test_new_api.compute.readonly']
352        field = model._fields['bar']
353
354        record = model.create({'foo': 'unprotected #1'})
355        self.assertEqual(record.bar, 'unprotected #1')
356
357        record.write({'foo': 'unprotected #2'})
358        self.assertEqual(record.bar, 'unprotected #2')
359
360        # by protecting 'bar', we prevent it from being recomputed
361        with self.env.protecting([field], record):
362            record.write({'foo': 'protected'})
363            self.assertEqual(record.bar, 'unprotected #2')
364
365            # also works when nested
366            with self.env.protecting([field], record):
367                record.write({'foo': 'protected'})
368                self.assertEqual(record.bar, 'unprotected #2')
369
370            record.write({'foo': 'protected'})
371            self.assertEqual(record.bar, 'unprotected #2')
372
373        record.write({'foo': 'unprotected #3'})
374        self.assertEqual(record.bar, 'unprotected #3')
375
376        # also works with duplicated fields
377        with self.env.protecting([field, field], record):
378            record.write({'foo': 'protected'})
379            self.assertEqual(record.bar, 'unprotected #3')
380
381        record.write({'foo': 'unprotected #4'})
382        self.assertEqual(record.bar, 'unprotected #4')
383
384        # we protect 'bar' on a different record
385        with self.env.protecting([field], record):
386            record2 = model.create({'foo': 'unprotected'})
387            self.assertEqual(record2.bar, 'unprotected')
388
389    def test_11_computed_access(self):
390        """ test computed fields with access right errors """
391        User = self.env['res.users']
392        user1 = User.create({'name': 'Aaaah', 'login': 'a'})
393        user2 = User.create({'name': 'Boooh', 'login': 'b'})
394        user3 = User.create({'name': 'Crrrr', 'login': 'c'})
395        # add a rule to not give access to user2
396        self.env['ir.rule'].create({
397            'model_id': self.env['ir.model'].search([('model', '=', 'res.users')]).id,
398            'domain_force': "[('id', '!=', %d)]" % user2.id,
399        })
400        # DLE P72: Since we decided that we do not raise security access errors for data to which we had the occassion
401        # to put the value in the cache, we need to invalidate the cache for user1, user2 and user3 in order
402        # to test the below access error. Otherwise the above create calls set in the cache the information needed
403        # to compute `company_type` ('is_company'), and doesn't need to trigger a read.
404        # We need to force the read in order to test the security access
405        User.invalidate_cache()
406        # group users as a recordset, and read them as user demo
407        users = (user1 + user2 + user3).with_user(self.user_demo)
408        user1, user2, user3 = users
409        # regression test: a bug invalidated the field's value from cache
410        user1.company_type
411        with self.assertRaises(AccessError):
412            user2.company_type
413        user3.company_type
414
415    def test_12_recursive(self):
416        """ test recursively dependent fields """
417        Category = self.env['test_new_api.category']
418        abel = Category.create({'name': 'Abel'})
419        beth = Category.create({'name': 'Bethany'})
420        cath = Category.create({'name': 'Catherine'})
421        dean = Category.create({'name': 'Dean'})
422        ewan = Category.create({'name': 'Ewan'})
423        finn = Category.create({'name': 'Finnley'})
424        gabe = Category.create({'name': 'Gabriel'})
425
426        cath.parent = finn.parent = gabe
427        abel.parent = beth.parent = cath
428        dean.parent = ewan.parent = finn
429
430        self.assertEqual(abel.display_name, "Gabriel / Catherine / Abel")
431        self.assertEqual(beth.display_name, "Gabriel / Catherine / Bethany")
432        self.assertEqual(cath.display_name, "Gabriel / Catherine")
433        self.assertEqual(dean.display_name, "Gabriel / Finnley / Dean")
434        self.assertEqual(ewan.display_name, "Gabriel / Finnley / Ewan")
435        self.assertEqual(finn.display_name, "Gabriel / Finnley")
436        self.assertEqual(gabe.display_name, "Gabriel")
437
438        ewan.parent = cath
439        self.assertEqual(ewan.display_name, "Gabriel / Catherine / Ewan")
440
441        cath.parent = finn
442        self.assertEqual(ewan.display_name, "Gabriel / Finnley / Catherine / Ewan")
443
444    def test_12_recursive_recompute(self):
445        """ test recomputation on recursively dependent field """
446        a = self.env['test_new_api.recursive'].create({'name': 'A'})
447        b = self.env['test_new_api.recursive'].create({'name': 'B', 'parent': a.id})
448        c = self.env['test_new_api.recursive'].create({'name': 'C', 'parent': b.id})
449        d = self.env['test_new_api.recursive'].create({'name': 'D', 'parent': c.id})
450        self.assertEqual(a.full_name, 'A')
451        self.assertEqual(b.full_name, 'A / B')
452        self.assertEqual(c.full_name, 'A / B / C')
453        self.assertEqual(d.full_name, 'A / B / C / D')
454        self.assertEqual(a.display_name, 'A')
455        self.assertEqual(b.display_name, 'A / B')
456        self.assertEqual(c.display_name, 'A / B / C')
457        self.assertEqual(d.display_name, 'A / B / C / D')
458
459        a.name = 'A1'
460        self.assertEqual(a.full_name, 'A1')
461        self.assertEqual(b.full_name, 'A1 / B')
462        self.assertEqual(c.full_name, 'A1 / B / C')
463        self.assertEqual(d.full_name, 'A1 / B / C / D')
464        self.assertEqual(a.display_name, 'A1')
465        self.assertEqual(b.display_name, 'A1 / B')
466        self.assertEqual(c.display_name, 'A1 / B / C')
467        self.assertEqual(d.display_name, 'A1 / B / C / D')
468
469        b.parent = False
470        self.assertEqual(a.full_name, 'A1')
471        self.assertEqual(b.full_name, 'B')
472        self.assertEqual(c.full_name, 'B / C')
473        self.assertEqual(d.full_name, 'B / C / D')
474        self.assertEqual(a.display_name, 'A1')
475        self.assertEqual(b.display_name, 'B')
476        self.assertEqual(c.display_name, 'B / C')
477        self.assertEqual(d.display_name, 'B / C / D')
478
479        # rename several records to trigger several recomputations at once
480        (d + c + b).write({'name': 'X'})
481        self.assertEqual(a.full_name, 'A1')
482        self.assertEqual(b.full_name, 'X')
483        self.assertEqual(c.full_name, 'X / X')
484        self.assertEqual(d.full_name, 'X / X / X')
485        self.assertEqual(a.display_name, 'A1')
486        self.assertEqual(b.display_name, 'X')
487        self.assertEqual(c.display_name, 'X / X')
488        self.assertEqual(d.display_name, 'X / X / X')
489
490        # delete b; both c and d are deleted in cascade; c should also be marked
491        # to recompute, but recomputation should not fail...
492        b.unlink()
493        self.assertEqual((a + b + c + d).exists(), a)
494
495    def test_12_recursive_tree(self):
496        foo = self.env['test_new_api.recursive.tree'].create({'name': 'foo'})
497        self.assertEqual(foo.display_name, 'foo()')
498        bar = foo.create({'name': 'bar', 'parent_id': foo.id})
499        self.assertEqual(foo.display_name, 'foo(bar())')
500        baz = foo.create({'name': 'baz', 'parent_id': bar.id})
501        self.assertEqual(foo.display_name, 'foo(bar(baz()))')
502
503    def test_12_cascade(self):
504        """ test computed field depending on computed field """
505        message = self.env.ref('test_new_api.message_0_0')
506        message.invalidate_cache()
507        double_size = message.double_size
508        self.assertEqual(double_size, message.size)
509
510        record = self.env['test_new_api.cascade'].create({'foo': "Hi"})
511        self.assertEqual(record.baz, "<[Hi]>")
512        record.foo = "Ho"
513        self.assertEqual(record.baz, "<[Ho]>")
514
515    def test_12_dynamic_depends(self):
516        Model = self.registry['test_new_api.compute.dynamic.depends']
517        self.assertEqual(Model.full_name.depends, ())
518
519        # the dependencies of full_name are stored in a config parameter
520        self.env['ir.config_parameter'].set_param('test_new_api.full_name', 'name1,name2')
521
522        # this must re-evaluate the field's dependencies
523        self.env['base'].flush()
524        self.registry.setup_models(self.cr)
525        self.assertEqual(Model.full_name.depends, ('name1', 'name2'))
526
527    def test_13_inverse(self):
528        """ test inverse computation of fields """
529        Category = self.env['test_new_api.category']
530        abel = Category.create({'name': 'Abel'})
531        beth = Category.create({'name': 'Bethany'})
532        cath = Category.create({'name': 'Catherine'})
533        dean = Category.create({'name': 'Dean'})
534        ewan = Category.create({'name': 'Ewan'})
535        finn = Category.create({'name': 'Finnley'})
536        gabe = Category.create({'name': 'Gabriel'})
537        self.assertEqual(ewan.display_name, "Ewan")
538
539        ewan.display_name = "Abel / Bethany / Catherine / Erwan"
540
541        self.assertEqual(beth.parent, abel)
542        self.assertEqual(cath.parent, beth)
543        self.assertEqual(ewan.parent, cath)
544        self.assertEqual(ewan.name, "Erwan")
545
546        # check create/write with several records
547        vals = {'name': 'None', 'display_name': 'Foo'}
548        foo1, foo2 = Category.create([vals, vals])
549        self.assertEqual(foo1.name, 'Foo')
550        self.assertEqual(foo2.name, 'Foo')
551
552        (foo1 + foo2).write({'display_name': 'Bar'})
553        self.assertEqual(foo1.name, 'Bar')
554        self.assertEqual(foo2.name, 'Bar')
555
556        # create/write on 'foo' should only invoke the compute method
557        log = []
558        model = self.env['test_new_api.compute.inverse'].with_context(log=log)
559        record = model.create({'foo': 'Hi'})
560        self.assertEqual(record.foo, 'Hi')
561        self.assertEqual(record.bar, 'Hi')
562        self.assertCountEqual(log, ['compute'])
563
564        log.clear()
565        record.write({'foo': 'Ho'})
566        self.assertEqual(record.foo, 'Ho')
567        self.assertEqual(record.bar, 'Ho')
568        self.assertCountEqual(log, ['compute'])
569
570        # create/write on 'bar' should only invoke the inverse method
571        log.clear()
572        record = model.create({'bar': 'Hi'})
573        self.assertEqual(record.foo, 'Hi')
574        self.assertEqual(record.bar, 'Hi')
575        self.assertCountEqual(log, ['inverse'])
576
577        log.clear()
578        record.write({'bar': 'Ho'})
579        self.assertEqual(record.foo, 'Ho')
580        self.assertEqual(record.bar, 'Ho')
581        self.assertCountEqual(log, ['inverse'])
582
583        # Test compatibility multiple compute/inverse fields
584        log = []
585        model = self.env['test_new_api.multi_compute_inverse'].with_context(log=log)
586        record = model.create({
587            'bar1': '1',
588            'bar2': '2',
589            'bar3': '3',
590        })
591        self.assertEqual(record.foo, '1/2/3')
592        self.assertEqual(record.bar1, '1')
593        self.assertEqual(record.bar2, '2')
594        self.assertEqual(record.bar3, '3')
595        self.assertCountEqual(log, ['inverse1', 'inverse23'])
596
597        log.clear()
598        record.write({'bar2': '4', 'bar3': '5'})
599        self.assertEqual(record.foo, '1/4/5')
600        self.assertEqual(record.bar1, '1')
601        self.assertEqual(record.bar2, '4')
602        self.assertEqual(record.bar3, '5')
603        self.assertCountEqual(log, ['inverse23'])
604
605        log.clear()
606        record.write({'bar1': '6', 'bar2': '7'})
607        self.assertEqual(record.foo, '6/7/5')
608        self.assertEqual(record.bar1, '6')
609        self.assertEqual(record.bar2, '7')
610        self.assertEqual(record.bar3, '5')
611        self.assertCountEqual(log, ['inverse1', 'inverse23'])
612
613        log.clear()
614        record.write({'foo': 'A/B/C'})
615        self.assertEqual(record.foo, 'A/B/C')
616        self.assertEqual(record.bar1, 'A')
617        self.assertEqual(record.bar2, 'B')
618        self.assertEqual(record.bar3, 'C')
619        self.assertCountEqual(log, ['compute'])
620
621    def test_13_inverse_access(self):
622        """ test access rights on inverse fields """
623        foo = self.env['test_new_api.category'].create({'name': 'Foo'})
624        user = self.env['res.users'].create({'name': 'Foo', 'login': 'foo'})
625        self.assertFalse(user.has_group('base.group_system'))
626        # add group on non-stored inverse field
627        self.patch(type(foo).display_name, 'groups', 'base.group_system')
628        with self.assertRaises(AccessError):
629            foo.with_user(user).display_name = 'Forbidden'
630
631    def test_13_inverse_with_unlink(self):
632        """ test x2many delete command combined with an inverse field """
633        country1 = self.env['res.country'].create({'name': 'test country'})
634        country2 = self.env['res.country'].create({'name': 'other country'})
635        company = self.env['res.company'].create({
636            'name': 'test company',
637            'child_ids': [
638                (0, 0, {'name': 'Child Company 1'}),
639                (0, 0, {'name': 'Child Company 2'}),
640            ]
641        })
642        child_company = company.child_ids[0]
643
644        # check first that the field has an inverse and is not stored
645        field = type(company).country_id
646        self.assertFalse(field.store)
647        self.assertTrue(field.inverse)
648
649        company.write({'country_id': country1.id})
650        self.assertEqual(company.country_id, country1)
651
652        company.write({
653            'country_id': country2.id,
654            'child_ids': [(2, child_company.id)],
655        })
656        self.assertEqual(company.country_id, country2)
657
658    def test_14_search(self):
659        """ test search on computed fields """
660        discussion = self.env.ref('test_new_api.discussion_0')
661
662        # determine message sizes
663        sizes = set(message.size for message in discussion.messages)
664
665        # search for messages based on their size
666        for size in sizes:
667            messages0 = self.env['test_new_api.message'].search(
668                [('discussion', '=', discussion.id), ('size', '<=', size)])
669
670            messages1 = self.env['test_new_api.message'].browse()
671            for message in discussion.messages:
672                if message.size <= size:
673                    messages1 += message
674
675            self.assertEqual(messages0, messages1)
676
677    def test_15_constraint(self):
678        """ test new-style Python constraints """
679        discussion = self.env.ref('test_new_api.discussion_0')
680        discussion.flush()
681
682        # remove oneself from discussion participants: we can no longer create
683        # messages in discussion
684        discussion.participants -= self.env.user
685        with self.assertRaises(ValidationError):
686            self.env['test_new_api.message'].create({'discussion': discussion.id, 'body': 'Whatever'})
687
688        # make sure that assertRaises() does not leave fields to recompute
689        self.assertFalse(self.env.fields_to_compute())
690
691        # put back oneself into discussion participants: now we can create
692        # messages in discussion
693        discussion.participants += self.env.user
694        self.env['test_new_api.message'].create({'discussion': discussion.id, 'body': 'Whatever'})
695
696        # check constraint on recomputed field
697        self.assertTrue(discussion.messages)
698        with self.assertRaises(ValidationError):
699            discussion.name = "X"
700            discussion.flush()
701
702    def test_15_constraint_inverse(self):
703        """ test constraint method on normal field and field with inverse """
704        log = []
705        model = self.env['test_new_api.compute.inverse'].with_context(log=log, log_constraint=True)
706
707        # create/write with normal field only
708        log.clear()
709        record = model.create({'baz': 'Hi'})
710        self.assertCountEqual(log, ['constraint'])
711
712        log.clear()
713        record.write({'baz': 'Ho'})
714        self.assertCountEqual(log, ['constraint'])
715
716        # create/write with inverse field only
717        log.clear()
718        record = model.create({'bar': 'Hi'})
719        self.assertCountEqual(log, ['inverse', 'constraint'])
720
721        log.clear()
722        record.write({'bar': 'Ho'})
723        self.assertCountEqual(log, ['inverse', 'constraint'])
724
725        # create/write with both fields only
726        log.clear()
727        record = model.create({'bar': 'Hi', 'baz': 'Hi'})
728        self.assertCountEqual(log, ['inverse', 'constraint'])
729
730        log.clear()
731        record.write({'bar': 'Ho', 'baz': 'Ho'})
732        self.assertCountEqual(log, ['inverse', 'constraint'])
733
734    def test_16_compute_unassigned(self):
735        model = self.env['test_new_api.compute.unassigned']
736
737        # real record
738        record = model.create({})
739        with self.assertRaises(ValueError):
740            record.bar
741        self.assertEqual(record.bare, False)
742        self.assertEqual(record.bars, False)
743        self.assertEqual(record.bares, False)
744
745        # new record
746        record = model.new()
747        with self.assertRaises(ValueError):
748            record.bar
749        self.assertEqual(record.bare, False)
750        self.assertEqual(record.bars, False)
751        self.assertEqual(record.bares, False)
752
753    def test_16_compute_unassigned_access_error(self):
754        # create two records
755        records = self.env['test_new_api.compute.unassigned'].create([{}, {}])
756        records.flush()
757
758        # alter access rights: regular users cannot read 'records'
759        access = self.env.ref('test_new_api.access_test_new_api_compute_unassigned')
760        access.perm_read = False
761        access.flush()
762
763        # switch to environment with user demo
764        records = records.with_user(self.user_demo)
765        records.env.cache.invalidate()
766
767        # check that records are not accessible
768        with self.assertRaises(AccessError):
769            records[0].bars
770        with self.assertRaises(AccessError):
771            records[1].bars
772
773        # Modify the records and flush() changes with the current environment:
774        # this should not trigger an access error, whatever the order in which
775        # records are considered.  It may fail in the following scenario:
776        #  - mark field 'bars' to compute on records
777        #  - access records[0].bars
778        #     - recompute bars on records (both) -> assign records[0] only
779        #     - return records[0].bars from cache
780        #  - access records[1].bars
781        #     - recompute nothing (done already)
782        #     - records[1].bars is not in cache
783        #     - fetch records[1].bars -> access error
784        records[0].foo = "assign"
785        records[1].foo = "x"
786        records.flush()
787
788        # try the other way around, too
789        records.env.cache.invalidate()
790        records[0].foo = "x"
791        records[1].foo = "assign"
792        records.flush()
793
794    def test_20_float(self):
795        """ test rounding of float fields """
796        record = self.env['test_new_api.mixed'].create({})
797        query = "SELECT 1 FROM test_new_api_mixed WHERE id=%s AND number=%s"
798
799        # 2.49609375 (exact float) must be rounded to 2.5
800        record.write({'number': 2.49609375})
801        record.flush()
802        self.cr.execute(query, [record.id, '2.5'])
803        self.assertTrue(self.cr.rowcount)
804        self.assertEqual(record.number, 2.5)
805
806        # 1.1 (1.1000000000000000888178420 in float) must be 1.1 in database
807        record.write({'number': 1.1})
808        record.flush()
809        self.cr.execute(query, [record.id, '1.1'])
810        self.assertTrue(self.cr.rowcount)
811        self.assertEqual(record.number, 1.1)
812
813    def test_21_float_digits(self):
814        """ test field description """
815        precision = self.env.ref('test_new_api.decimal_new_api_number')
816        description = self.env['test_new_api.mixed'].fields_get()['number2']
817        self.assertEqual(description['digits'], (16, precision.digits))
818
819    def check_monetary(self, record, amount, currency, msg=None):
820        # determine the possible roundings of amount
821        if currency:
822            ramount = currency.round(amount)
823            samount = float(float_repr(ramount, currency.decimal_places))
824        else:
825            ramount = samount = amount
826
827        # check the currency on record
828        self.assertEqual(record.currency_id, currency)
829
830        # check the value on the record
831        self.assertIn(record.amount, [ramount, samount], msg)
832
833        # check the value in the database
834        record.flush()
835        self.cr.execute('SELECT amount FROM test_new_api_mixed WHERE id=%s', [record.id])
836        value = self.cr.fetchone()[0]
837        self.assertEqual(value, samount, msg)
838
839    def test_20_monetary(self):
840        """ test monetary fields """
841        model = self.env['test_new_api.mixed']
842        currency = self.env['res.currency'].with_context(active_test=False)
843        amount = 14.70126
844
845        for rounding in [0.01, 0.0001, 1.0, 0]:
846            # first retrieve a currency corresponding to rounding
847            if rounding:
848                currency = currency.search([('rounding', '=', rounding)], limit=1)
849                self.assertTrue(currency, "No currency found for rounding %s" % rounding)
850            else:
851                # rounding=0 corresponds to currency=False
852                currency = currency.browse()
853
854            # case 1: create with amount and currency
855            record = model.create({'amount': amount, 'currency_id': currency.id})
856            self.check_monetary(record, amount, currency, 'create(amount, currency)')
857
858            # case 2: assign amount
859            record.amount = 0
860            record.amount = amount
861            self.check_monetary(record, amount, currency, 'assign(amount)')
862
863            # case 3: write with amount and currency
864            record.write({'amount': 0, 'currency_id': False})
865            record.write({'amount': amount, 'currency_id': currency.id})
866            self.check_monetary(record, amount, currency, 'write(amount, currency)')
867
868            # case 4: write with amount only
869            record.write({'amount': 0})
870            record.write({'amount': amount})
871            self.check_monetary(record, amount, currency, 'write(amount)')
872
873            # case 5: write with amount on several records
874            records = record + model.create({'currency_id': currency.id})
875            records.write({'amount': 0})
876            records.write({'amount': amount})
877            for record in records:
878                self.check_monetary(record, amount, currency, 'multi write(amount)')
879
880    def test_20_monetary_opw_2223134(self):
881        """ test monetary fields with cache override """
882        model = self.env['test_new_api.monetary_order']
883        currency = self.env.ref('base.USD')
884
885        def check(value):
886            self.assertEqual(record.total, value)
887            record.flush()
888            self.cr.execute('SELECT total FROM test_new_api_monetary_order WHERE id=%s', [record.id])
889            [total] = self.cr.fetchone()
890            self.assertEqual(total, value)
891
892        # create, and compute amount
893        record = model.create({
894            'currency_id': currency.id,
895            'line_ids': [(0, 0, {'subtotal': 1.0})],
896        })
897        check(1.0)
898
899        # delete and add a line: the deletion of the line clears the cache, then
900        # the recomputation of 'total' must prefetch record.currency_id without
901        # screwing up the new value in cache
902        record.write({
903            'line_ids': [(2, record.line_ids.id), (0, 0, {'subtotal': 1.0})],
904        })
905        check(1.0)
906
907    def test_20_like(self):
908        """ test filtered_domain() on char fields. """
909        record = self.env['test_new_api.multi.tag'].create({'name': 'Foo'})
910        self.assertTrue(record.filtered_domain([('name', 'like', 'F')]))
911        self.assertTrue(record.filtered_domain([('name', 'ilike', 'f')]))
912
913        record.name = 'Bar'
914        self.assertFalse(record.filtered_domain([('name', 'like', 'F')]))
915        self.assertFalse(record.filtered_domain([('name', 'ilike', 'f')]))
916
917        record.name = False
918        self.assertFalse(record.filtered_domain([('name', 'like', 'F')]))
919        self.assertFalse(record.filtered_domain([('name', 'ilike', 'f')]))
920
921    def test_21_date(self):
922        """ test date fields """
923        record = self.env['test_new_api.mixed'].create({})
924
925        # one may assign False or None
926        record.date = None
927        self.assertFalse(record.date)
928
929        # one may assign date but not datetime objects
930        record.date = date(2012, 5, 1)
931        self.assertEqual(record.date, date(2012, 5, 1))
932
933        # DLE P41: We now support to assign datetime to date. Not sure this is the good practice though.
934        # with self.assertRaises(TypeError):
935        #     record.date = datetime(2012, 5, 1, 10, 45, 0)
936
937        # one may assign dates and datetime in the default format, and it must be checked
938        record.date = '2012-05-01'
939        self.assertEqual(record.date, date(2012, 5, 1))
940
941        record.date = "2012-05-01 10:45:00"
942        self.assertEqual(record.date, date(2012, 5, 1))
943
944        with self.assertRaises(ValueError):
945            record.date = '12-5-1'
946
947        # check filtered_domain
948        self.assertTrue(record.filtered_domain([('date', '<', '2012-05-02')]))
949        self.assertTrue(record.filtered_domain([('date', '<', date(2012, 5, 2))]))
950        self.assertTrue(record.filtered_domain([('date', '<', datetime(2012, 5, 2, 12, 0, 0))]))
951        self.assertTrue(record.filtered_domain([('date', '!=', False)]))
952        self.assertFalse(record.filtered_domain([('date', '=', False)]))
953
954        record.date = None
955        self.assertFalse(record.filtered_domain([('date', '<', '2012-05-02')]))
956        self.assertFalse(record.filtered_domain([('date', '<', date(2012, 5, 2))]))
957        self.assertFalse(record.filtered_domain([('date', '<', datetime(2012, 5, 2, 12, 0, 0))]))
958        self.assertFalse(record.filtered_domain([('date', '!=', False)]))
959        self.assertTrue(record.filtered_domain([('date', '=', False)]))
960
961    def test_21_datetime(self):
962        """ test datetime fields """
963        for i in range(0, 10):
964            self.assertEqual(fields.Datetime.now().microsecond, 0)
965
966        record = self.env['test_new_api.mixed'].create({})
967
968        # assign falsy value
969        record.moment = None
970        self.assertFalse(record.moment)
971
972        # assign string
973        record.moment = '2012-05-01'
974        self.assertEqual(record.moment, datetime(2012, 5, 1))
975        record.moment = '2012-05-01 06:00:00'
976        self.assertEqual(record.moment, datetime(2012, 5, 1, 6))
977        with self.assertRaises(ValueError):
978            record.moment = '12-5-1'
979
980        # assign date or datetime
981        record.moment = date(2012, 5, 1)
982        self.assertEqual(record.moment, datetime(2012, 5, 1))
983        record.moment = datetime(2012, 5, 1, 6)
984        self.assertEqual(record.moment, datetime(2012, 5, 1, 6))
985
986        # check filtered_domain
987        self.assertTrue(record.filtered_domain([('moment', '<', '2012-05-02')]))
988        self.assertTrue(record.filtered_domain([('moment', '<', date(2012, 5, 2))]))
989        self.assertTrue(record.filtered_domain([('moment', '<', datetime(2012, 5, 1, 12, 0, 0))]))
990        self.assertTrue(record.filtered_domain([('moment', '!=', False)]))
991        self.assertFalse(record.filtered_domain([('moment', '=', False)]))
992
993        record.moment = None
994        self.assertFalse(record.filtered_domain([('moment', '<', '2012-05-02')]))
995        self.assertFalse(record.filtered_domain([('moment', '<', date(2012, 5, 2))]))
996        self.assertFalse(record.filtered_domain([('moment', '<', datetime(2012, 5, 2, 12, 0, 0))]))
997        self.assertFalse(record.filtered_domain([('moment', '!=', False)]))
998        self.assertTrue(record.filtered_domain([('moment', '=', False)]))
999
1000    def test_21_date_datetime_helpers(self):
1001        """ test date/datetime fields helpers """
1002        _date = fields.Date.from_string("2077-10-23")
1003        _datetime = fields.Datetime.from_string("2077-10-23 09:42:00")
1004
1005        # addition
1006        self.assertEqual(add(_date, days=5), date(2077, 10, 28))
1007        self.assertEqual(add(_datetime, seconds=10), datetime(2077, 10, 23, 9, 42, 10))
1008
1009        # subtraction
1010        self.assertEqual(subtract(_date, months=1), date(2077, 9, 23))
1011        self.assertEqual(subtract(_datetime, hours=2), datetime(2077, 10, 23, 7, 42, 0))
1012
1013        # start_of
1014        # year
1015        self.assertEqual(start_of(_date, 'year'), date(2077, 1, 1))
1016        self.assertEqual(start_of(_datetime, 'year'), datetime(2077, 1, 1))
1017
1018        # quarter
1019        q1 = date(2077, 1, 1)
1020        q2 = date(2077, 4, 1)
1021        q3 = date(2077, 7, 1)
1022        q4 = date(2077, 10, 1)
1023        self.assertEqual(start_of(_date.replace(month=3), 'quarter'), q1)
1024        self.assertEqual(start_of(_date.replace(month=5), 'quarter'), q2)
1025        self.assertEqual(start_of(_date.replace(month=7), 'quarter'), q3)
1026        self.assertEqual(start_of(_date, 'quarter'), q4)
1027        self.assertEqual(start_of(_datetime, 'quarter'), datetime.combine(q4, time.min))
1028
1029        # month
1030        self.assertEqual(start_of(_date, 'month'), date(2077, 10, 1))
1031        self.assertEqual(start_of(_datetime, 'month'), datetime(2077, 10, 1))
1032
1033        # week
1034        self.assertEqual(start_of(_date, 'week'), date(2077, 10, 18))
1035        self.assertEqual(start_of(_datetime, 'week'), datetime(2077, 10, 18))
1036
1037        # day
1038        self.assertEqual(start_of(_date, 'day'), _date)
1039        self.assertEqual(start_of(_datetime, 'day'), _datetime.replace(hour=0, minute=0, second=0))
1040
1041        # hour
1042        with self.assertRaises(ValueError):
1043            start_of(_date, 'hour')
1044        self.assertEqual(start_of(_datetime, 'hour'), _datetime.replace(minute=0, second=0))
1045
1046        # invalid
1047        with self.assertRaises(ValueError):
1048            start_of(_datetime, 'poop')
1049
1050        # end_of
1051        # year
1052        self.assertEqual(end_of(_date, 'year'), _date.replace(month=12, day=31))
1053        self.assertEqual(end_of(_datetime, 'year'),
1054                         datetime.combine(_date.replace(month=12, day=31), time.max))
1055
1056        # quarter
1057        q1 = date(2077, 3, 31)
1058        q2 = date(2077, 6, 30)
1059        q3 = date(2077, 9, 30)
1060        q4 = date(2077, 12, 31)
1061        self.assertEqual(end_of(_date.replace(month=2), 'quarter'), q1)
1062        self.assertEqual(end_of(_date.replace(month=4), 'quarter'), q2)
1063        self.assertEqual(end_of(_date.replace(month=9), 'quarter'), q3)
1064        self.assertEqual(end_of(_date, 'quarter'), q4)
1065        self.assertEqual(end_of(_datetime, 'quarter'), datetime.combine(q4, time.max))
1066
1067        # month
1068        self.assertEqual(end_of(_date, 'month'), _date.replace(day=31))
1069        self.assertEqual(end_of(_datetime, 'month'),
1070                         datetime.combine(date(2077, 10, 31), time.max))
1071
1072        # week
1073        self.assertEqual(end_of(_date, 'week'), date(2077, 10, 24))
1074        self.assertEqual(end_of(_datetime, 'week'),
1075                         datetime.combine(datetime(2077, 10, 24), time.max))
1076
1077        # day
1078        self.assertEqual(end_of(_date, 'day'), _date)
1079        self.assertEqual(end_of(_datetime, 'day'), datetime.combine(_datetime, time.max))
1080
1081        # hour
1082        with self.assertRaises(ValueError):
1083            end_of(_date, 'hour')
1084        self.assertEqual(end_of(_datetime, 'hour'),
1085                         datetime.combine(_datetime, time.max).replace(hour=_datetime.hour))
1086
1087        # invalid
1088        with self.assertRaises(ValueError):
1089            end_of(_datetime, 'crap')
1090
1091    def test_22_selection(self):
1092        """ test selection fields """
1093        record = self.env['test_new_api.mixed'].create({})
1094
1095        # one may assign False or None
1096        record.lang = None
1097        self.assertFalse(record.lang)
1098
1099        # one may assign a value, and it must be checked
1100        for language in self.env['res.lang'].search([]):
1101            record.lang = language.code
1102        with self.assertRaises(ValueError):
1103            record.lang = 'zz_ZZ'
1104
1105    def test_23_relation(self):
1106        """ test relation fields """
1107        demo = self.user_demo
1108        message = self.env.ref('test_new_api.message_0_0')
1109
1110        # check environment of record and related records
1111        self.assertEqual(message.env, self.env)
1112        self.assertEqual(message.discussion.env, self.env)
1113
1114        demo_env = self.env(user=demo)
1115        self.assertNotEqual(demo_env, self.env)
1116
1117        # check environment of record and related records
1118        self.assertEqual(message.env, self.env)
1119        self.assertEqual(message.discussion.env, self.env)
1120
1121        # "migrate" message into demo_env, and check again
1122        demo_message = message.with_user(demo)
1123        self.assertEqual(demo_message.env, demo_env)
1124        self.assertEqual(demo_message.discussion.env, demo_env)
1125
1126        # See YTI FIXME
1127        message.discussion.invalidate_cache()
1128
1129        # assign record's parent to a record in demo_env
1130        message.discussion = message.discussion.copy({'name': 'Copy'})
1131
1132        # both message and its parent field must be in self.env
1133        self.assertEqual(message.env, self.env)
1134        self.assertEqual(message.discussion.env, self.env)
1135
1136    def test_24_reference(self):
1137        """ test reference fields. """
1138        record = self.env['test_new_api.mixed'].create({})
1139
1140        # one may assign False or None
1141        record.reference = None
1142        self.assertFalse(record.reference)
1143
1144        # one may assign a user or a partner...
1145        record.reference = self.env.user
1146        self.assertEqual(record.reference, self.env.user)
1147        record.reference = self.env.user.partner_id
1148        self.assertEqual(record.reference, self.env.user.partner_id)
1149        # ... but no record from a model that starts with 'ir.'
1150        with self.assertRaises(ValueError):
1151            record.reference = self.env['ir.model'].search([], limit=1)
1152
1153    def test_25_related(self):
1154        """ test related fields. """
1155        message = self.env.ref('test_new_api.message_0_0')
1156        discussion = message.discussion
1157
1158        # by default related fields are not stored
1159        field = message._fields['discussion_name']
1160        self.assertFalse(field.store)
1161        self.assertFalse(field.readonly)
1162
1163        # check value of related field
1164        self.assertEqual(message.discussion_name, discussion.name)
1165
1166        # change discussion name, and check result
1167        discussion.name = 'Foo'
1168        self.assertEqual(message.discussion_name, 'Foo')
1169
1170        # change discussion name via related field, and check result
1171        message.discussion_name = 'Bar'
1172        self.assertEqual(discussion.name, 'Bar')
1173        self.assertEqual(message.discussion_name, 'Bar')
1174
1175        # change discussion name via related field on several records
1176        discussion1 = discussion.create({'name': 'X1'})
1177        discussion2 = discussion.create({'name': 'X2'})
1178        discussion1.participants = discussion2.participants = self.env.user
1179        message1 = message.create({'discussion': discussion1.id})
1180        message2 = message.create({'discussion': discussion2.id})
1181        self.assertEqual(message1.discussion_name, 'X1')
1182        self.assertEqual(message2.discussion_name, 'X2')
1183
1184        (message1 + message2).write({'discussion_name': 'X3'})
1185        self.assertEqual(discussion1.name, 'X3')
1186        self.assertEqual(discussion2.name, 'X3')
1187
1188        # search on related field, and check result
1189        search_on_related = self.env['test_new_api.message'].search([('discussion_name', '=', 'Bar')])
1190        search_on_regular = self.env['test_new_api.message'].search([('discussion.name', '=', 'Bar')])
1191        self.assertEqual(search_on_related, search_on_regular)
1192
1193        # check that field attributes are copied
1194        message_field = message.fields_get(['discussion_name'])['discussion_name']
1195        discussion_field = discussion.fields_get(['name'])['name']
1196        self.assertEqual(message_field['help'], discussion_field['help'])
1197
1198    def test_25_related_single(self):
1199        """ test related fields with a single field in the path. """
1200        record = self.env['test_new_api.related'].create({'name': 'A'})
1201        self.assertEqual(record.related_name, record.name)
1202        self.assertEqual(record.related_related_name, record.name)
1203
1204        # check searching on related fields
1205        records0 = record.search([('name', '=', 'A')])
1206        self.assertIn(record, records0)
1207        records1 = record.search([('related_name', '=', 'A')])
1208        self.assertEqual(records1, records0)
1209        records2 = record.search([('related_related_name', '=', 'A')])
1210        self.assertEqual(records2, records0)
1211
1212        # check writing on related fields
1213        record.write({'related_name': 'B'})
1214        self.assertEqual(record.name, 'B')
1215        record.write({'related_related_name': 'C'})
1216        self.assertEqual(record.name, 'C')
1217
1218    def test_25_related_multi(self):
1219        """ test write() on several related fields based on a common computed field. """
1220        foo = self.env['test_new_api.foo'].create({'name': 'A', 'value1': 1, 'value2': 2})
1221        oof = self.env['test_new_api.foo'].create({'name': 'B', 'value1': 1, 'value2': 2})
1222        bar = self.env['test_new_api.bar'].create({'name': 'A'})
1223        self.assertEqual(bar.foo, foo)
1224        self.assertEqual(bar.value1, 1)
1225        self.assertEqual(bar.value2, 2)
1226
1227        foo.invalidate_cache()
1228        bar.write({'value1': 3, 'value2': 4})
1229        self.assertEqual(foo.value1, 3)
1230        self.assertEqual(foo.value2, 4)
1231
1232        # modify 'name', and search on 'foo': this should flush 'name'
1233        bar.name = 'B'
1234        self.assertEqual(bar.foo, oof)
1235        self.assertIn(bar, bar.search([('foo', 'in', oof.ids)]))
1236
1237    def test_25_one2many_inverse_related(self):
1238        left = self.env['test_new_api.trigger.left'].create({})
1239        right = self.env['test_new_api.trigger.right'].create({})
1240        self.assertFalse(left.right_id)
1241        self.assertFalse(right.left_ids)
1242        self.assertFalse(right.left_size)
1243
1244        # create middle: this should trigger left.right_id by traversing
1245        # middle.left_id, and right.left_size by traversing left.right_id
1246        # after its computation!
1247        middle = self.env['test_new_api.trigger.middle'].create({
1248            'left_id': left.id,
1249            'right_id': right.id,
1250        })
1251        self.assertEqual(left.right_id, right)
1252        self.assertEqual(right.left_ids, left)
1253        self.assertEqual(right.left_size, 1)
1254
1255        # delete middle: this should trigger left.right_id by traversing
1256        # middle.left_id, and right.left_size by traversing left.right_id
1257        # before its computation!
1258        middle.unlink()
1259        self.assertFalse(left.right_id)
1260        self.assertFalse(right.left_ids)
1261        self.assertFalse(right.left_size)
1262
1263    def test_26_inherited(self):
1264        """ test inherited fields. """
1265        # a bunch of fields are inherited from res_partner
1266        for user in self.env['res.users'].search([]):
1267            partner = user.partner_id
1268            for field in ('is_company', 'name', 'email', 'country_id'):
1269                self.assertEqual(getattr(user, field), getattr(partner, field))
1270                self.assertEqual(user[field], partner[field])
1271
1272    def test_27_company_dependent(self):
1273        """ test company-dependent fields. """
1274        # consider three companies
1275        company0 = self.env.ref('base.main_company')
1276        company1 = self.env['res.company'].create({'name': 'A'})
1277        company2 = self.env['res.company'].create({'name': 'B'})
1278
1279        # create one user per company
1280        user0 = self.env['res.users'].create({
1281            'name': 'Foo', 'login': 'foo', 'company_id': company0.id,
1282            'company_ids': [(6, 0, [company0.id, company1.id, company2.id])]})
1283        user1 = self.env['res.users'].create({
1284            'name': 'Bar', 'login': 'bar', 'company_id': company1.id,
1285            'company_ids': [(6, 0, [company0.id, company1.id, company2.id])]})
1286        user2 = self.env['res.users'].create({
1287            'name': 'Baz', 'login': 'baz', 'company_id': company2.id,
1288            'company_ids': [(6, 0, [company0.id, company1.id, company2.id])]})
1289
1290        # create values for many2one field
1291        tag0 = self.env['test_new_api.multi.tag'].create({'name': 'Qux'})
1292        tag1 = self.env['test_new_api.multi.tag'].create({'name': 'Quux'})
1293        tag2 = self.env['test_new_api.multi.tag'].create({'name': 'Quuz'})
1294
1295        # create default values for the company-dependent fields
1296        self.env['ir.property']._set_default('foo', 'test_new_api.company', 'default')
1297        self.env['ir.property']._set_default('foo', 'test_new_api.company', 'default1', company1)
1298        self.env['ir.property']._set_default('tag_id', 'test_new_api.company', tag0)
1299
1300        # assumption: users don't have access to 'ir.property'
1301        accesses = self.env['ir.model.access'].search([('model_id.model', '=', 'ir.property')])
1302        accesses.write(dict.fromkeys(['perm_read', 'perm_write', 'perm_create', 'perm_unlink'], False))
1303
1304        # create/modify a record, and check the value for each user
1305        record = self.env['test_new_api.company'].create({
1306            'foo': 'main',
1307            'date': '1932-11-09',
1308            'moment': '1932-11-09 00:00:00',
1309            'tag_id': tag1.id,
1310        })
1311        self.assertEqual(record.with_user(user0).foo, 'main')
1312        self.assertEqual(record.with_user(user1).foo, 'default1')
1313        self.assertEqual(record.with_user(user2).foo, 'default')
1314        self.assertEqual(str(record.with_user(user0).date), '1932-11-09')
1315        self.assertEqual(record.with_user(user1).date, False)
1316        self.assertEqual(record.with_user(user2).date, False)
1317        self.assertEqual(str(record.with_user(user0).moment), '1932-11-09 00:00:00')
1318        self.assertEqual(record.with_user(user1).moment, False)
1319        self.assertEqual(record.with_user(user2).moment, False)
1320        self.assertEqual(record.with_user(user0).tag_id, tag1)
1321        self.assertEqual(record.with_user(user1).tag_id, tag0)
1322        self.assertEqual(record.with_user(user2).tag_id, tag0)
1323
1324        record.with_user(user1).write({
1325            'foo': 'alpha',
1326            'date': '1932-12-10',
1327            'moment': '1932-12-10 23:59:59',
1328            'tag_id': tag2.id,
1329        })
1330        self.assertEqual(record.with_user(user0).foo, 'main')
1331        self.assertEqual(record.with_user(user1).foo, 'alpha')
1332        self.assertEqual(record.with_user(user2).foo, 'default')
1333        self.assertEqual(str(record.with_user(user0).date), '1932-11-09')
1334        self.assertEqual(str(record.with_user(user1).date), '1932-12-10')
1335        self.assertEqual(record.with_user(user2).date, False)
1336        self.assertEqual(str(record.with_user(user0).moment), '1932-11-09 00:00:00')
1337        self.assertEqual(str(record.with_user(user1).moment), '1932-12-10 23:59:59')
1338        self.assertEqual(record.with_user(user2).moment, False)
1339        self.assertEqual(record.with_user(user0).tag_id, tag1)
1340        self.assertEqual(record.with_user(user1).tag_id, tag2)
1341        self.assertEqual(record.with_user(user2).tag_id, tag0)
1342
1343        # regression: duplicated records caused values to be browse(browse(id))
1344        recs = record.create({}) + record + record
1345        recs.invalidate_cache()
1346        for rec in recs.with_user(user0):
1347            self.assertIsInstance(rec.tag_id.id, int)
1348
1349        # unlink value of a many2one (tag2), and check again
1350        tag2.unlink()
1351        self.assertEqual(record.with_user(user0).tag_id, tag1)
1352        self.assertEqual(record.with_user(user1).tag_id, tag0.browse())
1353        self.assertEqual(record.with_user(user2).tag_id, tag0)
1354
1355        record.with_user(user1).foo = False
1356        self.assertEqual(record.with_user(user0).foo, 'main')
1357        self.assertEqual(record.with_user(user1).foo, False)
1358        self.assertEqual(record.with_user(user2).foo, 'default')
1359
1360        record.with_user(user0).with_company(company1).foo = 'beta'
1361        record.invalidate_cache()
1362        self.assertEqual(record.with_user(user0).foo, 'main')
1363        self.assertEqual(record.with_user(user1).foo, 'beta')
1364        self.assertEqual(record.with_user(user2).foo, 'default')
1365
1366        # add group on company-dependent field
1367        self.assertFalse(user0.has_group('base.group_system'))
1368        self.patch(type(record).foo, 'groups', 'base.group_system')
1369        with self.assertRaises(AccessError):
1370            record.with_user(user0).foo = 'forbidden'
1371            record.flush()
1372
1373        user0.write({'groups_id': [(4, self.env.ref('base.group_system').id)]})
1374        record.with_user(user0).foo = 'yes we can'
1375
1376        # add ir.rule to prevent access on record
1377        self.assertTrue(user0.has_group('base.group_user'))
1378        rule = self.env['ir.rule'].create({
1379            'model_id': self.env['ir.model']._get_id(record._name),
1380            'groups': [self.env.ref('base.group_user').id],
1381            'domain_force': str([('id', '!=', record.id)]),
1382        })
1383        with self.assertRaises(AccessError):
1384            record.with_user(user0).foo = 'forbidden'
1385            record.flush()
1386
1387        # create company record and attribute
1388        company_record = self.env['test_new_api.company'].create({'foo': 'ABC'})
1389        attribute_record = self.env['test_new_api.company.attr'].create({
1390            'company': company_record.id,
1391            'quantity': 1,
1392        })
1393        self.assertEqual(attribute_record.bar, 'ABC')
1394
1395        # change quantity, 'bar' should recompute to 'ABCABC'
1396        attribute_record.quantity = 2
1397        self.assertEqual(attribute_record.bar, 'ABCABC')
1398
1399        # change company field 'foo', 'bar' should recompute to 'DEFDEF'
1400        company_record.foo = 'DEF'
1401        self.assertEqual(attribute_record.company.foo, 'DEF')
1402        self.assertEqual(attribute_record.bar, 'DEFDEF')
1403
1404        # a low priviledge user should be able to search on company_dependent fields
1405        company_record.env.user.groups_id -= self.env.ref('base.group_system')
1406        self.assertFalse(company_record.env.user.has_group('base.group_system'))
1407        company_records = self.env['test_new_api.company'].search([('foo', '=', 'DEF')])
1408        self.assertEqual(len(company_records), 1)
1409
1410    def test_30_read(self):
1411        """ test computed fields as returned by read(). """
1412        discussion = self.env.ref('test_new_api.discussion_0')
1413
1414        for message in discussion.messages:
1415            display_name = message.display_name
1416            size = message.size
1417
1418            data = message.read(['display_name', 'size'])[0]
1419            self.assertEqual(data['display_name'], display_name)
1420            self.assertEqual(data['size'], size)
1421
1422    def test_31_prefetch(self):
1423        """ test prefetch of records handle AccessError """
1424        Category = self.env['test_new_api.category']
1425        cat1 = Category.create({'name': 'NOACCESS'})
1426        cat2 = Category.create({'name': 'ACCESS', 'parent': cat1.id})
1427        cats = cat1 + cat2
1428
1429        self.env.clear()
1430
1431        cat1, cat2 = cats
1432        self.assertEqual(cat2.name, 'ACCESS')
1433        # both categories should be ready for prefetching
1434        self.assertItemsEqual(cat2._prefetch_ids, cats.ids)
1435        # but due to our (lame) overwrite of `read`, it should not forbid us to read records we have access to
1436        self.assertFalse(cat2.discussions)
1437        self.assertEqual(cat2.parent, cat1)
1438        with self.assertRaises(AccessError):
1439            cat1.name
1440
1441    def test_40_real_vs_new(self):
1442        """ test field access on new records vs real records. """
1443        Model = self.env['test_new_api.category']
1444        real_record = Model.create({'name': 'Foo'})
1445        self.env.cache.invalidate()
1446        new_origin = Model.new({'name': 'Bar'}, origin=real_record)
1447        new_record = Model.new({'name': 'Baz'})
1448
1449        # non-computed non-stored field: default value
1450        real_record = real_record.with_context(default_dummy='WTF')
1451        new_origin = new_origin.with_context(default_dummy='WTF')
1452        new_record = new_record.with_context(default_dummy='WTF')
1453        self.assertEqual(real_record.dummy, 'WTF')
1454        self.assertEqual(new_origin.dummy, 'WTF')
1455        self.assertEqual(new_record.dummy, 'WTF')
1456
1457        # non-computed stored field: origin or default if no origin
1458        real_record = real_record.with_context(default_color=42)
1459        new_origin = new_origin.with_context(default_color=42)
1460        new_record = new_record.with_context(default_color=42)
1461        self.assertEqual(real_record.color, 0)
1462        self.assertEqual(new_origin.color, 0)
1463        self.assertEqual(new_record.color, 42)
1464
1465        # computed non-stored field: always computed
1466        self.assertEqual(real_record.display_name, 'Foo')
1467        self.assertEqual(new_origin.display_name, 'Bar')
1468        self.assertEqual(new_record.display_name, 'Baz')
1469
1470        # computed stored field: origin or computed if no origin
1471        Model = self.env['test_new_api.recursive']
1472        real_record = Model.create({'name': 'Foo'})
1473        new_origin = Model.new({'name': 'Bar'}, origin=real_record)
1474        new_record = Model.new({'name': 'Baz'})
1475        self.assertEqual(real_record.display_name, 'Foo')
1476        self.assertEqual(new_origin.display_name, 'Bar')
1477        self.assertEqual(new_record.display_name, 'Baz')
1478
1479        # computed stored field with recomputation: always computed
1480        real_record.name = 'Fool'
1481        new_origin.name = 'Barr'
1482        new_record.name = 'Bazz'
1483        self.assertEqual(real_record.display_name, 'Fool')
1484        self.assertEqual(new_origin.display_name, 'Barr')
1485        self.assertEqual(new_record.display_name, 'Bazz')
1486
1487    def test_40_new_defaults(self):
1488        """ Test new records with defaults. """
1489        user = self.env.user
1490        discussion = self.env.ref('test_new_api.discussion_0')
1491
1492        # create a new message; fields have their default value if not given
1493        new_msg = self.env['test_new_api.message'].new({'body': "XXX"})
1494        self.assertFalse(new_msg.id)
1495        self.assertEqual(new_msg.body, "XXX")
1496        self.assertEqual(new_msg.author, user)
1497
1498        # assign some fields; should have no side effect
1499        new_msg.discussion = discussion
1500        new_msg.body = "YYY"
1501        self.assertEqual(new_msg.discussion, discussion)
1502        self.assertEqual(new_msg.body, "YYY")
1503        self.assertNotIn(new_msg, discussion.messages)
1504
1505        # check computed values of fields
1506        self.assertEqual(new_msg.name, "[%s] %s" % (discussion.name, user.name))
1507        self.assertEqual(new_msg.size, 3)
1508
1509        # extra tests for x2many fields with default
1510        cat1 = self.env['test_new_api.category'].create({'name': "Cat1"})
1511        cat2 = self.env['test_new_api.category'].create({'name': "Cat2"})
1512        discussion = discussion.with_context(default_categories=[(4, cat1.id)])
1513        # no value gives the default value
1514        new_disc = discussion.new({'name': "Foo"})
1515        self.assertEqual(new_disc.categories._origin, cat1)
1516        # value overrides default value
1517        new_disc = discussion.new({'name': "Foo", 'categories': [(4, cat2.id)]})
1518        self.assertEqual(new_disc.categories._origin, cat2)
1519
1520    def test_40_new_fields(self):
1521        """ Test new records with relational fields. """
1522        # create a new discussion with all kinds of relational fields
1523        msg0 = self.env['test_new_api.message'].create({'body': "XXX"})
1524        msg1 = self.env['test_new_api.message'].create({'body': "WWW"})
1525        cat0 = self.env['test_new_api.category'].create({'name': 'AAA'})
1526        cat1 = self.env['test_new_api.category'].create({'name': 'DDD'})
1527        new_disc = self.env['test_new_api.discussion'].new({
1528            'name': "Stuff",
1529            'moderator': self.env.uid,
1530            'messages': [
1531                (4, msg0.id),
1532                (4, msg1.id), (1, msg1.id, {'body': "YYY"}),
1533                (0, 0, {'body': "ZZZ"})
1534            ],
1535            'categories': [
1536                (4, cat0.id),
1537                (4, cat1.id), (1, cat1.id, {'name': "BBB"}),
1538                (0, 0, {'name': "CCC"})
1539            ],
1540        })
1541        self.assertFalse(new_disc.id)
1542
1543        # many2one field values are actual records
1544        self.assertEqual(new_disc.moderator.id, self.env.uid)
1545
1546        # x2many fields values are new records
1547        new_msg0, new_msg1, new_msg2 = new_disc.messages
1548        self.assertFalse(new_msg0.id)
1549        self.assertFalse(new_msg1.id)
1550        self.assertFalse(new_msg2.id)
1551
1552        new_cat0, new_cat1, new_cat2 = new_disc.categories
1553        self.assertFalse(new_cat0.id)
1554        self.assertFalse(new_cat1.id)
1555        self.assertFalse(new_cat2.id)
1556
1557        # the x2many has its inverse field set
1558        self.assertEqual(new_msg0.discussion, new_disc)
1559        self.assertEqual(new_msg1.discussion, new_disc)
1560        self.assertEqual(new_msg2.discussion, new_disc)
1561
1562        self.assertFalse(msg0.discussion)
1563        self.assertFalse(msg1.discussion)
1564
1565        self.assertEqual(new_cat0.discussions, new_disc)    # add other discussions
1566        self.assertEqual(new_cat1.discussions, new_disc)
1567        self.assertEqual(new_cat2.discussions, new_disc)
1568
1569        self.assertNotIn(new_disc, cat0.discussions)
1570        self.assertNotIn(new_disc, cat1.discussions)
1571
1572        # new lines are connected to their origin
1573        self.assertEqual(new_msg0._origin, msg0)
1574        self.assertEqual(new_msg1._origin, msg1)
1575        self.assertFalse(new_msg2._origin)
1576
1577        self.assertEqual(new_cat0._origin, cat0)
1578        self.assertEqual(new_cat1._origin, cat1)
1579        self.assertFalse(new_cat2._origin)
1580
1581        # the field values are either specific, or the same as the origin
1582        self.assertEqual(new_msg0.body, "XXX")
1583        self.assertEqual(new_msg1.body, "YYY")
1584        self.assertEqual(new_msg2.body, "ZZZ")
1585
1586        self.assertEqual(msg0.body, "XXX")
1587        self.assertEqual(msg1.body, "WWW")
1588
1589        self.assertEqual(new_cat0.name, "AAA")
1590        self.assertEqual(new_cat1.name, "BBB")
1591        self.assertEqual(new_cat2.name, "CCC")
1592
1593        self.assertEqual(cat0.name, "AAA")
1594        self.assertEqual(cat1.name, "DDD")
1595
1596        # special case for many2one fields that define _inherits
1597        new_email = self.env['test_new_api.emailmessage'].new({'body': "XXX"})
1598        self.assertFalse(new_email.id)
1599        self.assertTrue(new_email.message)
1600        self.assertFalse(new_email.message.id)
1601        self.assertEqual(new_email.body, "XXX")
1602
1603        new_email = self.env['test_new_api.emailmessage'].new({'message': msg0.id})
1604        self.assertFalse(new_email.id)
1605        self.assertFalse(new_email._origin)
1606        self.assertFalse(new_email.message.id)
1607        self.assertEqual(new_email.message._origin, msg0)
1608        self.assertEqual(new_email.body, "XXX")
1609
1610        # check that this does not generate an infinite recursion
1611        new_disc._convert_to_write(new_disc._cache)
1612
1613    def test_40_new_inherited_fields(self):
1614        """ Test the behavior of new records with inherited fields. """
1615        email = self.env['test_new_api.emailmessage'].new({'body': 'XXX'})
1616        self.assertEqual(email.body, 'XXX')
1617        self.assertEqual(email.message.body, 'XXX')
1618
1619        email.body = 'YYY'
1620        self.assertEqual(email.body, 'YYY')
1621        self.assertEqual(email.message.body, 'YYY')
1622
1623        email.message.body = 'ZZZ'
1624        self.assertEqual(email.body, 'ZZZ')
1625        self.assertEqual(email.message.body, 'ZZZ')
1626
1627    def test_40_new_ref_origin(self):
1628        """ Test the behavior of new records with ref/origin. """
1629        Discussion = self.env['test_new_api.discussion']
1630        new = Discussion.new
1631
1632        # new records with identical/different refs
1633        xs = new() + new(ref='a') + new(ref='b') + new(ref='b')
1634        self.assertEqual([x == y for x in xs for y in xs], [
1635            1, 0, 0, 0,
1636            0, 1, 0, 0,
1637            0, 0, 1, 1,
1638            0, 0, 1, 1,
1639        ])
1640        for x in xs:
1641            self.assertFalse(x._origin)
1642
1643        # new records with identical/different origins
1644        a, b = Discussion.create([{'name': "A"}, {'name': "B"}])
1645        xs = new() + new(origin=a) + new(origin=b) + new(origin=b)
1646        self.assertEqual([x == y for x in xs for y in xs], [
1647            1, 0, 0, 0,
1648            0, 1, 0, 0,
1649            0, 0, 1, 1,
1650            0, 0, 1, 1,
1651        ])
1652        self.assertFalse(xs[0]._origin)
1653        self.assertEqual(xs[1]._origin, a)
1654        self.assertEqual(xs[2]._origin, b)
1655        self.assertEqual(xs[3]._origin, b)
1656        self.assertEqual(xs._origin, a + b + b)
1657        self.assertEqual(xs._origin._origin, a + b + b)
1658
1659        # new records with refs and origins
1660        x1 = new(ref='a')
1661        x2 = new(origin=b)
1662        self.assertNotEqual(x1, x2)
1663
1664        # new discussion based on existing discussion
1665        disc = self.env.ref('test_new_api.discussion_0')
1666        new_disc = disc.new(origin=disc)
1667        self.assertFalse(new_disc.id)
1668        self.assertEqual(new_disc._origin, disc)
1669        self.assertEqual(new_disc.name, disc.name)
1670        # many2one field
1671        self.assertEqual(new_disc.moderator, disc.moderator)
1672        # one2many field
1673        self.assertTrue(new_disc.messages)
1674        self.assertNotEqual(new_disc.messages, disc.messages)
1675        self.assertEqual(new_disc.messages._origin, disc.messages)
1676        # many2many field
1677        self.assertTrue(new_disc.participants)
1678        self.assertNotEqual(new_disc.participants, disc.participants)
1679        self.assertEqual(new_disc.participants._origin, disc.participants)
1680
1681        # provide many2one field as a dict of values; the value is a new record
1682        # with the given 'id' as origin (if given, of course)
1683        new_msg = disc.messages.new({
1684            'discussion': {'name': disc.name},
1685        })
1686        self.assertTrue(new_msg.discussion)
1687        self.assertFalse(new_msg.discussion.id)
1688        self.assertFalse(new_msg.discussion._origin)
1689
1690        new_msg = disc.messages.new({
1691            'discussion': {'name': disc.name, 'id': disc.id},
1692        })
1693        self.assertTrue(new_msg.discussion)
1694        self.assertFalse(new_msg.discussion.id)
1695        self.assertEqual(new_msg.discussion._origin, disc)
1696
1697        # check convert_to_write
1698        tag = self.env['test_new_api.multi.tag'].create({'name': 'Foo'})
1699        rec = self.env['test_new_api.multi'].create({
1700            'lines': [(0, 0, {'tags': [(6, 0, tag.ids)]})],
1701        })
1702        new = rec.new(origin=rec)
1703        self.assertEqual(new.lines.tags._origin, rec.lines.tags)
1704        vals = new._convert_to_write(new._cache)
1705        self.assertEqual(vals['lines'], [(6, 0, rec.lines.ids)])
1706
1707    def test_41_new_compute(self):
1708        """ Check recomputation of fields on new records. """
1709        move = self.env['test_new_api.move'].create({
1710            'line_ids': [(0, 0, {'quantity': 1}), (0, 0, {'quantity': 1})],
1711        })
1712        move.flush()
1713        line = move.line_ids[0]
1714
1715        new_move = move.new(origin=move)
1716        new_line = line.new(origin=line)
1717
1718        # move_id is fetched from origin
1719        self.assertEqual(new_line.move_id, move)
1720        self.assertEqual(new_move.quantity, 2)
1721        self.assertEqual(move.quantity, 2)
1722
1723        # modifying new_line must trigger recomputation on new_move, even if
1724        # new_line.move_id is not new_move!
1725        new_line.quantity = 2
1726        self.assertEqual(new_line.move_id, move)
1727        self.assertEqual(new_move.quantity, 3)
1728        self.assertEqual(move.quantity, 2)
1729
1730    def test_41_new_one2many(self):
1731        """ Check command on one2many field on new record. """
1732        move = self.env['test_new_api.move'].create({})
1733        line = self.env['test_new_api.move_line'].create({'move_id': move.id, 'quantity': 1})
1734        move.flush()
1735
1736        new_move = move.new(origin=move)
1737        new_line = line.new(origin=line)
1738        self.assertEqual(new_move.line_ids, new_line)
1739
1740        # drop line, and create a new one
1741        new_move.line_ids = [(2, new_line.id), (0, 0, {'quantity': 2})]
1742        self.assertEqual(len(new_move.line_ids), 1)
1743        self.assertFalse(new_move.line_ids.id)
1744        self.assertEqual(new_move.line_ids.quantity, 2)
1745
1746        # assign line to new move without origin
1747        new_move = move.new()
1748        new_move.line_ids = line
1749        self.assertFalse(new_move.line_ids.id)
1750        self.assertEqual(new_move.line_ids._origin, line)
1751        self.assertEqual(new_move.line_ids.move_id, new_move)
1752
1753    @mute_logger('odoo.addons.base.models.ir_model')
1754    def test_41_new_related(self):
1755        """ test the behavior of related fields starting on new records. """
1756        # make discussions unreadable for demo user
1757        access = self.env.ref('test_new_api.access_discussion')
1758        access.write({'perm_read': False})
1759
1760        # create an environment for demo user
1761        env = self.env(user=self.user_demo)
1762        self.assertEqual(env.user.login, "demo")
1763
1764        # create a new message as demo user
1765        discussion = self.env.ref('test_new_api.discussion_0')
1766        message = env['test_new_api.message'].new({'discussion': discussion})
1767        self.assertEqual(message.discussion, discussion)
1768
1769        # read the related field discussion_name
1770        self.assertEqual(message.discussion.env, env)
1771        self.assertEqual(message.discussion_name, discussion.name)
1772        # DLE P75: message.discussion.name is put in the cache as sudo thanks to the computation of message.discussion_name
1773        # As we decided that now if we had the chance to access the value at some point in the code, and that it was stored in the cache
1774        # it's not a big deal to no longer raise the accesserror, as we had the chance to get the value at some point
1775        # with self.assertRaises(AccessError):
1776        #     message.discussion.name
1777
1778    @mute_logger('odoo.addons.base.models.ir_model')
1779    def test_42_new_related(self):
1780        """ test the behavior of related fields traversing new records. """
1781        # make discussions unreadable for demo user
1782        access = self.env.ref('test_new_api.access_discussion')
1783        access.write({'perm_read': False})
1784
1785        # create an environment for demo user
1786        env = self.env(user=self.user_demo)
1787        self.assertEqual(env.user.login, "demo")
1788
1789        # create a new discussion and a new message as demo user
1790        discussion = env['test_new_api.discussion'].new({'name': 'Stuff'})
1791        message = env['test_new_api.message'].new({'discussion': discussion})
1792        self.assertEqual(message.discussion, discussion)
1793
1794        # read the related field discussion_name
1795        self.assertNotEqual(message.sudo().env, message.env)
1796        self.assertEqual(message.discussion_name, discussion.name)
1797
1798    def test_43_new_related(self):
1799        """ test the behavior of one2many related fields """
1800        partner = self.env['res.partner'].create({
1801            'name': 'Foo',
1802            'child_ids': [(0, 0, {'name': 'Bar'})],
1803        })
1804        multi = self.env['test_new_api.multi'].new()
1805        multi.partner = partner
1806        self.assertEqual(multi.partners.mapped('name'), ['Bar'])
1807
1808    def test_50_defaults(self):
1809        """ test default values. """
1810        fields = ['discussion', 'body', 'author', 'size']
1811        defaults = self.env['test_new_api.message'].default_get(fields)
1812        self.assertEqual(defaults, {'author': self.env.uid})
1813
1814        defaults = self.env['test_new_api.mixed'].default_get(['number'])
1815        self.assertEqual(defaults, {'number': 3.14})
1816
1817    def test_50_search_many2one(self):
1818        """ test search through a path of computed fields"""
1819        messages = self.env['test_new_api.message'].search(
1820            [('author_partner.name', '=', 'Marc Demo')])
1821        self.assertEqual(messages, self.env.ref('test_new_api.message_0_1'))
1822
1823    def test_60_one2many_domain(self):
1824        """ test the cache consistency of a one2many field with a domain """
1825        discussion = self.env.ref('test_new_api.discussion_0')
1826        message = discussion.messages[0]
1827        self.assertNotIn(message, discussion.important_messages)
1828
1829        message.important = True
1830        self.assertIn(message, discussion.important_messages)
1831
1832        # writing on very_important_messages should call its domain method
1833        self.assertIn(message, discussion.very_important_messages)
1834        discussion.write({'very_important_messages': [(5,)]})
1835        self.assertFalse(discussion.very_important_messages)
1836        self.assertFalse(message.exists())
1837
1838    def test_60_many2many_domain(self):
1839        """ test the cache consistency of a many2many field with a domain """
1840        discussion = self.env.ref('test_new_api.discussion_0')
1841        category = self.env['test_new_api.category'].create({'name': "Foo"})
1842        discussion.categories = category
1843        discussion.flush()
1844        discussion.invalidate_cache()
1845
1846        # patch the many2many field to give it a domain (this simply avoids
1847        # adding yet another test model)
1848        field = discussion._fields['categories']
1849        self.patch(field, 'domain', [('color', '!=', 42)])
1850        self.registry.setup_models(self.cr)
1851
1852        # the category is in the many2many
1853        self.assertIn(category, discussion.categories)
1854
1855        # modify the category; it should not longer be in the many2many
1856        category.color = 42
1857        self.assertNotIn(category, discussion.categories)
1858
1859        # modify again the category; it should be back in the many2many
1860        category.color = 69
1861        self.assertIn(category, discussion.categories)
1862
1863    def test_70_x2many_write(self):
1864        discussion = self.env.ref('test_new_api.discussion_0')
1865        # See YTI FIXME
1866        discussion.invalidate_cache()
1867
1868        Message = self.env['test_new_api.message']
1869        # There must be 3 messages, 0 important
1870        self.assertEqual(len(discussion.messages), 3)
1871        self.assertEqual(len(discussion.important_messages), 0)
1872        self.assertEqual(len(discussion.very_important_messages), 0)
1873        discussion.important_messages = [(0, 0, {
1874            'body': 'What is the answer?',
1875            'important': True,
1876        })]
1877        # There must be 4 messages, 1 important
1878        self.assertEqual(len(discussion.messages), 4)
1879        self.assertEqual(len(discussion.important_messages), 1)
1880        self.assertEqual(len(discussion.very_important_messages), 1)
1881        discussion.very_important_messages |= Message.new({
1882            'body': '42',
1883            'important': True,
1884        })
1885        # There must be 5 messages, 2 important
1886        self.assertEqual(len(discussion.messages), 5)
1887        self.assertEqual(len(discussion.important_messages), 2)
1888        self.assertEqual(len(discussion.very_important_messages), 2)
1889
1890    def test_70_relational_inverse(self):
1891        """ Check the consistency of relational fields with inverse(s). """
1892        discussion = self.env.ref('test_new_api.discussion_0')
1893        demo_discussion = discussion.with_user(self.user_demo)
1894
1895        # check that the demo user sees the same messages
1896        self.assertEqual(demo_discussion.messages, discussion.messages)
1897
1898        # See YTI FIXME
1899        discussion.invalidate_cache()
1900        demo_discussion.invalidate_cache()
1901
1902        # add a message as user demo
1903        messages = demo_discussion.messages
1904        message = messages.create({'discussion': discussion.id})
1905        self.assertEqual(demo_discussion.messages, messages + message)
1906        self.assertEqual(demo_discussion.messages, discussion.messages)
1907
1908        # add a message as superuser
1909        messages = discussion.messages
1910        message = messages.create({'discussion': discussion.id})
1911        self.assertEqual(discussion.messages, messages + message)
1912        self.assertEqual(demo_discussion.messages, discussion.messages)
1913
1914    def test_71_relational_inverse(self):
1915        """ Check the consistency of relational fields with inverse(s). """
1916        move1 = self.env['test_new_api.move'].create({})
1917        move2 = self.env['test_new_api.move'].create({})
1918        line = self.env['test_new_api.move_line'].create({'move_id': move1.id})
1919        line.flush()
1920
1921        self.env.cache.invalidate()
1922        line.with_context(prefetch_fields=False).move_id
1923
1924        # Setting 'move_id' updates the one2many field that is based on it,
1925        # which has a domain.  Here we check that evaluating the domain does not
1926        # accidentally override 'move_id' (by prefetch).
1927        line.move_id = move2
1928        self.assertEqual(line.move_id, move2)
1929
1930    def test_72_relational_inverse(self):
1931        """ Check the consistency of relational fields with inverse(s). """
1932        move1 = self.env['test_new_api.move'].create({})
1933        move2 = self.env['test_new_api.move'].create({})
1934
1935        # makes sure that line.move_id is flushed before search
1936        line = self.env['test_new_api.move_line'].create({'move_id': move1.id})
1937        moves = self.env['test_new_api.move'].search([('line_ids', 'in', line.id)])
1938        self.assertEqual(moves, move1)
1939
1940        # makes sure that line.move_id is flushed before search
1941        line.move_id = move2
1942        moves = self.env['test_new_api.move'].search([('line_ids', 'in', line.id)])
1943        self.assertEqual(moves, move2)
1944
1945    def test_80_copy(self):
1946        Translations = self.env['ir.translation']
1947        discussion = self.env.ref('test_new_api.discussion_0')
1948        message = self.env.ref('test_new_api.message_0_0')
1949        message1 = self.env.ref('test_new_api.message_0_1')
1950
1951        email = self.env.ref('test_new_api.emailmessage_0_0')
1952        self.assertEqual(email.message, message)
1953
1954        self.env['res.lang']._activate_lang('fr_FR')
1955
1956        def count(msg):
1957            # return the number of translations of msg.label
1958            return Translations.search_count([
1959                ('name', '=', 'test_new_api.message,label'),
1960                ('res_id', '=', msg.id),
1961            ])
1962
1963        # set a translation for message.label
1964        email.with_context(lang='fr_FR').label = "bonjour"
1965        self.assertEqual(count(message), 1)
1966        self.assertEqual(count(message1), 0)
1967
1968        # setting the parent record should not copy its translations
1969        email.copy({'message': message1.id})
1970        self.assertEqual(count(message), 1)
1971        self.assertEqual(count(message1), 0)
1972
1973        # setting a one2many should not copy translations on the lines
1974        discussion.copy({'messages': [(6, 0, message1.ids)]})
1975        self.assertEqual(count(message), 1)
1976        self.assertEqual(count(message1), 0)
1977
1978    def test_85_binary_guess_zip(self):
1979        from odoo.addons.base.tests.test_mimetypes import ZIP
1980        # Regular ZIP files can be uploaded by non-admin users
1981        self.env['test_new_api.binary_svg'].with_user(
1982            self.env.ref('base.user_demo'),
1983        ).create({
1984            'name': 'Test without attachment',
1985            'image_wo_attachment': base64.b64decode(ZIP),
1986        })
1987
1988    def test_86_text_base64_guess_svg(self):
1989        from odoo.addons.base.tests.test_mimetypes import SVG
1990        with self.assertRaises(UserError) as e:
1991            self.env['test_new_api.binary_svg'].with_user(
1992                self.env.ref('base.user_demo'),
1993            ).create({
1994                'name': 'Test without attachment',
1995                'image_wo_attachment': SVG.decode("utf-8"),
1996            })
1997        self.assertEqual(e.exception.args[0], 'Only admins can upload SVG files.')
1998
1999    def test_90_binary_svg(self):
2000        from odoo.addons.base.tests.test_mimetypes import SVG
2001        # This should work without problems
2002        self.env['test_new_api.binary_svg'].create({
2003            'name': 'Test without attachment',
2004            'image_wo_attachment': SVG,
2005        })
2006        # And this gives error
2007        with self.assertRaises(UserError):
2008            self.env['test_new_api.binary_svg'].with_user(
2009                self.user_demo,
2010            ).create({
2011                'name': 'Test without attachment',
2012                'image_wo_attachment': SVG,
2013            })
2014
2015    def test_91_binary_svg_attachment(self):
2016        from odoo.addons.base.tests.test_mimetypes import SVG
2017        # This doesn't neuter SVG with admin
2018        record = self.env['test_new_api.binary_svg'].create({
2019            'name': 'Test without attachment',
2020            'image_attachment': SVG,
2021        })
2022        attachment = self.env['ir.attachment'].search([
2023            ('res_model', '=', record._name),
2024            ('res_field', '=', 'image_attachment'),
2025            ('res_id', '=', record.id),
2026        ])
2027        self.assertEqual(attachment.mimetype, 'image/svg+xml')
2028        # ...but this should be neutered with demo user
2029        record = self.env['test_new_api.binary_svg'].with_user(
2030            self.user_demo,
2031        ).create({
2032            'name': 'Test without attachment',
2033            'image_attachment': SVG,
2034        })
2035        attachment = self.env['ir.attachment'].search([
2036            ('res_model', '=', record._name),
2037            ('res_field', '=', 'image_attachment'),
2038            ('res_id', '=', record.id),
2039        ])
2040        self.assertEqual(attachment.mimetype, 'text/plain')
2041
2042    def test_92_binary_self_avatar_svg(self):
2043        from odoo.addons.base.tests.test_mimetypes import SVG
2044        demo_user = self.user_demo
2045        # User demo changes his own avatar
2046        demo_user.with_user(demo_user).image_1920 = SVG
2047        # The SVG file should have been neutered
2048        attachment = self.env['ir.attachment'].search([
2049            ('res_model', '=', demo_user.partner_id._name),
2050            ('res_field', '=', 'image_1920'),
2051            ('res_id', '=', demo_user.partner_id.id),
2052        ])
2053        self.assertEqual(attachment.mimetype, 'text/plain')
2054
2055    def test_93_monetary_related(self):
2056        """ Check the currency field on related monetary fields. """
2057        # check base field
2058        field = self.env['test_new_api.monetary_base']._fields['amount']
2059        self.assertEqual(field.currency_field, 'base_currency_id')
2060
2061        # related fields must use the field 'currency_id' or 'x_currency_id'
2062        field = self.env['test_new_api.monetary_related']._fields['amount']
2063        self.assertEqual(field.related, ('monetary_id', 'amount'))
2064        self.assertEqual(field.currency_field, 'currency_id')
2065
2066        field = self.env['test_new_api.monetary_custom']._fields['x_amount']
2067        self.assertEqual(field.related, ('monetary_id', 'amount'))
2068        self.assertEqual(field.currency_field, 'x_currency_id')
2069
2070        # inherited field must use the same field as its parent field
2071        field = self.env['test_new_api.monetary_inherits']._fields['amount']
2072        self.assertEqual(field.related, ('monetary_id', 'amount'))
2073        self.assertEqual(field.currency_field, 'base_currency_id')
2074
2075    def test_94_image(self):
2076        f = io.BytesIO()
2077        Image.new('RGB', (4000, 2000), '#4169E1').save(f, 'PNG')
2078        f.seek(0)
2079        image_w = base64.b64encode(f.read())
2080
2081        f = io.BytesIO()
2082        Image.new('RGB', (2000, 4000), '#4169E1').save(f, 'PNG')
2083        f.seek(0)
2084        image_h = base64.b64encode(f.read())
2085
2086        record = self.env['test_new_api.model_image'].create({
2087            'name': 'image',
2088            'image': image_w,
2089            'image_128': image_w,
2090        })
2091
2092        # test create (no resize)
2093        self.assertEqual(record.image, image_w)
2094        # test create (resize, width limited)
2095        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_128))).size, (128, 64))
2096        # test create related store (resize, width limited)
2097        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_512))).size, (512, 256))
2098        # test create related no store (resize, width limited)
2099        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_256))).size, (256, 128))
2100
2101        record.write({
2102            'image': image_h,
2103            'image_128': image_h,
2104        })
2105
2106        # test write (no resize)
2107        self.assertEqual(record.image, image_h)
2108        # test write (resize, height limited)
2109        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_128))).size, (64, 128))
2110        # test write related store (resize, height limited)
2111        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_512))).size, (256, 512))
2112        # test write related no store (resize, height limited)
2113        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_256))).size, (128, 256))
2114
2115        record = self.env['test_new_api.model_image'].create({
2116            'name': 'image',
2117            'image': image_h,
2118            'image_128': image_h,
2119        })
2120
2121        # test create (no resize)
2122        self.assertEqual(record.image, image_h)
2123        # test create (resize, height limited)
2124        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_128))).size, (64, 128))
2125        # test create related store (resize, height limited)
2126        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_512))).size, (256, 512))
2127        # test create related no store (resize, height limited)
2128        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_256))).size, (128, 256))
2129
2130        record.write({
2131            'image': image_w,
2132            'image_128': image_w,
2133        })
2134
2135        # test write (no resize)
2136        self.assertEqual(record.image, image_w)
2137        # test write (resize, width limited)
2138        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_128))).size, (128, 64))
2139        # test write related store (resize, width limited)
2140        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_512))).size, (512, 256))
2141        # test write related store (resize, width limited)
2142        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_256))).size, (256, 128))
2143
2144        # test create inverse store
2145        record = self.env['test_new_api.model_image'].create({
2146            'name': 'image',
2147            'image_512': image_w,
2148        })
2149        record.invalidate_cache(fnames=['image_512'], ids=record.ids)
2150        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_512))).size, (512, 256))
2151        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image))).size, (4000, 2000))
2152        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_256))).size, (256, 128))
2153        # test write inverse store
2154        record.write({
2155            'image_512': image_h,
2156        })
2157        record.invalidate_cache(fnames=['image_512'], ids=record.ids)
2158        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_512))).size, (256, 512))
2159        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image))).size, (2000, 4000))
2160        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_256))).size, (128, 256))
2161
2162        # test create inverse no store
2163        record = self.env['test_new_api.model_image'].create({
2164            'name': 'image',
2165            'image_256': image_w,
2166        })
2167        record.invalidate_cache(fnames=['image_256'], ids=record.ids)
2168        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_512))).size, (512, 256))
2169        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image))).size, (4000, 2000))
2170        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_256))).size, (256, 128))
2171        # test write inverse no store
2172        record.write({
2173            'image_256': image_h,
2174        })
2175        record.invalidate_cache(fnames=['image_256'], ids=record.ids)
2176        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_512))).size, (256, 512))
2177        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image))).size, (2000, 4000))
2178        self.assertEqual(Image.open(io.BytesIO(base64.b64decode(record.image_256))).size, (128, 256))
2179
2180        # test bin_size
2181        record_bin_size = record.with_context(bin_size=True)
2182        self.assertEqual(record_bin_size.image, b'31.54 Kb')
2183        self.assertEqual(record_bin_size.image_512, b'1.02 Kb')
2184        self.assertEqual(record_bin_size.image_256, b'424.00 bytes')
2185
2186        # ensure image_data_uri works (value must be bytes and not string)
2187        self.assertEqual(record.image_256[:8], b'iVBORw0K')
2188        self.assertEqual(image_data_uri(record.image_256)[:30], 'data:image/png;base64,iVBORw0K')
2189
2190        # ensure invalid image raises
2191        with self.assertRaises(UserError), self.cr.savepoint():
2192            record.write({
2193                'image': 'invalid image',
2194            })
2195
2196        # assignment of invalid image on new record does nothing, the value is
2197        # taken from origin instead (use-case: onchange)
2198        new_record = record.new(origin=record)
2199        new_record.image = '31.54 Kb'
2200        self.assertEqual(record.image, image_h)
2201        self.assertEqual(new_record.image, image_h)
2202
2203        # assignment to new record with origin should not do any query
2204        with self.assertQueryCount(0):
2205            new_record.image = image_w
2206
2207    def test_95_binary_bin_size(self):
2208        binary_value = base64.b64encode(b'content')
2209        binary_size = b'7.00 bytes'
2210
2211        def assertBinaryValue(record, value):
2212            for field in ('binary', 'binary_related_store', 'binary_related_no_store'):
2213                self.assertEqual(record[field], value)
2214
2215        # created, flushed, and first read without context
2216        record = self.env['test_new_api.model_binary'].create({'binary': binary_value})
2217        record.flush()
2218        record.invalidate_cache()
2219        record_no_bin_size = record.with_context(bin_size=False)
2220        record_bin_size = record.with_context(bin_size=True)
2221
2222        assertBinaryValue(record, binary_value)
2223        assertBinaryValue(record_no_bin_size, binary_value)
2224        assertBinaryValue(record_bin_size, binary_size)
2225
2226        # created, flushed, and first read with bin_size=False
2227        record_no_bin_size = self.env['test_new_api.model_binary'].with_context(bin_size=False).create({'binary': binary_value})
2228        record_no_bin_size.flush()
2229        record_no_bin_size.invalidate_cache()
2230        record = self.env['test_new_api.model_binary'].browse(record.id)
2231        record_bin_size = record.with_context(bin_size=True)
2232
2233        assertBinaryValue(record_no_bin_size, binary_value)
2234        assertBinaryValue(record, binary_value)
2235        assertBinaryValue(record_bin_size, binary_size)
2236
2237        # created, flushed, and first read with bin_size=True
2238        record_bin_size = self.env['test_new_api.model_binary'].with_context(bin_size=True).create({'binary': binary_value})
2239        record_bin_size.flush()
2240        record_bin_size.invalidate_cache()
2241        record = self.env['test_new_api.model_binary'].browse(record.id)
2242        record_no_bin_size = record.with_context(bin_size=False)
2243
2244        assertBinaryValue(record_bin_size, binary_size)
2245        assertBinaryValue(record_no_bin_size, binary_value)
2246        assertBinaryValue(record, binary_value)
2247
2248        # created without context and flushed with bin_size
2249        record = self.env['test_new_api.model_binary'].create({'binary': binary_value})
2250        record_no_bin_size = record.with_context(bin_size=False)
2251        record_bin_size = record.with_context(bin_size=True)
2252        record_bin_size.flush()
2253        record_bin_size.invalidate_cache()
2254
2255        assertBinaryValue(record, binary_value)
2256        assertBinaryValue(record_no_bin_size, binary_value)
2257        assertBinaryValue(record_bin_size, binary_size)
2258
2259        # check computed binary field with arbitrary Python value
2260        record = self.env['test_new_api.model_binary'].create({})
2261        record.flush()
2262        record.invalidate_cache()
2263        record_no_bin_size = record.with_context(bin_size=False)
2264        record_bin_size = record.with_context(bin_size=True)
2265
2266        expected_value = [(record.id, False)]
2267        self.assertEqual(record.binary_computed, expected_value)
2268        self.assertEqual(record_no_bin_size.binary_computed, expected_value)
2269        self.assertEqual(record_bin_size.binary_computed, expected_value)
2270
2271    def test_96_order_m2o(self):
2272        belgium, congo = self.env['test_new_api.country'].create([
2273            {'name': "Duchy of Brabant"},
2274            {'name': "Congo"},
2275        ])
2276        cities = self.env['test_new_api.city'].create([
2277            {'name': "Brussels", 'country_id': belgium.id},
2278            {'name': "Kinshasa", 'country_id': congo.id},
2279        ])
2280        # cities are sorted by country_id, name
2281        self.assertEqual(cities.sorted().mapped('name'), ["Kinshasa", "Brussels"])
2282
2283        # change order of countries, and check sorted()
2284        belgium.name = "Belgium"
2285        self.assertEqual(cities.sorted().mapped('name'), ["Brussels", "Kinshasa"])
2286
2287    def test_97_ir_rule_m2m_field(self):
2288        """Ensures m2m fields can't be read if the left records can't be read.
2289        Also makes sure reading m2m doesn't take more queries than necessary."""
2290        tag = self.env['test_new_api.multi.tag'].create({})
2291        record = self.env['test_new_api.multi.line'].create({
2292            'name': 'image',
2293            'tags': [(4, tag.id)],
2294        })
2295
2296        # only one query as admin: reading pivot table
2297        with self.assertQueryCount(1):
2298            record.read(['tags'])
2299
2300        user = self.env['res.users'].create({'name': "user", 'login': "user"})
2301        record_user = record.with_user(user)
2302
2303        # prep the following query count by caching access check related data
2304        record_user.read(['tags'])
2305
2306        # only one query as user: reading pivot table
2307        with self.assertQueryCount(1):
2308            record_user.read(['tags'])
2309
2310        # create a passing ir.rule
2311        self.env['ir.rule'].create({
2312            'model_id': self.env['ir.model']._get(record._name).id,
2313            'domain_force': "[('id', '=', %d)]" % record.id,
2314        })
2315
2316        # prep the following query count by caching access check related data
2317        record_user.read(['tags'])
2318
2319        # still only 1 query: reading pivot table
2320        # access rules are checked in python in this case
2321        with self.assertQueryCount(1):
2322            record_user.read(['tags'])
2323
2324        # create a blocking ir.rule
2325        self.env['ir.rule'].create({
2326            'model_id': self.env['ir.model']._get(record._name).id,
2327            'domain_force': "[('id', '!=', %d)]" % record.id,
2328        })
2329
2330        # ensure ir.rule is applied even when reading m2m
2331        with self.assertRaises(AccessError):
2332            record_user.read(['tags'])
2333
2334
2335class TestX2many(common.TransactionCase):
2336    def test_definition_many2many(self):
2337        """ Test the definition of inherited many2many fields. """
2338        field = self.env['test_new_api.multi.line']._fields['tags']
2339        self.assertEqual(field.relation, 'test_new_api_multi_line_test_new_api_multi_tag_rel')
2340        self.assertEqual(field.column1, 'test_new_api_multi_line_id')
2341        self.assertEqual(field.column2, 'test_new_api_multi_tag_id')
2342
2343        field = self.env['test_new_api.multi.line2']._fields['tags']
2344        self.assertEqual(field.relation, 'test_new_api_multi_line2_test_new_api_multi_tag_rel')
2345        self.assertEqual(field.column1, 'test_new_api_multi_line2_id')
2346        self.assertEqual(field.column2, 'test_new_api_multi_tag_id')
2347
2348    def test_10_ondelete_many2many(self):
2349        """Test A can't be deleted when used on the relation."""
2350        record_a = self.env['test_new_api.model_a'].create({'name': 'a'})
2351        record_b = self.env['test_new_api.model_b'].create({'name': 'b'})
2352        record_a.write({
2353            'a_restricted_b_ids': [(6, 0, record_b.ids)],
2354        })
2355        with self.assertRaises(psycopg2.IntegrityError):
2356            with mute_logger('odoo.sql_db'), self.cr.savepoint():
2357                record_a.unlink()
2358        # Test B is still cascade.
2359        record_b.unlink()
2360        self.assertFalse(record_b.exists())
2361
2362    def test_11_ondelete_many2many(self):
2363        """Test B can't be deleted when used on the relation."""
2364        record_a = self.env['test_new_api.model_a'].create({'name': 'a'})
2365        record_b = self.env['test_new_api.model_b'].create({'name': 'b'})
2366        record_a.write({
2367            'b_restricted_b_ids': [(6, 0, record_b.ids)],
2368        })
2369        with self.assertRaises(psycopg2.IntegrityError):
2370            with mute_logger('odoo.sql_db'), self.cr.savepoint():
2371                record_b.unlink()
2372        # Test A is still cascade.
2373        record_a.unlink()
2374        self.assertFalse(record_a.exists())
2375
2376    def test_12_active_test_one2many(self):
2377        Model = self.env['test_new_api.model_active_field']
2378
2379        parent = Model.create({})
2380        self.assertFalse(parent.children_ids)
2381
2382        # create with implicit active_test=True in context
2383        child1, child2 = Model.create([
2384            {'parent_id': parent.id, 'active': True},
2385            {'parent_id': parent.id, 'active': False},
2386        ])
2387        act_children = child1
2388        all_children = child1 + child2
2389        self.assertEqual(parent.children_ids, act_children)
2390        self.assertEqual(parent.with_context(active_test=True).children_ids, act_children)
2391        self.assertEqual(parent.with_context(active_test=False).children_ids, all_children)
2392
2393        # create with active_test=False in context
2394        child3, child4 = Model.with_context(active_test=False).create([
2395            {'parent_id': parent.id, 'active': True},
2396            {'parent_id': parent.id, 'active': False},
2397        ])
2398        act_children = child1 + child3
2399        all_children = child1 + child2 + child3 + child4
2400        self.assertEqual(parent.children_ids, act_children)
2401        self.assertEqual(parent.with_context(active_test=True).children_ids, act_children)
2402        self.assertEqual(parent.with_context(active_test=False).children_ids, all_children)
2403
2404        # replace active children
2405        parent.write({'children_ids': [(6, 0, [child1.id])]})
2406        act_children = child1
2407        all_children = child1 + child2 + child4
2408        self.assertEqual(parent.children_ids, act_children)
2409        self.assertEqual(parent.with_context(active_test=True).children_ids, act_children)
2410        self.assertEqual(parent.with_context(active_test=False).children_ids, all_children)
2411
2412        # replace all children
2413        parent.with_context(active_test=False).write({'children_ids': [(6, 0, [child1.id])]})
2414        act_children = child1
2415        all_children = child1
2416        self.assertEqual(parent.children_ids, act_children)
2417        self.assertEqual(parent.with_context(active_test=True).children_ids, act_children)
2418        self.assertEqual(parent.with_context(active_test=False).children_ids, all_children)
2419
2420        # check recomputation of inactive records
2421        parent.write({'children_ids': [(6, 0, child4.ids)]})
2422        self.assertTrue(child4.parent_active)
2423        parent.active = False
2424        self.assertFalse(child4.parent_active)
2425
2426    def test_12_active_test_one2many_with_context(self):
2427        Model = self.env['test_new_api.model_active_field']
2428        parent = Model.create({})
2429        all_children = Model.create([
2430            {'parent_id': parent.id, 'active': True},
2431            {'parent_id': parent.id, 'active': False},
2432        ])
2433        act_children = all_children[0]
2434
2435        self.assertEqual(parent.children_ids, act_children)
2436        self.assertEqual(parent.with_context(active_test=True).children_ids, act_children)
2437        self.assertEqual(parent.with_context(active_test=False).children_ids, all_children)
2438
2439        self.assertEqual(parent.all_children_ids, all_children)
2440        self.assertEqual(parent.with_context(active_test=True).all_children_ids, all_children)
2441        self.assertEqual(parent.with_context(active_test=False).all_children_ids, all_children)
2442
2443        self.assertEqual(parent.active_children_ids, act_children)
2444        self.assertEqual(parent.with_context(active_test=True).active_children_ids, act_children)
2445        self.assertEqual(parent.with_context(active_test=False).active_children_ids, act_children)
2446
2447        # check read()
2448        self.env.cache.invalidate()
2449        self.assertEqual(parent.children_ids, act_children)
2450        self.assertEqual(parent.all_children_ids, all_children)
2451        self.assertEqual(parent.active_children_ids, act_children)
2452
2453        self.env.cache.invalidate()
2454        self.assertEqual(parent.with_context(active_test=False).children_ids, all_children)
2455        self.assertEqual(parent.with_context(active_test=False).all_children_ids, all_children)
2456        self.assertEqual(parent.with_context(active_test=False).active_children_ids, act_children)
2457
2458    def test_12_active_test_one2many_search(self):
2459        Model = self.env['test_new_api.model_active_field']
2460        parent = Model.create({})
2461        all_children = Model.create([
2462            {'name': 'A', 'parent_id': parent.id, 'active': True},
2463            {'name': 'B', 'parent_id': parent.id, 'active': False},
2464        ])
2465
2466        # a one2many field without context does not match its inactive children
2467        self.assertIn(parent, Model.search([('children_ids.name', '=', 'A')]))
2468        self.assertNotIn(parent, Model.search([('children_ids.name', '=', 'B')]))
2469
2470        # a one2many field with active_test=False matches its inactive children
2471        self.assertIn(parent, Model.search([('all_children_ids.name', '=', 'A')]))
2472        self.assertIn(parent, Model.search([('all_children_ids.name', '=', 'B')]))
2473
2474    def test_search_many2many(self):
2475        """ Tests search on many2many fields. """
2476        tags = self.env['test_new_api.multi.tag']
2477        tagA = tags.create({})
2478        tagB = tags.create({})
2479        tagC = tags.create({})
2480        recs = self.env['test_new_api.multi.line']
2481        recW = recs.create({})
2482        recX = recs.create({'tags': [(4, tagA.id)]})
2483        recY = recs.create({'tags': [(4, tagB.id)]})
2484        recZ = recs.create({'tags': [(4, tagA.id), (4, tagB.id)]})
2485        recs = recW + recX + recY + recZ
2486
2487        # test 'in'
2488        result = recs.search([('tags', 'in', (tagA + tagB).ids)])
2489        self.assertEqual(result, recX + recY + recZ)
2490
2491        result = recs.search([('tags', 'in', tagA.ids)])
2492        self.assertEqual(result, recX + recZ)
2493
2494        result = recs.search([('tags', 'in', tagB.ids)])
2495        self.assertEqual(result, recY + recZ)
2496
2497        result = recs.search([('tags', 'in', tagC.ids)])
2498        self.assertEqual(result, recs.browse())
2499
2500        result = recs.search([('tags', 'in', [])])
2501        self.assertEqual(result, recs.browse())
2502
2503        # test 'not in'
2504        result = recs.search([('id', 'in', recs.ids), ('tags', 'not in', (tagA + tagB).ids)])
2505        self.assertEqual(result, recs - recX - recY - recZ)
2506
2507        result = recs.search([('id', 'in', recs.ids), ('tags', 'not in', tagA.ids)])
2508        self.assertEqual(result, recs - recX - recZ)
2509
2510        result = recs.search([('id', 'in', recs.ids), ('tags', 'not in', tagB.ids)])
2511        self.assertEqual(result, recs - recY - recZ)
2512
2513        result = recs.search([('id', 'in', recs.ids), ('tags', 'not in', tagC.ids)])
2514        self.assertEqual(result, recs)
2515
2516        result = recs.search([('id', 'in', recs.ids), ('tags', 'not in', [])])
2517        self.assertEqual(result, recs)
2518
2519        # special case: compare with False
2520        result = recs.search([('id', 'in', recs.ids), ('tags', '=', False)])
2521        self.assertEqual(result, recW)
2522
2523        result = recs.search([('id', 'in', recs.ids), ('tags', '!=', False)])
2524        self.assertEqual(result, recs - recW)
2525
2526    def test_search_one2many(self):
2527        """ Tests search on one2many fields. """
2528        recs = self.env['test_new_api.multi']
2529        recX = recs.create({'lines': [(0, 0, {}), (0, 0, {})]})
2530        recY = recs.create({'lines': [(0, 0, {})]})
2531        recZ = recs.create({})
2532        recs = recX + recY + recZ
2533        line1, line2, line3 = recs.lines
2534        line4 = recs.create({'lines': [(0, 0, {})]}).lines
2535        line0 = line4.create({})
2536
2537        # test 'in'
2538        result = recs.search([('id', 'in', recs.ids), ('lines', 'in', (line1 + line2 + line3 + line4).ids)])
2539        self.assertEqual(result, recX + recY)
2540
2541        result = recs.search([('id', 'in', recs.ids), ('lines', 'in', (line1 + line3 + line4).ids)])
2542        self.assertEqual(result, recX + recY)
2543
2544        result = recs.search([('id', 'in', recs.ids), ('lines', 'in', (line1 + line4).ids)])
2545        self.assertEqual(result, recX)
2546
2547        result = recs.search([('id', 'in', recs.ids), ('lines', 'in', line4.ids)])
2548        self.assertEqual(result, recs.browse())
2549
2550        result = recs.search([('id', 'in', recs.ids), ('lines', 'in', [])])
2551        self.assertEqual(result, recs.browse())
2552
2553        # test 'not in'
2554        result = recs.search([('id', 'in', recs.ids), ('lines', 'not in', (line1 + line2 + line3).ids)])
2555        self.assertEqual(result, recs - recX - recY)
2556
2557        result = recs.search([('id', 'in', recs.ids), ('lines', 'not in', (line1 + line3).ids)])
2558        self.assertEqual(result, recs - recX - recY)
2559
2560        result = recs.search([('id', 'in', recs.ids), ('lines', 'not in', line1.ids)])
2561        self.assertEqual(result, recs - recX)
2562
2563        result = recs.search([('id', 'in', recs.ids), ('lines', 'not in', (line1 + line4).ids)])
2564        self.assertEqual(result, recs - recX)
2565
2566        result = recs.search([('id', 'in', recs.ids), ('lines', 'not in', line4.ids)])
2567        self.assertEqual(result, recs)
2568
2569        result = recs.search([('id', 'in', recs.ids), ('lines', 'not in', [])])
2570        self.assertEqual(result, recs)
2571
2572        # these cases are weird
2573        result = recs.search([('id', 'in', recs.ids), ('lines', 'not in', (line1 + line0).ids)])
2574        self.assertEqual(result, recs.browse())
2575
2576        result = recs.search([('id', 'in', recs.ids), ('lines', 'not in', line0.ids)])
2577        self.assertEqual(result, recs.browse())
2578
2579        # special case: compare with False
2580        result = recs.search([('id', 'in', recs.ids), ('lines', '=', False)])
2581        self.assertEqual(result, recZ)
2582
2583        result = recs.search([('id', 'in', recs.ids), ('lines', '!=', False)])
2584        self.assertEqual(result, recs - recZ)
2585
2586    def test_create_batch_m2m(self):
2587        lines = self.env['test_new_api.multi.line'].create([{
2588            'tags': [(0, 0, {'name': str(j)}) for j in range(3)],
2589        } for i in range(3)])
2590        self.assertEqual(len(lines), 3)
2591        for line in lines:
2592            self.assertEqual(len(line.tags), 3)
2593
2594    def test_custom_m2m(self):
2595        model_id = self.env['ir.model']._get_id('res.partner')
2596        field = self.env['ir.model.fields'].create({
2597            'name': 'x_foo',
2598            'field_description': 'Foo',
2599            'model_id': model_id,
2600            'ttype': 'many2many',
2601            'relation': 'res.country',
2602            'store': False,
2603        })
2604        self.assertTrue(field.unlink())
2605
2606
2607class TestHtmlField(common.TransactionCase):
2608
2609    def setUp(self):
2610        super(TestHtmlField, self).setUp()
2611        self.model = self.env['test_new_api.mixed']
2612
2613    def test_00_sanitize(self):
2614        self.assertEqual(self.model._fields['comment1'].sanitize, False)
2615        self.assertEqual(self.model._fields['comment2'].sanitize_attributes, True)
2616        self.assertEqual(self.model._fields['comment2'].strip_classes, False)
2617        self.assertEqual(self.model._fields['comment3'].sanitize_attributes, True)
2618        self.assertEqual(self.model._fields['comment3'].strip_classes, True)
2619
2620        some_ugly_html = """<p>Oops this should maybe be sanitized
2621% if object.some_field and not object.oriented:
2622<table>
2623    % if object.other_field:
2624    <tr style="margin: 0px; border: 10px solid black;">
2625        ${object.mako_thing}
2626        <td>
2627    </tr>
2628    <tr class="custom_class">
2629        This is some html.
2630    </tr>
2631    % endif
2632    <tr>
2633%if object.dummy_field:
2634        <p>Youpie</p>
2635%endif"""
2636
2637        record = self.model.create({
2638            'comment1': some_ugly_html,
2639            'comment2': some_ugly_html,
2640            'comment3': some_ugly_html,
2641            'comment4': some_ugly_html,
2642        })
2643
2644        self.assertEqual(record.comment1, some_ugly_html, 'Error in HTML field: content was sanitized but field has sanitize=False')
2645
2646        self.assertIn('<tr class="', record.comment2)
2647
2648        # sanitize should have closed tags left open in the original html
2649        self.assertIn('</table>', record.comment3, 'Error in HTML field: content does not seem to have been sanitized despise sanitize=True')
2650        self.assertIn('</td>', record.comment3, 'Error in HTML field: content does not seem to have been sanitized despise sanitize=True')
2651        self.assertIn('<tr style="', record.comment3, 'Style attr should not have been stripped')
2652        # sanitize does not keep classes if asked to
2653        self.assertNotIn('<tr class="', record.comment3)
2654
2655        self.assertNotIn('<tr style="', record.comment4, 'Style attr should have been stripped')
2656
2657
2658class TestMagicFields(common.TransactionCase):
2659
2660    def test_write_date(self):
2661        record = self.env['test_new_api.discussion'].create({'name': 'Booba'})
2662        self.assertEqual(record.create_uid, self.env.user)
2663        self.assertEqual(record.write_uid, self.env.user)
2664
2665    def test_mro_mixin(self):
2666        #                               Mixin
2667        #                                |
2668        #                                |
2669        #                                |
2670        #   ExtendedDisplay    'test_new_api.mixin'    Display    'base'
2671        #         |                      |                |         |
2672        #         +----------------------+-+--------------+---------+
2673        #                                  |
2674        #                       'test_new_api.display'
2675        #
2676        # The field 'display_name' is defined as store=True on the class Display
2677        # above.  The field 'display_name' on the model 'test_new_api.mixin' is
2678        # expected to be automatic and non-stored.  But the field 'display_name'
2679        # on the model 'test_new_api.display' should not be automatic: it must
2680        # correspond to the definition given in class Display, even if the MRO
2681        # of the model shows the automatic field on the mixin model before the
2682        # actual definition.
2683        registry = self.env.registry
2684        models = registry.models
2685
2686        # check setup of models in alphanumeric order
2687        self.patch(registry, 'models', OrderedDict(sorted(models.items())))
2688        registry.model_cache.clear()
2689        registry.setup_models(self.cr)
2690        field = registry['test_new_api.display'].display_name
2691        self.assertFalse(field.automatic)
2692        self.assertTrue(field.store)
2693
2694        # check setup of models in reverse alphanumeric order
2695        self.patch(registry, 'models', OrderedDict(sorted(models.items(), reverse=True)))
2696        registry.model_cache.clear()
2697        registry.setup_models(self.cr)
2698        field = registry['test_new_api.display'].display_name
2699        self.assertFalse(field.automatic)
2700        self.assertTrue(field.store)
2701
2702
2703class TestParentStore(common.TransactionCase):
2704
2705    def setUp(self):
2706        super(TestParentStore, self).setUp()
2707        # make a tree of categories:
2708        #   0
2709        #  /|\
2710        # 1 2 3
2711        #    /|\
2712        #   4 5 6
2713        #      /|\
2714        #     7 8 9
2715        Cat = self.env['test_new_api.category']
2716        cat0 = Cat.create({'name': '0'})
2717        cat1 = Cat.create({'name': '1', 'parent': cat0.id})
2718        cat2 = Cat.create({'name': '2', 'parent': cat0.id})
2719        cat3 = Cat.create({'name': '3', 'parent': cat0.id})
2720        cat4 = Cat.create({'name': '4', 'parent': cat3.id})
2721        cat5 = Cat.create({'name': '5', 'parent': cat3.id})
2722        cat6 = Cat.create({'name': '6', 'parent': cat3.id})
2723        cat7 = Cat.create({'name': '7', 'parent': cat6.id})
2724        cat8 = Cat.create({'name': '8', 'parent': cat6.id})
2725        cat9 = Cat.create({'name': '9', 'parent': cat6.id})
2726        self._cats = Cat.concat(cat0, cat1, cat2, cat3, cat4,
2727                                cat5, cat6, cat7, cat8, cat9)
2728
2729    def cats(self, *indexes):
2730        """ Return the given categories. """
2731        ids = self._cats.ids
2732        return self._cats.browse([ids[index] for index in indexes])
2733
2734    def assertChildOf(self, category, children):
2735        self.assertEqual(category.search([('id', 'child_of', category.ids)]), children)
2736
2737    def assertParentOf(self, category, parents):
2738        self.assertEqual(category.search([('id', 'parent_of', category.ids)]), parents)
2739
2740    def test_base(self):
2741        """ Check the initial tree structure. """
2742        self.assertChildOf(self.cats(0), self.cats(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
2743        self.assertChildOf(self.cats(1), self.cats(1))
2744        self.assertChildOf(self.cats(2), self.cats(2))
2745        self.assertChildOf(self.cats(3), self.cats(3, 4, 5, 6, 7, 8, 9))
2746        self.assertChildOf(self.cats(4), self.cats(4))
2747        self.assertChildOf(self.cats(5), self.cats(5))
2748        self.assertChildOf(self.cats(6), self.cats(6, 7, 8, 9))
2749        self.assertChildOf(self.cats(7), self.cats(7))
2750        self.assertChildOf(self.cats(8), self.cats(8))
2751        self.assertChildOf(self.cats(9), self.cats(9))
2752        self.assertParentOf(self.cats(0), self.cats(0))
2753        self.assertParentOf(self.cats(1), self.cats(0, 1))
2754        self.assertParentOf(self.cats(2), self.cats(0, 2))
2755        self.assertParentOf(self.cats(3), self.cats(0, 3))
2756        self.assertParentOf(self.cats(4), self.cats(0, 3, 4))
2757        self.assertParentOf(self.cats(5), self.cats(0, 3, 5))
2758        self.assertParentOf(self.cats(6), self.cats(0, 3, 6))
2759        self.assertParentOf(self.cats(7), self.cats(0, 3, 6, 7))
2760        self.assertParentOf(self.cats(8), self.cats(0, 3, 6, 8))
2761        self.assertParentOf(self.cats(9), self.cats(0, 3, 6, 9))
2762
2763    def test_base_compute(self):
2764        """ Check the tree structure after computation from scratch. """
2765        self.cats()._parent_store_compute()
2766        self.assertChildOf(self.cats(0), self.cats(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
2767        self.assertChildOf(self.cats(1), self.cats(1))
2768        self.assertChildOf(self.cats(2), self.cats(2))
2769        self.assertChildOf(self.cats(3), self.cats(3, 4, 5, 6, 7, 8, 9))
2770        self.assertChildOf(self.cats(4), self.cats(4))
2771        self.assertChildOf(self.cats(5), self.cats(5))
2772        self.assertChildOf(self.cats(6), self.cats(6, 7, 8, 9))
2773        self.assertChildOf(self.cats(7), self.cats(7))
2774        self.assertChildOf(self.cats(8), self.cats(8))
2775        self.assertChildOf(self.cats(9), self.cats(9))
2776        self.assertParentOf(self.cats(0), self.cats(0))
2777        self.assertParentOf(self.cats(1), self.cats(0, 1))
2778        self.assertParentOf(self.cats(2), self.cats(0, 2))
2779        self.assertParentOf(self.cats(3), self.cats(0, 3))
2780        self.assertParentOf(self.cats(4), self.cats(0, 3, 4))
2781        self.assertParentOf(self.cats(5), self.cats(0, 3, 5))
2782        self.assertParentOf(self.cats(6), self.cats(0, 3, 6))
2783        self.assertParentOf(self.cats(7), self.cats(0, 3, 6, 7))
2784        self.assertParentOf(self.cats(8), self.cats(0, 3, 6, 8))
2785        self.assertParentOf(self.cats(9), self.cats(0, 3, 6, 9))
2786
2787    def test_delete(self):
2788        """ Delete a node. """
2789        self.cats(6).unlink()
2790        self.assertChildOf(self.cats(0), self.cats(0, 1, 2, 3, 4, 5))
2791        self.assertChildOf(self.cats(3), self.cats(3, 4, 5))
2792        self.assertChildOf(self.cats(5), self.cats(5))
2793        self.assertParentOf(self.cats(0), self.cats(0))
2794        self.assertParentOf(self.cats(3), self.cats(0, 3))
2795        self.assertParentOf(self.cats(5), self.cats(0, 3, 5))
2796
2797    def test_move_1_0(self):
2798        """ Move a node to a root position. """
2799        self.cats(6).write({'parent': False})
2800        self.assertChildOf(self.cats(0), self.cats(0, 1, 2, 3, 4, 5))
2801        self.assertChildOf(self.cats(3), self.cats(3, 4, 5))
2802        self.assertChildOf(self.cats(6), self.cats(6, 7, 8, 9))
2803        self.assertParentOf(self.cats(9), self.cats(6, 9))
2804
2805    def test_move_1_1(self):
2806        """ Move a node into an empty subtree. """
2807        self.cats(6).write({'parent': self.cats(1).id})
2808        self.assertChildOf(self.cats(0), self.cats(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
2809        self.assertChildOf(self.cats(1), self.cats(1, 6, 7, 8, 9))
2810        self.assertChildOf(self.cats(3), self.cats(3, 4, 5))
2811        self.assertChildOf(self.cats(6), self.cats(6, 7, 8, 9))
2812        self.assertParentOf(self.cats(9), self.cats(0, 1, 6, 9))
2813
2814    def test_move_1_N(self):
2815        """ Move a node into a non-empty subtree. """
2816        self.cats(6).write({'parent': self.cats(0).id})
2817        self.assertChildOf(self.cats(0), self.cats(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
2818        self.assertChildOf(self.cats(3), self.cats(3, 4, 5))
2819        self.assertChildOf(self.cats(6), self.cats(6, 7, 8, 9))
2820        self.assertParentOf(self.cats(9), self.cats(0, 6, 9))
2821
2822    def test_move_N_0(self):
2823        """ Move multiple nodes to root position. """
2824        self.cats(5, 6).write({'parent': False})
2825        self.assertChildOf(self.cats(0), self.cats(0, 1, 2, 3, 4))
2826        self.assertChildOf(self.cats(3), self.cats(3, 4))
2827        self.assertChildOf(self.cats(5), self.cats(5))
2828        self.assertChildOf(self.cats(6), self.cats(6, 7, 8, 9))
2829        self.assertParentOf(self.cats(5), self.cats(5))
2830        self.assertParentOf(self.cats(9), self.cats(6, 9))
2831
2832    def test_move_N_1(self):
2833        """ Move multiple nodes to an empty subtree. """
2834        self.cats(5, 6).write({'parent': self.cats(1).id})
2835        self.assertChildOf(self.cats(0), self.cats(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
2836        self.assertChildOf(self.cats(1), self.cats(1, 5, 6, 7, 8, 9))
2837        self.assertChildOf(self.cats(3), self.cats(3, 4))
2838        self.assertChildOf(self.cats(5), self.cats(5))
2839        self.assertChildOf(self.cats(6), self.cats(6, 7, 8, 9))
2840        self.assertParentOf(self.cats(5), self.cats(0, 1, 5))
2841        self.assertParentOf(self.cats(9), self.cats(0, 1, 6, 9))
2842
2843    def test_move_N_N(self):
2844        """ Move multiple nodes to a non- empty subtree. """
2845        self.cats(5, 6).write({'parent': self.cats(0).id})
2846        self.assertChildOf(self.cats(0), self.cats(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
2847        self.assertChildOf(self.cats(3), self.cats(3, 4))
2848        self.assertChildOf(self.cats(5), self.cats(5))
2849        self.assertChildOf(self.cats(6), self.cats(6, 7, 8, 9))
2850        self.assertParentOf(self.cats(5), self.cats(0, 5))
2851        self.assertParentOf(self.cats(9), self.cats(0, 6, 9))
2852
2853    def test_move_1_cycle(self):
2854        """ Move a node to create a cycle. """
2855        with self.assertRaises(UserError):
2856            self.cats(3).write({'parent': self.cats(9).id})
2857
2858    def test_move_N_cycle(self):
2859        """ Move multiple nodes to create a cycle. """
2860        with self.assertRaises(UserError):
2861            self.cats(1, 3).write({'parent': self.cats(9).id})
2862
2863
2864class TestRequiredMany2one(common.TransactionCase):
2865
2866    def test_explicit_ondelete(self):
2867        field = self.env['test_new_api.req_m2o']._fields['foo']
2868        self.assertEqual(field.ondelete, 'cascade')
2869
2870    def test_implicit_ondelete(self):
2871        field = self.env['test_new_api.req_m2o']._fields['bar']
2872        self.assertEqual(field.ondelete, 'restrict')
2873
2874    def test_explicit_set_null(self):
2875        Model = self.env['test_new_api.req_m2o']
2876        field = Model._fields['foo']
2877
2878        # invalidate registry to redo the setup afterwards
2879        self.registry.registry_invalidated = True
2880        self.patch(field, 'ondelete', 'set null')
2881
2882        with self.assertRaises(ValueError):
2883            field._setup_regular_base(Model)
2884
2885
2886class TestRequiredMany2oneTransient(common.TransactionCase):
2887
2888    def test_explicit_ondelete(self):
2889        field = self.env['test_new_api.req_m2o_transient']._fields['foo']
2890        self.assertEqual(field.ondelete, 'restrict')
2891
2892    def test_implicit_ondelete(self):
2893        field = self.env['test_new_api.req_m2o_transient']._fields['bar']
2894        self.assertEqual(field.ondelete, 'cascade')
2895
2896    def test_explicit_set_null(self):
2897        Model = self.env['test_new_api.req_m2o_transient']
2898        field = Model._fields['foo']
2899
2900        # invalidate registry to redo the setup afterwards
2901        self.registry.registry_invalidated = True
2902        self.patch(field, 'ondelete', 'set null')
2903
2904        with self.assertRaises(ValueError):
2905            field._setup_regular_base(Model)
2906
2907
2908@common.tagged('m2oref')
2909class TestMany2oneReference(common.TransactionCase):
2910
2911    def test_delete_m2o_reference_records(self):
2912        m = self.env['test_new_api.model_many2one_reference']
2913        self.env.cr.execute("SELECT max(id) FROM test_new_api_model_many2one_reference")
2914        ids = self.env.cr.fetchone()
2915        # fake record to emulate the unlink of a non-existant record
2916        foo = m.browse(1 if not ids[0] else (ids[0] + 1))
2917        self.assertTrue(foo.unlink())
2918
2919
2920@common.tagged('selection_abstract')
2921class TestSelectionDeleteUpdate(common.TransactionCase):
2922
2923    MODEL_ABSTRACT = 'test_new_api.state_mixin'
2924
2925    def setUp(self):
2926        super().setUp()
2927        # enable unlinking ir.model.fields.selection
2928        self.patch(self.registry, 'ready', False)
2929
2930    def test_unlink_asbtract(self):
2931        self.env['ir.model.fields.selection'].search([
2932            ('field_id.model', '=', self.MODEL_ABSTRACT),
2933            ('field_id.name', '=', 'state'),
2934            ('value', '=', 'confirmed'),
2935        ], limit=1).unlink()
2936
2937
2938@common.tagged('selection_ondelete_base')
2939class TestSelectionOndelete(common.TransactionCase):
2940
2941    MODEL_BASE = 'test_new_api.model_selection_base'
2942    MODEL_REQUIRED = 'test_new_api.model_selection_required'
2943    MODEL_NONSTORED = 'test_new_api.model_selection_non_stored'
2944    MODEL_WRITE_OVERRIDE = 'test_new_api.model_selection_required_for_write_override'
2945
2946    def setUp(self):
2947        super().setUp()
2948        # enable unlinking ir.model.fields.selection
2949        self.patch(self.registry, 'ready', False)
2950
2951    def _unlink_option(self, model, option):
2952        self.env['ir.model.fields.selection'].search([
2953            ('field_id.model', '=', model),
2954            ('field_id.name', '=', 'my_selection'),
2955            ('value', '=', option),
2956        ], limit=1).unlink()
2957
2958    def test_ondelete_default(self):
2959        # create some records, one of which having the extended selection option
2960        rec1 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'foo'})
2961        rec2 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'bar'})
2962        rec3 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'baz'})
2963
2964        # test that all values are correct before the removal of the value
2965        self.assertEqual(rec1.my_selection, 'foo')
2966        self.assertEqual(rec2.my_selection, 'bar')
2967        self.assertEqual(rec3.my_selection, 'baz')
2968
2969        # unlink the extended option (simulates a module uninstall)
2970        self._unlink_option(self.MODEL_REQUIRED, 'baz')
2971
2972        # verify that the ondelete policy has succesfully been applied
2973        self.assertEqual(rec1.my_selection, 'foo')
2974        self.assertEqual(rec2.my_selection, 'bar')
2975        self.assertEqual(rec3.my_selection, 'foo')   # reset to default
2976
2977    def test_ondelete_base_null_explicit(self):
2978        rec1 = self.env[self.MODEL_BASE].create({'my_selection': 'foo'})
2979        rec2 = self.env[self.MODEL_BASE].create({'my_selection': 'bar'})
2980        rec3 = self.env[self.MODEL_BASE].create({'my_selection': 'quux'})
2981
2982        self.assertEqual(rec1.my_selection, 'foo')
2983        self.assertEqual(rec2.my_selection, 'bar')
2984        self.assertEqual(rec3.my_selection, 'quux')
2985
2986        self._unlink_option(self.MODEL_BASE, 'quux')
2987
2988        self.assertEqual(rec1.my_selection, 'foo')
2989        self.assertEqual(rec2.my_selection, 'bar')
2990        self.assertFalse(rec3.my_selection)
2991
2992    def test_ondelete_base_null_implicit(self):
2993        rec1 = self.env[self.MODEL_BASE].create({'my_selection': 'foo'})
2994        rec2 = self.env[self.MODEL_BASE].create({'my_selection': 'bar'})
2995        rec3 = self.env[self.MODEL_BASE].create({'my_selection': 'ham'})
2996
2997        self.assertEqual(rec1.my_selection, 'foo')
2998        self.assertEqual(rec2.my_selection, 'bar')
2999        self.assertEqual(rec3.my_selection, 'ham')
3000
3001        self._unlink_option(self.MODEL_BASE, 'ham')
3002
3003        self.assertEqual(rec1.my_selection, 'foo')
3004        self.assertEqual(rec2.my_selection, 'bar')
3005        self.assertFalse(rec3.my_selection)
3006
3007    def test_ondelete_cascade(self):
3008        rec1 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'foo'})
3009        rec2 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'bar'})
3010        rec3 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'eggs'})
3011
3012        self.assertEqual(rec1.my_selection, 'foo')
3013        self.assertEqual(rec2.my_selection, 'bar')
3014        self.assertEqual(rec3.my_selection, 'eggs')
3015
3016        self._unlink_option(self.MODEL_REQUIRED, 'eggs')
3017
3018        self.assertEqual(rec1.my_selection, 'foo')
3019        self.assertEqual(rec2.my_selection, 'bar')
3020        self.assertFalse(rec3.exists())
3021
3022    def test_ondelete_literal(self):
3023        rec1 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'foo'})
3024        rec2 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'bar'})
3025        rec3 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'bacon'})
3026
3027        self.assertEqual(rec1.my_selection, 'foo')
3028        self.assertEqual(rec2.my_selection, 'bar')
3029        self.assertEqual(rec3.my_selection, 'bacon')
3030
3031        self._unlink_option(self.MODEL_REQUIRED, 'bacon')
3032
3033        self.assertEqual(rec1.my_selection, 'foo')
3034        self.assertEqual(rec2.my_selection, 'bar')
3035        self.assertEqual(rec3.my_selection, 'bar')
3036
3037    def test_ondelete_multiple_explicit(self):
3038        rec1 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'foo'})
3039        rec2 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'eevee'})
3040        rec3 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'pikachu'})
3041
3042        self.assertEqual(rec1.my_selection, 'foo')
3043        self.assertEqual(rec2.my_selection, 'eevee')
3044        self.assertEqual(rec3.my_selection, 'pikachu')
3045
3046        self._unlink_option(self.MODEL_REQUIRED, 'eevee')
3047        self._unlink_option(self.MODEL_REQUIRED, 'pikachu')
3048
3049        self.assertEqual(rec1.my_selection, 'foo')
3050        self.assertEqual(rec2.my_selection, 'bar')
3051        self.assertEqual(rec3.my_selection, 'foo')
3052
3053    def test_ondelete_callback(self):
3054        rec = self.env[self.MODEL_REQUIRED].create({'my_selection': 'knickers'})
3055
3056        self.assertEqual(rec.my_selection, 'knickers')
3057
3058        self._unlink_option(self.MODEL_REQUIRED, 'knickers')
3059
3060        self.assertEqual(rec.my_selection, 'foo')
3061        self.assertFalse(rec.active)
3062
3063    def test_non_stored_selection(self):
3064        rec = self.env[self.MODEL_NONSTORED].create({})
3065        rec.my_selection = 'foo'
3066
3067        self.assertEqual(rec.my_selection, 'foo')
3068
3069        self._unlink_option(self.MODEL_NONSTORED, 'foo')
3070
3071        self.assertFalse(rec.my_selection)
3072
3073    def test_required_base_selection_field(self):
3074        # test that no ondelete action is executed on a required selection field that is not
3075        # extended, only required fields that extend it with selection_add should
3076        # have ondelete actions defined
3077        rec = self.env[self.MODEL_REQUIRED].create({'my_selection': 'foo'})
3078        self.assertEqual(rec.my_selection, 'foo')
3079
3080        self._unlink_option(self.MODEL_REQUIRED, 'foo')
3081        self.assertEqual(rec.my_selection, 'foo')
3082
3083    @mute_logger('odoo.addons.base.models.ir_model')
3084    def test_write_override_selection(self):
3085        # test that on override to write that raises an error does not prevent the ondelete
3086        # policy from executing and cleaning up what needs to be cleaned up
3087        rec = self.env[self.MODEL_WRITE_OVERRIDE].create({'my_selection': 'divinity'})
3088        self.assertEqual(rec.my_selection, 'divinity')
3089
3090        self._unlink_option(self.MODEL_WRITE_OVERRIDE, 'divinity')
3091        self.assertEqual(rec.my_selection, 'foo')
3092
3093
3094@common.tagged('selection_ondelete_advanced')
3095class TestSelectionOndeleteAdvanced(common.TransactionCase):
3096
3097    MODEL_BASE = 'test_new_api.model_selection_base'
3098    MODEL_REQUIRED = 'test_new_api.model_selection_required'
3099
3100    def setUp(self):
3101        super().setUp()
3102        # necessary cleanup for resetting changes in the registry
3103        for model_name in (self.MODEL_BASE, self.MODEL_REQUIRED):
3104            Model = self.registry[model_name]
3105            self.addCleanup(setattr, Model, '__bases__', Model.__bases__)
3106        self.addCleanup(self.registry.model_cache.clear)
3107
3108    def test_ondelete_unexisting_policy(self):
3109        class Foo(models.Model):
3110            _module = None
3111            _inherit = self.MODEL_REQUIRED
3112
3113            my_selection = fields.Selection(selection_add=[
3114                ('random', "Random stuff"),
3115            ], ondelete={'random': 'poop'})
3116
3117        Foo._build_model(self.registry, self.env.cr)
3118
3119        with self.assertRaises(ValueError):
3120            self.registry.setup_models(self.env.cr)
3121
3122    def test_ondelete_default_no_default(self):
3123        class Foo(models.Model):
3124            _module = None
3125            _inherit = self.MODEL_BASE
3126
3127            my_selection = fields.Selection(selection_add=[
3128                ('corona', "Corona beers suck"),
3129            ], ondelete={'corona': 'set default'})
3130
3131        Foo._build_model(self.registry, self.env.cr)
3132
3133        with self.assertRaises(AssertionError):
3134            self.registry.setup_models(self.env.cr)
3135
3136    def test_ondelete_required_null_explicit(self):
3137        class Foo(models.Model):
3138            _module = None
3139            _inherit = self.MODEL_REQUIRED
3140
3141            my_selection = fields.Selection(selection_add=[
3142                ('brap', "Brap"),
3143            ], ondelete={'brap': 'set null'})
3144
3145        Foo._build_model(self.registry, self.env.cr)
3146
3147        with self.assertRaises(ValueError):
3148            self.registry.setup_models(self.env.cr)
3149
3150    def test_ondelete_required_null_implicit(self):
3151        class Foo(models.Model):
3152            _module = None
3153            _inherit = self.MODEL_REQUIRED
3154
3155            my_selection = fields.Selection(selection_add=[
3156                ('boing', "Boyoyoyoing"),
3157            ])
3158
3159        Foo._build_model(self.registry, self.env.cr)
3160
3161        with self.assertRaises(ValueError):
3162            self.registry.setup_models(self.env.cr)
3163
3164
3165class TestFieldParametersValidation(common.TransactionCase):
3166    def test_invalid_parameter(self):
3167        self.addCleanup(self.registry.model_cache.clear)
3168
3169        class Foo(models.Model):
3170            _module = None
3171            _name = _description = 'test_new_api.field_parameter_validation'
3172
3173            name = fields.Char(invalid_parameter=42)
3174
3175        Foo._build_model(self.registry, self.env.cr)
3176        self.addCleanup(self.registry.__delitem__, Foo._name)
3177
3178        with self.assertLogs('odoo.fields', level='WARNING') as cm:
3179            self.registry.setup_models(self.env.cr)
3180
3181        self.assertTrue(cm.output[0].startswith(
3182            "WARNING:odoo.fields:Field test_new_api.field_parameter_validation.name: "
3183            "unknown parameter 'invalid_parameter'"
3184        ))
3185
3186
3187def insert(model, *fnames):
3188    """ Return the expected query string to INSERT the given columns. """
3189    columns = ['create_uid', 'create_date', 'write_uid', 'write_date'] + sorted(fnames)
3190    return 'INSERT INTO "{}" ("id", {}) VALUES (nextval(%s), {}) RETURNING id'.format(
3191        model._table,
3192        ", ".join('"{}"'.format(column) for column in columns),
3193        ", ".join('%s' for column in columns),
3194    )
3195
3196
3197def update(model, *fnames):
3198    """ Return the expected query string to UPDATE the given columns. """
3199    columns = sorted(fnames) + ['write_uid', 'write_date']
3200    return 'UPDATE "{}" SET {} WHERE id IN %s'.format(
3201        model._table,
3202        ", ".join('"{}" = %s'.format(column) for column in columns),
3203    )
3204
3205
3206class TestComputeQueries(common.TransactionCase):
3207    """ Test the queries made by create() with computed fields. """
3208
3209    def test_compute_readonly(self):
3210        model = self.env['test_new_api.compute.readonly']
3211        model.create({})
3212
3213        # no value, no default
3214        with self.assertQueries([insert(model, 'foo'), update(model, 'bar')]):
3215            record = model.create({'foo': 'Foo'})
3216        self.assertEqual(record.bar, 'Foo')
3217
3218        # some value, no default
3219        with self.assertQueries([insert(model, 'foo', 'bar'), update(model, 'bar')]):
3220            record = model.create({'foo': 'Foo', 'bar': 'Bar'})
3221        self.assertEqual(record.bar, 'Foo')
3222
3223        model = model.with_context(default_bar='Def')
3224
3225        # no value, some default
3226        with self.assertQueries([insert(model, 'foo', 'bar'), update(model, 'bar')]):
3227            record = model.create({'foo': 'Foo'})
3228        self.assertEqual(record.bar, 'Foo')
3229
3230        # some value, some default
3231        with self.assertQueries([insert(model, 'foo', 'bar'), update(model, 'bar')]):
3232            record = model.create({'foo': 'Foo', 'bar': 'Bar'})
3233        self.assertEqual(record.bar, 'Foo')
3234
3235    def test_compute_readwrite(self):
3236        model = self.env['test_new_api.compute.readwrite']
3237        model.create({})
3238
3239        # no value, no default
3240        with self.assertQueries([insert(model, 'foo'), update(model, 'bar')]):
3241            record = model.create({'foo': 'Foo'})
3242        self.assertEqual(record.bar, 'Foo')
3243
3244        # some value, no default
3245        with self.assertQueries([insert(model, 'foo', 'bar')]):
3246            record = model.create({'foo': 'Foo', 'bar': 'Bar'})
3247        self.assertEqual(record.bar, 'Bar')
3248
3249        model = model.with_context(default_bar='Def')
3250
3251        # no value, some default
3252        with self.assertQueries([insert(model, 'foo', 'bar')]):
3253            record = model.create({'foo': 'Foo'})
3254        self.assertEqual(record.bar, 'Def')
3255
3256        # some value, some default
3257        with self.assertQueries([insert(model, 'foo', 'bar')]):
3258            record = model.create({'foo': 'Foo', 'bar': 'Bar'})
3259        self.assertEqual(record.bar, 'Bar')
3260
3261    def test_compute_inverse(self):
3262        model = self.env['test_new_api.compute.inverse']
3263        model.create({})
3264
3265        # no value, no default
3266        with self.assertQueries([insert(model, 'foo'), update(model, 'bar')]):
3267            record = model.create({'foo': 'Foo'})
3268        self.assertEqual(record.foo, 'Foo')
3269        self.assertEqual(record.bar, 'Foo')
3270
3271        # some value, no default
3272        with self.assertQueries([insert(model, 'foo', 'bar'), update(model, 'foo')]):
3273            record = model.create({'foo': 'Foo', 'bar': 'Bar'})
3274        self.assertEqual(record.foo, 'Bar')
3275        self.assertEqual(record.bar, 'Bar')
3276
3277        model = model.with_context(default_bar='Def')
3278
3279        # no value, some default
3280        with self.assertQueries([insert(model, 'foo', 'bar'), update(model, 'foo')]):
3281            record = model.create({'foo': 'Foo'})
3282        self.assertEqual(record.foo, 'Def')
3283        self.assertEqual(record.bar, 'Def')
3284
3285        # some value, some default
3286        with self.assertQueries([insert(model, 'foo', 'bar'), update(model, 'foo')]):
3287            record = model.create({'foo': 'Foo', 'bar': 'Bar'})
3288        self.assertEqual(record.foo, 'Bar')
3289        self.assertEqual(record.bar, 'Bar')
3290
3291class test_shared_cache(TransactionCaseWithUserDemo):
3292    def test_shared_cache_computed_field(self):
3293        # Test case: Check that the shared cache is not used if a compute_sudo stored field
3294        # is computed IF there is an ir.rule defined on this specific model.
3295
3296        # Real life example:
3297        # A user can only see its own timesheets on a task, but the field "Planned Hours",
3298        # which is stored-compute_sudo, should take all the timesheet lines into account
3299        # However, when adding a new line and then recomputing the value, no existing line
3300        # from another user is binded on self, then the value is erased and saved on the
3301        # database.
3302
3303        task = self.env['test_new_api.model_shared_cache_compute_parent'].create({
3304            'name': 'Shared Task'})
3305        self.env['test_new_api.model_shared_cache_compute_line'].create({
3306            'user_id': self.env.ref('base.user_admin').id,
3307            'parent_id': task.id,
3308            'amount': 1,
3309        })
3310        self.assertEqual(task.total_amount, 1)
3311
3312        self.env['base'].flush()
3313        task.invalidate_cache()  # Start fresh, as it would be the case on 2 different sessions.
3314
3315        task = task.with_user(self.user_demo)
3316        with common.Form(task) as task_form:
3317            # Use demo has no access to the already existing line
3318            self.assertEqual(len(task_form.line_ids), 0)
3319            # But see the real total_amount
3320            self.assertEqual(task_form.total_amount, 1)
3321            # Now let's add a new line (and retrigger the compute method)
3322            with task_form.line_ids.new() as line:
3323                line.amount = 2
3324            # The new value for total_amount, should be 3, not 2.
3325            self.assertEqual(task_form.total_amount, 2)
3326