1# -*- coding: utf-8 -*- 2# Part of Odoo. See LICENSE file for full copyright and licensing details. 3 4from datetime import date 5from psycopg2 import IntegrityError, ProgrammingError 6 7import odoo 8from odoo.exceptions import UserError, ValidationError, AccessError 9from odoo.tools import mute_logger 10from odoo.tests import common 11 12 13class TestServerActionsBase(common.TransactionCase): 14 15 def setUp(self): 16 super(TestServerActionsBase, self).setUp() 17 18 # Data on which we will run the server action 19 self.test_country = self.env['res.country'].create({ 20 'name': 'TestingCountry', 21 'code': 'TY', 22 'address_format': 'SuperFormat', 23 }) 24 self.test_partner = self.env['res.partner'].create({ 25 'name': 'TestingPartner', 26 'city': 'OrigCity', 27 'country_id': self.test_country.id, 28 }) 29 self.context = { 30 'active_model': 'res.partner', 31 'active_id': self.test_partner.id, 32 } 33 34 # Model data 35 Model = self.env['ir.model'] 36 Fields = self.env['ir.model.fields'] 37 self.res_partner_model = Model.search([('model', '=', 'res.partner')]) 38 self.res_partner_name_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'name')]) 39 self.res_partner_city_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'city')]) 40 self.res_partner_country_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'country_id')]) 41 self.res_partner_parent_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'parent_id')]) 42 self.res_partner_children_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'child_ids')]) 43 self.res_partner_category_field = Fields.search([('model', '=', 'res.partner'), ('name', '=', 'category_id')]) 44 self.res_country_model = Model.search([('model', '=', 'res.country')]) 45 self.res_country_name_field = Fields.search([('model', '=', 'res.country'), ('name', '=', 'name')]) 46 self.res_country_code_field = Fields.search([('model', '=', 'res.country'), ('name', '=', 'code')]) 47 self.res_partner_category_model = Model.search([('model', '=', 'res.partner.category')]) 48 self.res_partner_category_name_field = Fields.search([('model', '=', 'res.partner.category'), ('name', '=', 'name')]) 49 50 # create server action to 51 self.action = self.env['ir.actions.server'].create({ 52 'name': 'TestAction', 53 'model_id': self.res_partner_model.id, 54 'model_name': 'res.partner', 55 'state': 'code', 56 'code': 'record.write({"comment": "MyComment"})', 57 }) 58 59 60class TestServerActions(TestServerActionsBase): 61 62 def test_00_action(self): 63 self.action.with_context(self.context).run() 64 self.assertEqual(self.test_partner.comment, 'MyComment', 'ir_actions_server: invalid condition check') 65 self.test_partner.write({'comment': False}) 66 67 # Do: create contextual action 68 self.action.create_action() 69 self.assertEqual(self.action.binding_model_id.model, 'res.partner') 70 71 # Do: remove contextual action 72 self.action.unlink_action() 73 self.assertFalse(self.action.binding_model_id) 74 75 def test_10_code(self): 76 self.action.write({ 77 'state': 'code', 78 'code': ("partner_name = record.name + '_code'\n" 79 "record.env['res.partner'].create({'name': partner_name})"), 80 }) 81 run_res = self.action.with_context(self.context).run() 82 self.assertFalse(run_res, 'ir_actions_server: code server action correctly finished should return False') 83 84 partners = self.test_partner.search([('name', 'ilike', 'TestingPartner_code')]) 85 self.assertEqual(len(partners), 1, 'ir_actions_server: 1 new partner should have been created') 86 87 def test_20_crud_create(self): 88 # Do: create a new record in another model 89 self.action.write({ 90 'state': 'object_create', 91 'crud_model_id': self.res_country_model.id, 92 'link_field_id': False, 93 'fields_lines': [(5,), 94 (0, 0, {'col1': self.res_country_name_field.id, 'value': 'record.name', 'evaluation_type': 'equation'}), 95 (0, 0, {'col1': self.res_country_code_field.id, 'value': 'record.name[0:2]', 'evaluation_type': 'equation'})], 96 }) 97 run_res = self.action.with_context(self.context).run() 98 self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False') 99 # Test: new country created 100 country = self.test_country.search([('name', 'ilike', 'TestingPartner')]) 101 self.assertEqual(len(country), 1, 'ir_actions_server: TODO') 102 self.assertEqual(country.code, 'TE', 'ir_actions_server: TODO') 103 104 def test_20_crud_create_link_many2one(self): 105 _city = 'TestCity' 106 _name = 'TestNew' 107 108 # Do: create a new record in the same model and link it with a many2one 109 self.action.write({ 110 'state': 'object_create', 111 'crud_model_id': self.action.model_id.id, 112 'link_field_id': self.res_partner_parent_field.id, 113 'fields_lines': [(0, 0, {'col1': self.res_partner_name_field.id, 'value': _name}), 114 (0, 0, {'col1': self.res_partner_city_field.id, 'value': _city})], 115 }) 116 run_res = self.action.with_context(self.context).run() 117 self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False') 118 # Test: new partner created 119 partner = self.test_partner.search([('name', 'ilike', _name)]) 120 self.assertEqual(len(partner), 1, 'ir_actions_server: TODO') 121 self.assertEqual(partner.city, _city, 'ir_actions_server: TODO') 122 # Test: new partner linked 123 self.assertEqual(self.test_partner.parent_id, partner, 'ir_actions_server: TODO') 124 125 def test_20_crud_create_link_one2many(self): 126 _name = 'TestNew' 127 128 # Do: create a new record in the same model and link it with a one2many 129 self.action.write({ 130 'state': 'object_create', 131 'crud_model_id': self.action.model_id.id, 132 'link_field_id': self.res_partner_children_field.id, 133 'fields_lines': [(0, 0, {'col1': self.res_partner_name_field.id, 'value': _name})], 134 }) 135 run_res = self.action.with_context(self.context).run() 136 self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False') 137 # Test: new partner created 138 partner = self.test_partner.search([('name', 'ilike', _name)]) 139 self.assertEqual(len(partner), 1, 'ir_actions_server: TODO') 140 self.assertEqual(partner.name, _name, 'ir_actions_server: TODO') 141 # Test: new partner linked 142 self.assertIn(partner, self.test_partner.child_ids, 'ir_actions_server: TODO') 143 144 def test_20_crud_create_link_many2many(self): 145 # Do: create a new record in another model 146 self.action.write({ 147 'state': 'object_create', 148 'crud_model_id': self.res_partner_category_model.id, 149 'link_field_id': self.res_partner_category_field.id, 150 'fields_lines': [(0, 0, {'col1': self.res_partner_category_name_field.id, 'value': 'record.name', 'evaluation_type': 'equation'})], 151 }) 152 run_res = self.action.with_context(self.context).run() 153 self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False') 154 # Test: new category created 155 category = self.env['res.partner.category'].search([('name', 'ilike', 'TestingPartner')]) 156 self.assertEqual(len(category), 1, 'ir_actions_server: TODO') 157 self.assertIn(category, self.test_partner.category_id) 158 159 def test_30_crud_write(self): 160 _name = 'TestNew' 161 162 # Do: update partner name 163 self.action.write({ 164 'state': 'object_write', 165 'fields_lines': [(0, 0, {'col1': self.res_partner_name_field.id, 'value': _name})], 166 }) 167 run_res = self.action.with_context(self.context).run() 168 self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False') 169 # Test: partner updated 170 partner = self.test_partner.search([('name', 'ilike', _name)]) 171 self.assertEqual(len(partner), 1, 'ir_actions_server: TODO') 172 self.assertEqual(partner.city, 'OrigCity', 'ir_actions_server: TODO') 173 174 @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') 175 def test_40_multi(self): 176 # Data: 2 server actions that will be nested 177 action1 = self.action.create({ 178 'name': 'Subaction1', 179 'sequence': 1, 180 'model_id': self.res_partner_model.id, 181 'state': 'code', 182 'code': 'action = {"type": "ir.actions.act_window"}', 183 }) 184 action2 = self.action.create({ 185 'name': 'Subaction2', 186 'sequence': 2, 187 'model_id': self.res_partner_model.id, 188 'crud_model_id': self.res_partner_model.id, 189 'state': 'object_create', 190 'fields_lines': [(0, 0, {'col1': self.res_partner_name_field.id, 'value': 'RaoulettePoiluchette'}), 191 (0, 0, {'col1': self.res_partner_city_field.id, 'value': 'TestingCity'})], 192 }) 193 action3 = self.action.create({ 194 'name': 'Subaction3', 195 'sequence': 3, 196 'model_id': self.res_partner_model.id, 197 'state': 'code', 198 'code': 'action = {"type": "ir.actions.act_url"}', 199 }) 200 self.action.write({ 201 'state': 'multi', 202 'child_ids': [(6, 0, [action1.id, action2.id, action3.id])], 203 }) 204 205 # Do: run the action 206 res = self.action.with_context(self.context).run() 207 208 # Test: new partner created 209 # currently res_partner overrides default['name'] whatever its value 210 partner = self.test_partner.search([('name', 'ilike', 'RaoulettePoiluchette')]) 211 self.assertEqual(len(partner), 1) 212 # Test: action returned 213 self.assertEqual(res.get('type'), 'ir.actions.act_url') 214 215 # Test loops 216 with self.assertRaises(ValidationError): 217 self.action.write({ 218 'child_ids': [(6, 0, [self.action.id])] 219 }) 220 221 def test_50_groups(self): 222 """ check the action is returned only for groups dedicated to user """ 223 Actions = self.env['ir.actions.actions'] 224 225 group0 = self.env['res.groups'].create({'name': 'country group'}) 226 227 self.context = { 228 'active_model': 'res.country', 229 'active_id': self.test_country.id, 230 } 231 232 # Do: update model and group 233 self.action.write({ 234 'model_id': self.res_country_model.id, 235 'binding_model_id': self.res_country_model.id, 236 'groups_id': [(4, group0.id, 0)], 237 'code': 'record.write({"vat_label": "VatFromTest"})', 238 }) 239 240 # Test: action is not returned 241 bindings = Actions.get_bindings('res.country') 242 self.assertFalse(bindings) 243 244 with self.assertRaises(AccessError): 245 self.action.with_context(self.context).run() 246 self.assertFalse(self.test_country.vat_label) 247 248 # add group to the user, and test again 249 self.env.user.write({'groups_id': [(4, group0.id)]}) 250 251 bindings = Actions.get_bindings('res.country') 252 self.assertItemsEqual(bindings.get('action'), self.action.read()) 253 254 self.action.with_context(self.context).run() 255 self.assertEqual(self.test_country.vat_label, 'VatFromTest', 'vat label should be changed to VatFromTest') 256 257 def test_60_sort(self): 258 """ check the actions sorted by sequence """ 259 Actions = self.env['ir.actions.actions'] 260 261 # Do: update model 262 self.action.write({ 263 'model_id': self.res_country_model.id, 264 'binding_model_id': self.res_country_model.id, 265 }) 266 self.action2 = self.action.copy({'name': 'TestAction2', 'sequence': 1}) 267 268 # Test: action returned by sequence 269 bindings = Actions.get_bindings('res.country') 270 self.assertEqual([vals.get('name') for vals in bindings['action']], ['TestAction2', 'TestAction']) 271 self.assertEqual([vals.get('sequence') for vals in bindings['action']], [1, 5]) 272 273 def test_70_copy_action(self): 274 # first check that the base case (reset state) works normally 275 r = self.env['ir.actions.todo'].create({ 276 'action_id': self.action.id, 277 'state': 'done', 278 }) 279 self.assertEqual(r.state, 'done') 280 self.assertEqual( 281 r.copy().state, 'open', 282 "by default state should be reset by copy" 283 ) 284 285 # then check that on server action we've changed that 286 self.assertEqual( 287 self.action.copy().state, 'code', 288 "copying a server action should not reset the state" 289 ) 290 291 def test_80_permission(self): 292 self.action.write({ 293 'state': 'code', 294 'code': """record.write({'date': datetime.date.today()})""", 295 }) 296 297 user_demo = self.env.ref("base.user_demo") 298 self_demo = self.action.with_user(user_demo.id) 299 300 # can write on contact partner 301 self.test_partner.type = "contact" 302 self.test_partner.with_user(user_demo.id).check_access_rule("write") 303 304 self_demo.with_context(self.context).run() 305 self.assertEqual(self.test_partner.date, date.today()) 306 307 # but can not write on private address 308 self.test_partner.type = "private" 309 with self.assertRaises(AccessError): 310 self.test_partner.with_user(user_demo.id).check_access_rule("write") 311 # nor execute a server action on it 312 with self.assertRaises(AccessError), mute_logger('odoo.addons.base.models.ir_actions'): 313 self_demo.with_context(self.context).run() 314 315 316class TestCustomFields(common.TransactionCase): 317 MODEL = 'res.partner' 318 COMODEL = 'res.users' 319 320 def setUp(self): 321 # check that the registry is properly reset 322 registry = odoo.registry() 323 fnames = set(registry[self.MODEL]._fields) 324 @self.addCleanup 325 def check_registry(): 326 assert set(registry[self.MODEL]._fields) == fnames 327 328 super(TestCustomFields, self).setUp() 329 330 # use a test cursor instead of a real cursor 331 self.registry.enter_test_mode(self.cr) 332 self.addCleanup(self.registry.leave_test_mode) 333 334 def create_field(self, name, *, field_type='char'): 335 """ create a custom field and return it """ 336 model = self.env['ir.model'].search([('model', '=', self.MODEL)]) 337 field = self.env['ir.model.fields'].create({ 338 'model_id': model.id, 339 'name': name, 340 'field_description': name, 341 'ttype': field_type, 342 }) 343 self.assertIn(name, self.env[self.MODEL]._fields) 344 return field 345 346 def create_view(self, name): 347 """ create a view with the given field name """ 348 return self.env['ir.ui.view'].create({ 349 'name': 'yet another view', 350 'model': self.MODEL, 351 'arch': '<tree string="X"><field name="%s"/></tree>' % name, 352 }) 353 354 def test_create_custom(self): 355 """ custom field names must be start with 'x_' """ 356 with self.assertRaises(ValidationError): 357 self.create_field('foo') 358 359 def test_rename_custom(self): 360 """ custom field names must be start with 'x_' """ 361 field = self.create_field('x_foo') 362 with self.assertRaises(ValidationError): 363 field.name = 'foo' 364 365 def test_create_valid(self): 366 """ field names must be valid pg identifiers """ 367 with self.assertRaises(ValidationError): 368 self.create_field('x_foo bar') 369 370 def test_rename_valid(self): 371 """ field names must be valid pg identifiers """ 372 field = self.create_field('x_foo') 373 with self.assertRaises(ValidationError): 374 field.name = 'x_foo bar' 375 376 def test_create_unique(self): 377 """ one cannot create two fields with the same name on a given model """ 378 self.create_field('x_foo') 379 with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'): 380 self.create_field('x_foo') 381 382 def test_rename_unique(self): 383 """ one cannot create two fields with the same name on a given model """ 384 field1 = self.create_field('x_foo') 385 field2 = self.create_field('x_bar') 386 with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'): 387 field2.name = field1.name 388 389 def test_remove_without_view(self): 390 """ try removing a custom field that does not occur in views """ 391 field = self.create_field('x_foo') 392 field.unlink() 393 394 def test_rename_without_view(self): 395 """ try renaming a custom field that does not occur in views """ 396 field = self.create_field('x_foo') 397 field.name = 'x_bar' 398 399 @mute_logger('odoo.addons.base.models.ir_ui_view') 400 def test_remove_with_view(self): 401 """ try removing a custom field that occurs in a view """ 402 field = self.create_field('x_foo') 403 self.create_view('x_foo') 404 405 # try to delete the field, this should fail but not modify the registry 406 with self.assertRaises(UserError): 407 field.unlink() 408 self.assertIn('x_foo', self.env[self.MODEL]._fields) 409 410 @mute_logger('odoo.addons.base.models.ir_ui_view') 411 def test_rename_with_view(self): 412 """ try renaming a custom field that occurs in a view """ 413 field = self.create_field('x_foo') 414 self.create_view('x_foo') 415 416 # try to delete the field, this should fail but not modify the registry 417 with self.assertRaises(UserError): 418 field.name = 'x_bar' 419 self.assertIn('x_foo', self.env[self.MODEL]._fields) 420 421 def test_unlink_with_inverse(self): 422 """ create a custom o2m and then delete its m2o inverse """ 423 model = self.env['ir.model']._get(self.MODEL) 424 comodel = self.env['ir.model']._get(self.COMODEL) 425 426 m2o_field = self.env['ir.model.fields'].create({ 427 'model_id': comodel.id, 428 'name': 'x_my_m2o', 429 'field_description': 'my_m2o', 430 'ttype': 'many2one', 431 'relation': self.MODEL, 432 }) 433 434 o2m_field = self.env['ir.model.fields'].create({ 435 'model_id': model.id, 436 'name': 'x_my_o2m', 437 'field_description': 'my_o2m', 438 'ttype': 'one2many', 439 'relation': self.COMODEL, 440 'relation_field': m2o_field.name, 441 }) 442 443 # normal mode: you cannot break dependencies 444 with self.assertRaises(UserError): 445 m2o_field.unlink() 446 447 # uninstall mode: unlink dependant fields 448 m2o_field.with_context(_force_unlink=True).unlink() 449 self.assertFalse(o2m_field.exists()) 450 451 def test_unlink_with_dependant(self): 452 """ create a computed field, then delete its dependency """ 453 # Also applies to compute fields 454 comodel = self.env['ir.model'].search([('model', '=', self.COMODEL)]) 455 456 field = self.create_field('x_my_char') 457 458 dependant = self.env['ir.model.fields'].create({ 459 'model_id': comodel.id, 460 'name': 'x_oh_boy', 461 'field_description': 'x_oh_boy', 462 'ttype': 'char', 463 'related': 'partner_id.x_my_char', 464 }) 465 466 # normal mode: you cannot break dependencies 467 with self.assertRaises(UserError): 468 field.unlink() 469 470 # uninstall mode: unlink dependant fields 471 field.with_context(_force_unlink=True).unlink() 472 self.assertFalse(dependant.exists()) 473 474 def test_create_binary(self): 475 """ binary custom fields should be created as attachment=True to avoid 476 bloating the DB when creating e.g. image fields via studio 477 """ 478 self.create_field('x_image', field_type='binary') 479 custom_binary = self.env[self.MODEL]._fields['x_image'] 480 481 self.assertTrue(custom_binary.attachment) 482 483 def test_related_field(self): 484 """ create a custom related field, and check filled values """ 485 # 486 # Add a custom field equivalent to the following definition: 487 # 488 # class Partner(models.Model) 489 # _inherit = 'res.partner' 490 # x_oh_boy = fields.Char(related="country_id.code", store=True) 491 # 492 493 # pick N=100 records in comodel 494 countries = self.env['res.country'].search([('code', '!=', False)], limit=100) 495 self.assertEqual(len(countries), 100, "Not enough records in comodel 'res.country'") 496 497 # create records in model, with N distinct values for the related field 498 partners = self.env['res.partner'].create([ 499 {'name': country.code, 'country_id': country.id} for country in countries 500 ]) 501 partners.flush() 502 503 # determine how many queries it takes to create a non-computed field 504 query_count = self.cr.sql_log_count 505 self.env['ir.model.fields'].create({ 506 'model_id': self.env['ir.model']._get_id('res.partner'), 507 'name': 'x_oh_box', 508 'field_description': 'x_oh_box', 509 'ttype': 'char', 510 }) 511 query_count = self.cr.sql_log_count - query_count 512 513 # create the related field, and assert it only takes 1 extra queries 514 with self.assertQueryCount(query_count + 1): 515 self.env['ir.model.fields'].create({ 516 'model_id': self.env['ir.model']._get_id('res.partner'), 517 'name': 'x_oh_boy', 518 'field_description': 'x_oh_boy', 519 'ttype': 'char', 520 'related': 'country_id.code', 521 'store': True, 522 }) 523 524 # check the computed values 525 for partner in partners: 526 self.assertEqual(partner.x_oh_boy, partner.country_id.code) 527 528 def test_selection(self): 529 """ custom selection field """ 530 Model = self.env[self.MODEL] 531 model = self.env['ir.model'].search([('model', '=', self.MODEL)]) 532 field = self.env['ir.model.fields'].create({ 533 'model_id': model.id, 534 'name': 'x_sel', 535 'field_description': "Custom Selection", 536 'ttype': 'selection', 537 'selection_ids': [ 538 (0, 0, {'value': 'foo', 'name': 'Foo', 'sequence': 0}), 539 (0, 0, {'value': 'bar', 'name': 'Bar', 'sequence': 1}), 540 ], 541 }) 542 543 x_sel = Model._fields['x_sel'] 544 self.assertEqual(x_sel.type, 'selection') 545 self.assertEqual(x_sel.selection, [('foo', 'Foo'), ('bar', 'Bar')]) 546 547 # add selection value 'baz' 548 field.selection_ids.create({ 549 'field_id': field.id, 'value': 'baz', 'name': 'Baz', 'sequence': 2, 550 }) 551 x_sel = Model._fields['x_sel'] 552 self.assertEqual(x_sel.type, 'selection') 553 self.assertEqual(x_sel.selection, [('foo', 'Foo'), ('bar', 'Bar'), ('baz', 'Baz')]) 554 555 # assign values to records 556 rec1 = Model.create({'name': 'Rec1', 'x_sel': 'foo'}) 557 rec2 = Model.create({'name': 'Rec2', 'x_sel': 'bar'}) 558 rec3 = Model.create({'name': 'Rec3', 'x_sel': 'baz'}) 559 self.assertEqual(rec1.x_sel, 'foo') 560 self.assertEqual(rec2.x_sel, 'bar') 561 self.assertEqual(rec3.x_sel, 'baz') 562 563 # remove selection value 'foo' 564 field.selection_ids[0].unlink() 565 x_sel = Model._fields['x_sel'] 566 self.assertEqual(x_sel.type, 'selection') 567 self.assertEqual(x_sel.selection, [('bar', 'Bar'), ('baz', 'Baz')]) 568 569 self.assertEqual(rec1.x_sel, False) 570 self.assertEqual(rec2.x_sel, 'bar') 571 self.assertEqual(rec3.x_sel, 'baz') 572 573 # update selection value 'bar' 574 field.selection_ids[0].value = 'quux' 575 x_sel = Model._fields['x_sel'] 576 self.assertEqual(x_sel.type, 'selection') 577 self.assertEqual(x_sel.selection, [('quux', 'Bar'), ('baz', 'Baz')]) 578 579 self.assertEqual(rec1.x_sel, False) 580 self.assertEqual(rec2.x_sel, 'quux') 581 self.assertEqual(rec3.x_sel, 'baz') 582