from lxml import etree from unittest.mock import patch import odoo from odoo.tests import tagged from odoo.tests.common import TransactionCase from odoo.addons.web_studio.wizard.studio_export_wizard import DEFAULT_MODELS_TO_EXPORT, FIELDS_TO_EXPORT, MODELS_WITH_NOUPDATE, RELATIONS_NOT_TO_EXPORT from odoo.addons.web_studio.models.studio_export_model import PRESET_MODELS_DEFAULTS, \ DEFAULT_FIELDS_TO_EXCLUDE, ABSTRACT_MODEL_FIELDS_TO_EXCLUDE, RELATED_MODELS_TO_EXCLUDE from odoo.addons.web_studio.controllers.export_utils import XML_FIELDS 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() 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 setUp(self): self.addCleanup(self.registry.reset_changes) super().setUp() 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( reply_discussion=True, no_create=False, ) self.assertDictEqual( { 'partner_id': self.partner_elon.id, 'name': 'Elon Tusk', 'email': 'elon@spacex.com', 'create_values': {}, }, mail_suggested_recipients[0], 'custom partner field should be suggested in mail communications', ) sms_suggested_recipients = bfr._mail_get_partner_fields(introspect_fields=False) self.assertIn('x_studio_partner_id', sms_suggested_recipients, 'custom partner field should be included in sms communications') self.assertTrue(model.is_mail_activity) # resist to field name changes across versions self.assertTrue("activity_user_id" in self.env[model.model]._fields) got_views = self.env[model.model].get_views([(False, "search")]) search_view = etree.fromstring(got_views["views"]["search"]["arch"]) expected_filters = { 'activities_overdue': "[('my_activity_date_deadline', '<', 'today')]", 'activities_today': "[('my_activity_date_deadline', '=', 'today')]", 'activities_upcoming_all': "[('my_activity_date_deadline', '>', 'today')]", 'filter_activities_my': "[['activity_user_id', '=', uid]]" } for filter_name, expected_domain in expected_filters.items(): filter_element = search_view.xpath(f"//filter[@name='{filter_name}']")[0] self.assertEqual(filter_element.get("domain"), expected_domain) 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') list_view = self.env["ir.ui.view"].search([("model", "=", model.model), ("type", "=", "list")], limit=1) tree = etree.fromstring(list_view.get_combined_arch()) self.assertEqual(tree.attrib['default_order'], 'x_studio_sequence asc,id desc') 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 always have a sequence even when the option is not ticked') fields = self.env[model.model]._fields self.assertNotIn('x_studio_sequence', fields, 'no custom sequence field is set up when the sequence option is not ticked') 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') list_view = self.env["ir.ui.view"].search([("model", "=", model.model), ("type", "=", "list")], limit=1) kanban_view = self.env["ir.ui.view"].search([("model", "=", model.model), ("type", "=", "kanban")], limit=1) tree = etree.fromstring(list_view.get_combined_arch()) self.assertEqual(tree.attrib['default_order'], 'x_studio_priority desc,id desc') tree = etree.fromstring(kanban_view.get_combined_arch()) self.assertEqual(tree.attrib['default_order'], 'x_studio_priority desc,id desc') 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, 'list,form', 'list 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_23_model_option_stages_with_sequence(self): """Test that the `stage` behaviour is set up correctly.""" model_options = ['use_stages', 'use_sequence', '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 always have a sequence') fields = self.env[extra_model.model]._fields self.assertIn('x_studio_sequence', fields, 'a custom sequence field is set up for the main model') list_view = self.env["ir.ui.view"].search([("model", "=", model.model), ("type", "=", "list")], limit=1) kanban_view = self.env["ir.ui.view"].search([("model", "=", model.model), ("type", "=", "kanban")], limit=1) tree = etree.fromstring(list_view.get_combined_arch()) self.assertEqual(tree.attrib['default_order'], 'x_studio_priority desc,x_studio_sequence asc,id desc') tree = etree.fromstring(kanban_view.get_combined_arch()) self.assertEqual(tree.attrib['default_order'], 'x_studio_priority desc,x_studio_sequence asc,id desc') sequence_field = tree.xpath("//field[@name='x_studio_sequence']")[0] self.assertEqual(sequence_field.attrib, {'name': 'x_studio_sequence', 'widget': 'handle', 'invisible': '1'}) 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, *args, **kwargs): nonlocal count_setup_models count_setup_models += 1 orig_setup_models(registry, cr, *args, **kwargs) 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) def test_update_xmlid(self): record = self.env['ir.model.data'].search([], limit=1) with self.assertQueryCount(1): self.env['ir.model.data'].with_context(studio=True)._update_xmlids([ {'xml_id': 'web_studio.xmlid', 'record': record} ]) def test_ir_default_company_fields(self): model = self.env['ir.model'].create({ 'name': 'Rockets', 'model': 'x_rockets', 'field_id': [ Command.create({'name': 'x_studio_company_id', 'ttype': 'many2one', 'relation': 'res.company'}), Command.create({'name': 'x_company_id', 'ttype': 'many2one', 'relation': 'res.company'}), ] }) self.env["res.company"].create({"name": "new company test"}) defaults = self.env["ir.default"].search([("field_id", "in", model.field_id.ids)]) self.assertEqual(len(defaults), 2) self.assertEqual(defaults.mapped("field_id"), model.field_id.filtered(lambda f: f.relation == "res.company")) @tagged("-at_install", "post_install") class TestStudioIrModelHardcoded(TransactionCase): def test_23_export_hardcoded_models_and_fields(self): """ Test that all models and fields from hardcoded lists exist in the data model. Should be executed at post install time because obviously the models should all have a chance to get up to date. """ needed_modules = { "account", "account_edi", "account_edi_ubl_cii", "analytic", "appointment", "auth_signup", "base", "base_automation", "calendar", "crm", "documents", "event", "helpdesk", "hr", "hr_recruitment", "html_editor", "knowledge", "loyalty", "mail", "mail_mobile", "maintenance", "mrp", "phone_validation", "planning", "point_of_sale", "portal", "pos_enterprise", "pos_loyalty", "pos_restaurant", "product", "project", "purchase", "purchase_stock", "quality", "repair", "resource", "sale", "sale_management", "sale_planning", "sale_project", "sale_subscription", "sales_team", "sign", "stock", "survey", "uom", "web_map", "web_studio", "website", "website_sale", "worksheet", } modules = self.env["ir.module.module"].search([('name', 'in', list(needed_modules))]) self.assertEqual(len(needed_modules), len(modules), needed_modules.difference(modules.mapped('name'))) if ms := [m.name for m in modules if m.state != 'installed']: # At least one needed module is not installed, so we would not be able # to assert the hardcoded lists, so we skip the rest of the test. self.skipTest(f"Missing required modules {', '.join(ms)}") def check_ownership(model: str, field_name: str | None = None) -> None: self.assertIn(model, self.env, f"Unknown model {model}") if field_name: field = self.env[model]._fields.get(field_name) self.assertIsNotNone(field, f"Unknown field {model}.{field_name}") # sadly Field._module returns the module which last overrode # the field, and Field._modules has unstable ordering, so if # the field was overridden walk the MRO from the top module = field._module if len(field._modules) == 1 else next( cls for cls in reversed(self.registry[model].mro()) if hasattr(cls, field_name) )._module seen_modules.add(module) self.assertIn( module, needed_modules, f"{model}.{field_name} was not added by a listed module", ) else: seen_modules.add(self.env[model]._original_module) self.assertIn( self.env[model]._original_module, needed_modules, f"{model} was not added by a listed module", ) seen_modules = set() for model, defaults in PRESET_MODELS_DEFAULTS: check_ownership(model) for field in defaults: self.assertIn(field, self.env["studio.export.model"]._fields) for model, fields in DEFAULT_FIELDS_TO_EXCLUDE.items(): for field in fields: check_ownership(model, field) for model, fields in ABSTRACT_MODEL_FIELDS_TO_EXCLUDE.items(): for field in fields: check_ownership(model, field) for model, fields in FIELDS_TO_EXPORT.items(): for field in fields: check_ownership(model, field) for model in DEFAULT_MODELS_TO_EXPORT: check_ownership(model) for model in RELATED_MODELS_TO_EXCLUDE: check_ownership(model) for model in MODELS_WITH_NOUPDATE: check_ownership(model) for model, fields in RELATIONS_NOT_TO_EXPORT.items(): for field in fields: check_ownership(model, field) for model, field in XML_FIELDS: check_ownership(model, field) self.assertEqual(needed_modules, seen_modules) def test_24_export_all_required_fields(self): """Test that all required fields are exported""" for model, fields in FIELDS_TO_EXPORT.items(): required_fields = [ field_name for field_name, field_obj in self.env[model]._fields.items() if field_obj.required and field_obj.default is None ] for field in required_fields: self.assertIn(field, fields, f"required field {field} is not exported for model {model}")