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