from unittest.mock import patch import odoo from odoo.tests.common import TransactionCase from odoo.addons.web_studio.models.ir_model import OPTIONS_WL from odoo.exceptions import ValidationError from odoo import Command class TestStudioIrModel(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() # The test mode is necessary in this case. After each test, we call # registry.reset_changes(), which opens a new cursor to retrieve custom # models and fields. A regular cursor would correspond to the state of # the database before setUpClass(), which is not correct. Instead, a # test cursor will correspond to the state of the database of cls.cr at # that point, i.e., before the call to setUp(). cls.registry.enter_test_mode(cls.cr) cls.addClassCleanup(cls.registry.leave_test_mode) cls.partner_elon = cls.env['res.partner'].create({ 'name': 'Elon Tusk', # 🐗 'email': 'elon@spacex.com', }) # custom m2m field between two models which don't have one yet cls.source_model = cls.env["ir.model"].search([("model", "=", "res.currency")]) cls.destination_model = cls.env["ir.model"].search( [("model", "=", "res.country.state")] ) cls.m2m = cls.env["ir.model.fields"].create( { "ttype": "many2many", "model_id": cls.source_model.id, "relation": cls.destination_model.model, "name": "x_state_ids", } ) def test_00_model_creation(self): """Test that a model gets created with the selected options.""" model_options = ['use_partner', 'use_stages', 'use_image', 'use_responsible', 'lines'] (model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(extra_models.mapped('name'), ['Rockets Stages'], 'Only stages should be returned') line_model = self.env['ir.model'].search([('model', 'like', model.model + '_line')]) self.assertEqual(len(line_model), 1, 'one extra model should have been created for lines') created_fields = self.env[model.model]._fields.keys() expected_fields = ['x_studio_partner_id', 'x_studio_stage_id', 'x_studio_image', 'x_studio_user_id', model.model + '_line_ids'] self.assertTrue(all(list(filter(lambda x: item in x, created_fields)) for item in expected_fields), 'some expected fields have not been created automatically') def test_01_mail_inheritance(self): """Test that the mail inheritance behaves as expected on custom models.""" model_options = ['use_partner', 'use_mail'] (model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_models), 0, 'no extra model should have been created') self.assertTrue(model.is_mail_thread, 'model should inherit from mail.thread') # create a record bfr = self.env[model.model].create({ 'x_name': 'Big Fucking Rocket', 'x_studio_partner_id': self.partner_elon.id, }) # ensure the partner is suggested in email and sms communication mail_suggested_recipients = bfr._message_get_suggested_recipients() self.assertIn((self.partner_elon.id, '"Elon Tusk" ', None, 'Contact'), mail_suggested_recipients.get(bfr.id), 'custom partner field should be suggested in mail communications') sms_suggested_recipients = bfr._sms_get_partner_fields() self.assertIn('x_studio_partner_id', sms_suggested_recipients, 'custom partner field should be included in sms communications') def test_02_model_option_active(self): """Test that the `active` behaviour is set up correctly.""" model_options = ['use_active', 'use_mail'] (model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_models), 0, 'no extra model should have been created') fields = self.env[model.model]._fields self.assertIn('x_active', fields, 'a custom active field should be set up') default = self.env['ir.default'].get(model.model, 'x_active') self.assertTrue(default, 'the default value for the x_active field should be True') active_field = self.env['ir.model.fields'].search([('name', '=', 'x_active'), ('model_id', '=', model.id)]) self.assertTrue(active_field.tracking, 'the x_active field should be tracked') def test_03_model_option_sequence(self): """Test that the `sequence` behaviour is set up correctly.""" model_options = ['use_sequence', 'use_mail'] (model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_models), 0, 'no extra model should have been created') fields = self.env[model.model]._fields self.assertIn('x_studio_sequence', fields, 'a custom sequence field should be set up') default = self.env['ir.default'].get(model.model, 'x_studio_sequence') self.assertEqual(default, 10, 'the default value for the x_studio_sequence field should be 10') def test_04_model_option_responsible(self): """Test that the `responsible` behaviour is set up correctly.""" model_options = ['use_responsible', 'use_mail'] (model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_models), 0, 'no extra model should have been created') fields = self.env[model.model]._fields self.assertIn('x_studio_user_id', fields, 'a custom responsible (res.users) field should be set up') resp_field = self.env['ir.model.fields'].search([('name', '=', 'x_studio_user_id'), ('model_id', '=', model.id)]) self.assertTrue(resp_field.tracking, 'the x_studio_user_id field should be tracked') def test_05_model_option_partner(self): """Test that the `partner` behaviour is set up correctly.""" model_options = ['use_partner', 'use_mail'] (model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_models), 0, 'no extra model should have been created') fields = self.env[model.model]._fields self.assertIn('x_studio_partner_id', fields, 'a custom partner field should be set up') self.assertIn('x_studio_partner_phone', fields, 'a related field x_studio_partner_phone should be set up') self.assertIn('x_studio_partner_email', fields, 'a related field x_studio_partner_email should be set up') partner_field = self.env['ir.model.fields'].search([('name', '=', 'x_studio_partner_id'), ('model_id', '=', model.id)]) self.assertTrue(partner_field.tracking, 'the x_studio_partner_id field should be tracked') def test_06_model_option_company(self): """Test that the `company` behaviour is set up correctly.""" model_options = ['use_company', 'use_mail'] (model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_models), 0, 'no extra model should have been created') fields = self.env[model.model]._fields self.assertIn('x_studio_company_id', fields, 'a custom company field should be set up') mc_rule = self.env['ir.rule'].search([ ('model_id', '=', model.id), ('domain_force', 'like', 'x_studio_company_id') ]) self.assertEqual(len(mc_rule), 1, 'there should be a multi-company rule for the model') comp_field = self.env['ir.model.fields'].search([('name', '=', 'x_studio_company_id'), ('model_id', '=', model.id)]) self.assertTrue(comp_field.tracking, 'the x_studio_company_id field should be tracked') main_company = self.env.ref('base.main_company') default = self.env['ir.default'].get(model.model, 'x_studio_company_id', company_id=main_company.id) self.assertEqual(default, main_company.id, 'the default value for the x_studio_company_id should be set') new_company = self.env['res.company'].create({'name': 'SpaceY'}) new_default = self.env['ir.default'].get(model.model, 'x_studio_company_id', company_id=new_company.id) self.assertEqual(new_default, new_company.id, 'default values for new companies should be created with the company') def test_07_model_option_notes(self): """Test that the `notes` behaviour is set up correctly.""" model_options = ['use_notes', 'use_mail'] (model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_models), 0, 'no extra model should have been created') fields = self.env[model.model]._fields self.assertIn('x_studio_notes', fields, 'a custom notes field should be set up') def test_08_model_option_date(self): """Test that the `date` behaviour is set up correctly.""" model_options = ['use_date', 'use_mail'] (model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_models), 0, 'no extra model should have been created') fields = self.env[model.model]._fields self.assertIn('x_studio_date', fields, 'a custom date field should be set up') date_field = self.env['ir.model.fields'].search([('name', '=', 'x_studio_date'), ('model_id', '=', model.id)]) self.assertFalse(date_field.tracking, 'the x_studio_date field should not be tracked') def test_09_model_option_double_dates(self): """Test that the `double date` behaviour is set up correctly.""" model_options = ['use_double_dates', 'use_mail'] (model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_models), 0, 'no extra model should have been created') fields = self.env[model.model]._fields self.assertIn('x_studio_date_start', fields, 'a custom start date field should be set up') self.assertIn('x_studio_date_stop', fields, 'a custom stop date field should be set up') date_fields = self.env['ir.model.fields'].search([('name', 'like', 'x_studio_date'), ('model_id', '=', model.id)]) for date_field in date_fields: self.assertFalse(date_field.tracking, 'start/stop date fields should not be tracked') def test_10_model_option_value(self): """Test that the `value` behaviour is set up correctly.""" model_options = ['use_value', 'use_mail'] (model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_models), 0, 'no extra model should have been created') fields = self.env[model.model]._fields self.assertIn('x_studio_currency_id', fields, 'a custom currency field should be set up') self.assertIn('x_studio_currency_id', fields, 'a custom value field should be set up') value_field = self.env['ir.model.fields'].search([('name', '=', 'x_studio_value'), ('model_id', '=', model.id)]) self.assertTrue(value_field.tracking, 'the x_studio_value field should be tracked') main_company = self.env.ref('base.main_company') default = self.env['ir.default'].get(model.model, 'x_studio_currency_id', company_id=main_company.id) self.assertEqual(default, main_company.currency_id.id, 'the default value for the x_studio_currency_id should be set') new_company = self.env['res.company'].create({'name': 'SpaceY', 'currency_id': self.env.ref('base.INR').id}) new_default = self.env['ir.default'].get(model.model, 'x_studio_currency_id', company_id=new_company.id) self.assertEqual(new_default, new_company.currency_id.id, 'default currency for new companies should be create with the company') def test_11_model_option_image(self): """Test that the `image` behaviour is set up correctly.""" model_options = ['use_image', 'use_mail'] (model, extra_models) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_models), 0, 'no extra model should have been created') fields = self.env[model.model]._fields self.assertIn('x_studio_image', fields, 'a custom image field should be set up') def test_12_model_option_stages(self): """Test that the `stage` behaviour is set up correctly.""" model_options = ['use_stages', 'use_mail'] (model, extra_model) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_model), 1, 'an extra model should have been created for stages') stage_fields = self.env[extra_model.model]._fields self.assertIn('x_studio_sequence', stage_fields, 'stages should have a sequence') fields = self.env[model.model]._fields self.assertIn('x_studio_stage_id', fields, 'a custom stage field should be set up') self.assertIn('x_studio_priority', fields, 'a custom priority field should be set up') self.assertIn('x_color', fields, 'a custom color field should be set up') self.assertIn('x_studio_kanban_state', fields, 'a custom kanban state field should be set up') auto_stage = self.env[extra_model.model].search([]) default = self.env['ir.default'].get(model.model, 'x_studio_stage_id') self.assertEqual(default, auto_stage.ids[0], 'the default stage should be set') stage_field = self.env['ir.model.fields'].search([('name', '=', 'x_studio_stage_id'), ('model_id', '=', model.id)]) self.assertTrue(stage_field.tracking, 'the x_studio_stage_id field should be tracked') def test_13_model_option_tags(self): """Test that the `tags` behaviour is set up correctly.""" model_options = ['use_tags'] (model, extra_model) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) self.assertEqual(len(extra_model), 1, 'an extra model should have been created for tags') stage_fields = self.env[extra_model.model]._fields self.assertIn('x_color', stage_fields, 'tags should have a color') fields = self.env[model.model]._fields self.assertIn('x_studio_tag_ids', fields, 'a custom tags field should be set up') def test_14_all_options(self): """Test auto-view generation for custom models with all options enabled.""" # Enable ALL THE OPTIONS (model, extra_model) = self.env['ir.model'].studio_model_create('Rockets', options=OPTIONS_WL) # I'm just checking it doesn't crash for now 👐 def test_15_custom_model_security(self): """Test that ACLs are created for a custom model.""" model_options = [] (model, _) = self.env['ir.model'].studio_model_create('Rockets', options=model_options) acl_admin = self.env['ir.model.access'].search([ ('model_id', '=', model.id), ('group_id', '=', self.env.ref('base.group_system').id) ]) self.assertTrue(acl_admin.perm_read, 'admin should have read access on custom models') self.assertTrue(acl_admin.perm_write, 'admin should have write access on custom models') self.assertTrue(acl_admin.perm_create, 'admin should have create access on custom models') self.assertTrue(acl_admin.perm_unlink, 'admin should have unlink access on custom models') acl_user = self.env['ir.model.access'].search([ ('model_id', '=', model.id), ('group_id', '=', self.env.ref('base.group_user').id) ]) self.assertTrue(acl_user.perm_read, 'user should have read access on custom models') self.assertTrue(acl_user.perm_write, 'user should have write access on custom models') self.assertTrue(acl_user.perm_create, 'user should have create access on custom models') self.assertFalse(acl_user.perm_unlink, 'user should not have unlink access on custom models') def test_16_next_relation(self): """Check that creating the same m2m will result in a new relation table.""" IrModelFields = self.env["ir.model.fields"].with_context(studio=True) current_table = IrModelFields._custom_many2many_names( "res.currency", "res.country.state" )[0] new_m2m = IrModelFields.create( { "ttype": "many2many", "model_id": self.source_model.id, "relation": self.destination_model.model, "name": "x_state_ids_2", "relation_table": IrModelFields._get_next_relation( self.source_model.model, self.destination_model.model ), } ) self.assertNotEqual( new_m2m.relation_table, current_table, "the second m2m should have its own relation table", ) def test_17_reverse_relation(self): IrModelFields = self.env["ir.model.fields"].with_context(studio=True) reverse_m2m = IrModelFields.create( { "ttype": "many2many", "model_id": self.destination_model.id, "relation": self.source_model.model, "name": "x_currency_ids", "relation_table": IrModelFields._get_next_relation( self.destination_model.model, self.source_model.model ), } ) self.assertEqual( self.m2m.relation_table, reverse_m2m.relation_table, "the second m2m should have the same relation table as the first m2m of the source model", ) new_m2m = IrModelFields.create( { "ttype": "many2many", "model_id": self.source_model.id, "relation": self.destination_model.model, "name": "x_state_ids_2", "relation_table": IrModelFields._get_next_relation( self.source_model.model, self.destination_model.model ), } ) reverse_new_m2m = IrModelFields.create( { "ttype": "many2many", "model_id": self.destination_model.id, "relation": self.source_model.model, "name": "x_currency_ids_2", "relation_table": IrModelFields._get_next_relation( self.destination_model.model, self.source_model.model ), } ) self.assertEqual( new_m2m.relation_table, reverse_new_m2m.relation_table, "the second reverse m2m should have the same relation table as the second m2m of the source model", ) def test_18_lots_of_relations(self): IrModelFields = self.env["ir.model.fields"].with_context(studio=True) NUM_TEST = 10 # because some people are just that stupid attempt = 0 while attempt < NUM_TEST: attempt += 1 IrModelFields.create( { "ttype": "many2many", "model_id": self.source_model.id, "relation": self.destination_model.model, "name": "x_currency_ids_%s" % attempt, "relation_table": IrModelFields._get_next_relation( self.source_model.model, self.destination_model.model ), } ) latest_relation = IrModelFields.search_read( [ ("ttype", "=", "many2many"), ("model_id", "=", self.source_model.id), ("relation", "=", self.destination_model.model), ], fields=["relation_table"], order="id desc", limit=1, ) default = IrModelFields._custom_many2many_names( self.source_model.model, self.destination_model.model )[0] self.assertEqual( latest_relation[0]["relation_table"], "%s_%s" % (default, NUM_TEST) ) def test_19_custom_model_security(self): """Test that ACLs are created for a custom model using name create.""" model_id, name = self.env['ir.model'].with_context(studio=True).name_create('X_Rockets') acl_admin = self.env['ir.model.access'].search([ ('model_id', '=', model_id), ('group_id', '=', self.env.ref('base.group_system').id) ]) self.assertTrue(acl_admin.perm_read, 'admin should have read access on custom models') self.assertTrue(acl_admin.perm_write, 'admin should have write access on custom models') self.assertTrue(acl_admin.perm_create, 'admin should have create access on custom models') self.assertTrue(acl_admin.perm_unlink, 'admin should have unlink access on custom models') acl_user = self.env['ir.model.access'].search([ ('model_id', '=', model_id), ('group_id', '=', self.env.ref('base.group_user').id) ]) self.assertTrue(acl_user.perm_read, 'user should have read access on custom models') self.assertTrue(acl_user.perm_write, 'user should have write access on custom models') self.assertTrue(acl_user.perm_create, 'user should have create access on custom models') self.assertFalse(acl_user.perm_unlink, 'user should not have unlink access on custom models') def test_20_prevent_double_underscore(self): IrModelFields = self.env["ir.model.fields"] with self.assertRaises(ValidationError, msg="Custom field names cannot contain double underscores."): IrModelFields.create( { "ttype": "char", "model_id": self.source_model.id, "name": "x_studio_hello___hap", } ) def test_21_set_view_mode_new_window_action(self): """Test that the `view_mode` for window action is set correctly.""" model = self.env['ir.model'].create({ 'name': 'Rockets', 'model': 'x_rockets', 'field_id': [ Command.create({'name': 'x_name', 'ttype': 'char', 'field_description': 'Name'}), ] }) action = model._create_default_action('x_rockets') self.assertEqual(action.view_mode, 'tree,form', 'tree and form should be set as a default view mode on window action') def test_22_rename_window_action(self): """ Test renaming a menu will rename the windows action.""" model = self.env['ir.model'].create({ 'name': 'Rockets', 'model': 'x_rockets', 'field_id': [ Command.create({'name': 'x_name', 'ttype': 'char', 'field_description': 'Name'}), ] }) action = model._create_default_action('Rockets') action_ref = 'ir.actions.act_window,' + str(action.id) new_menu = self.env['ir.ui.menu'].with_context(studio=True).create({ 'name': 'Rockets', 'action': action_ref, }) self.assertEqual(action.name, new_menu.name, 'action and menu name should be same') # rename the menu name new_menu.name = 'new Rockets' self.assertEqual(action.name, new_menu.name, 'rename the menu name should rename the window action name') def test_performance_01_fields_batch(self): """Test number of call to setup_models when creating a model with multiple""" count_setup_models = 0 orig_setup_models = odoo.modules.registry.Registry.setup_models def setup_models(registry, cr): nonlocal count_setup_models count_setup_models += 1 orig_setup_models(registry, cr) with patch('odoo.modules.registry.Registry.setup_models', new=setup_models): # not: using a specific model (PerformanceIssues and not Rockets) is important since after the rollback of the test, # the model will be missing but x_rockets is still in the pool, breaking some optimizations self.env['ir.model'].with_context(studio=True).studio_model_create('PerformanceIssues', options=OPTIONS_WL) self.assertEqual(count_setup_models, 1)