from lxml import etree from odoo import Command from odoo.tests import tagged from odoo.tests.common import BaseCase, HttpCase from odoo.addons.web_studio.controllers.export import StudioExporter from odoo.addons.web_studio.wizard.studio_export_wizard import _find_circular_dependencies, FIELDS_TO_EXPORT from odoo.addons.http_routing.tests.common import MockRequest class TestExport(HttpCase): def setUp(self): super().setUp() self.exporter = StudioExporter() def test_export_currency_field(self): base_currency_field = self.env["res.partner"]._fields.get("currency_id") IrModelFields = self.env["ir.model.fields"].with_context(studio=True) if not base_currency_field or not (base_currency_field.type == "many2one" and base_currency_field.comodel_name == "res.currency"): IrModelFields.create({ "state": "base", "name": "x_currency" if base_currency_field else "currency_id", "model_id": self.env["ir.model"]._get("res.partner").id, "ttype": "many2one", "relation": "res.currency" }) currency_field = IrModelFields.create({ "name": "x_test_currency", "model_id": self.env["ir.model"]._get("res.partner").id, "ttype": "many2one", "relation": "res.currency" }) monetary = IrModelFields.create({ "name": "x_test_monetary", "model_id": self.env["ir.model"]._get("res.partner").id, "ttype": "monetary", "currency_field": currency_field.name, }) studio_module = self.env['ir.module.module'].get_studio_module() data = self.env['ir.model.data'].search([ ('studio', '=', True), ("model", "=", "ir.model.fields"), ("res_id", "in", (currency_field | monetary).ids) ]) data = self.env["studio.export.wizard.data"].create( [{"model": d.model, "res_id": d.res_id, "studio": d.studio} for d in data] ) wizard = self.env['studio.export.wizard'].create({ "default_export_data": [Command.set(data.ids)], "additional_models": [], }) export_info = wizard.get_export_info() content_iter = iter(self.exporter.generate_module_files(studio_module, export_info)) file_name = content = None with MockRequest(self.env): while file_name != "data/ir_model_fields.xml": file_name, content = next(content_iter) arch_fields = etree.fromstring(content) records = arch_fields.findall("record") currency_field = records[0] self.assertEqual(currency_field.find("./field[@name='name']").text, "x_test_currency") self.assertEqual(currency_field.find("./field[@name='currency_field']"), None) monetary_field = records[1] self.assertEqual(monetary_field.find("./field[@name='name']").text, "x_test_monetary") self.assertEqual(monetary_field.find("./field[@name='currency_field']").text, "x_test_currency") monetary.currency_field = False export_info = wizard.get_export_info() content_iter = iter(self.exporter.generate_module_files(studio_module, export_info)) file_name = content = None with MockRequest(self.env): while file_name != "data/ir_model_fields.xml": file_name, content = next(content_iter) arch_fields = etree.fromstring(content) records = arch_fields.findall("record") currency_field = records[0] self.assertEqual(currency_field.find("./field[@name='name']").text, "x_test_currency") self.assertEqual(currency_field.find("./field[@name='currency_field']"), None) monetary_field = records[1] self.assertEqual(monetary_field.find("./field[@name='name']").text, "x_test_monetary") # This assertion is correct technically: the python monetary field will fallback # on one of the hardcoded currency field names. # For this test though, on res.partner, the actual field will crash self.assertEqual(monetary_field.find("./field[@name='currency_field']"), None) def test_export_automation(self): ba = self.env["base.automation"].with_context(studio=True).create({ "name": "Cron BaseAuto", "trigger": "on_time", "model_id": self.env.ref("base.model_res_users").id, }) data = self.env['ir.model.data'].search([ ('studio', '=', True), ("model", "=", "base.automation"), ("res_id", "=", ba.id) ]) studio_module = self.env['ir.module.module'].get_studio_module() data = self.env["studio.export.wizard.data"].create( [{"model": d.model, "res_id": d.res_id, "studio": d.studio} for d in data] ) wizard = self.env['studio.export.wizard'].create({ "default_export_data": [Command.set(data.ids)], }) export_info = wizard.get_export_info() content_iter = iter(self.exporter.generate_module_files(studio_module, export_info)) file_name = content = None with MockRequest(self.env): while file_name != "data/base_automation.xml": file_name, content = next(content_iter) arch = etree.fromstring(content) records = arch.findall("record") self.assertEqual(len(records), 1) record = records[0] field_names = {field.get("name") for field in record.findall("./field")} self.assertEqual(field_names, { "model_id", "name", "trg_date_range_type", "trigger" }) def test_export_unarchived_records(self): IrFilter = self.env['ir.filters'].with_context(studio=True) filter_1 = IrFilter.create({ 'name': 'Filter 1', 'model_id': self.env['ir.model']._get('res.partner').id, 'domain': '[]', }) filter_2 = IrFilter.create({ 'name': 'Filter 2', 'model_id': self.env['ir.model']._get('res.partner').id, 'domain': '[]', }) self.assertTrue(filter_1.active, "Filter 1 should be active initially") self.assertTrue(filter_2.active, "Filter 2 should be active initially") filter_1.active = False self.assertFalse(filter_1.active, "Filter should be archived") filter_data = self.env['ir.model.data'].search([ ('studio', '=', True), ('model', '=', 'ir.filters'), ]) self.assertEqual(len(filter_data), 2, "Should have two ir.model.data records: one archived and one active filter") wizard = self.env['studio.export.wizard'].create({}) default_data = wizard._default_studio_export_data() wizard.default_export_data = [Command.set(default_data.ids)] export_info = wizard.get_export_info() studio_module = self.env['ir.module.module'].get_studio_module() content_iter = iter(self.exporter.generate_module_files(studio_module, export_info)) file_name = content = None with MockRequest(self.env): while file_name != "data/ir_filters.xml": file_name, content = next(content_iter) arch_filters = etree.fromstring(content) records = arch_filters.findall('record') self.assertEqual(len(records), 1, "Only the active filter should be exported.") def test_fields_to_export_are_not_excluded(self): for model, fields_to_export in FIELDS_TO_EXPORT.items(): export_model = self.env["studio.export.model"].create({ "model_id": self.env["ir.model"]._get(model).id, }) excluded_fields = export_model.excluded_fields.mapped("name") self.assertEqual(set(fields_to_export), set(fields_to_export) - set(excluded_fields)) @tagged("post_install", "-at_install") class TestExportTours(HttpCase): def test_can_export_new_module(self): self.start_tour("/odoo?debug=tests", 'can_export_new_module', login="admin") # check the export result made by the tour wizard = self.env['studio.export.wizard'].search([], limit=1, order='id desc') data, files, circular_dependencies = wizard.get_export_info() self.assertEqual(circular_dependencies, []) self.assertEqual(data, wizard.default_export_data) self.assertEqual(len(wizard.additional_export_data), 0) exported_models = {f[0] for f in files} self.assertEqual(exported_models, { 'ir.actions.act_window', 'ir.default', 'ir.model', 'ir.model.fields', 'ir.model.access', 'ir.ui.view', 'ir.ui.menu', }) self.assertEqual(len(data.filtered(lambda r: r.model == 'ir.model')), 1) self.assertEqual(self.env['ir.model'].browse(data.filtered(lambda r: r.model == 'ir.model').res_id).name, 'My App Records') created_menus = self.env['ir.ui.menu'].browse(data.filtered(lambda r: r.model == 'ir.ui.menu').mapped("res_id")).mapped("name") self.assertEqual(len(created_menus), 2) self.assertTrue('My New App' in created_menus) self.assertTrue('My App Records' in created_menus) class TestCircularDependencies(BaseCase): def test_circular_dependencies(self): self.assertEqual(_find_circular_dependencies({}), []) self.assertEqual(_find_circular_dependencies({1: []}), []) self.assertEqual(_find_circular_dependencies({1: [1]}), []) self.assertEqual(_find_circular_dependencies({1: [2]}), []) self.assertEqual(_find_circular_dependencies({1: [2], 2: [3]}), []) self.assertEqual(_find_circular_dependencies({1: [2, 3], 2: [3], 3: [4]}), []) self.assertEqual(_find_circular_dependencies({1: [2], 2: [3], 3: [1]}), [[1, 2, 3, 1]]) self.assertEqual(_find_circular_dependencies({1: [2, 3], 2: [3], 3: [1]}), [[1, 2, 3, 1]]) self.assertEqual(_find_circular_dependencies({1: [2], 2: [3], 3: [4], 4: [1]}), [[1, 2, 3, 4, 1]]) self.assertEqual(_find_circular_dependencies({1: [2], 2: [1], 3: [4], 4: [5], 5: [3]}), [[1, 2, 1], [3, 4, 5, 3]])