diff --git a/addons/board/controllers/main.py b/addons/board/controllers/main.py index 303f5b0908e..e3871b6f822 100644 --- a/addons/board/controllers/main.py +++ b/addons/board/controllers/main.py @@ -16,10 +16,9 @@ class Board(Controller): if action and action['res_model'] == 'board.board' and action['views'][0][1] == 'form' and action_id: # Maybe should check the content instead of model board.board ? view_id = action['views'][0][0] - board = request.env['board.board'].fields_view_get(view_id, 'form') - if board and 'arch' in board: - xml = ElementTree.fromstring(board['arch']) - column = xml.find('./board/column') + board_arch, _view = request.env['board.board']._get_view(view_id, 'form') + if board_arch: + column = board_arch.find('./board/column') if column is not None: new_action = ElementTree.Element('action', { 'name': str(action_id), @@ -29,7 +28,7 @@ class Board(Controller): 'domain': str(domain) }) column.insert(0, new_action) - arch = ElementTree.tostring(xml, encoding='unicode') + arch = ElementTree.tostring(board_arch, encoding='unicode') request.env['ir.ui.view.custom'].create({ 'user_id': request.session.uid, 'ref_id': view_id, diff --git a/addons/board/models/board.py b/addons/board/models/board.py index 3d7a47f7574..9dc2978fd8e 100644 --- a/addons/board/models/board.py +++ b/addons/board/models/board.py @@ -20,13 +20,13 @@ class Board(models.AbstractModel): return self @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + def get_view(self, view_id=None, view_type='form', **options): """ Overrides orm field_view_get. @return: Dictionary of Fields, arch and toolbar. """ - res = super(Board, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + res = super().get_view(view_id, view_type, **options) custom_view = self.env['ir.ui.view.custom'].search([('user_id', '=', self.env.uid), ('ref_id', '=', view_id)], limit=1) if custom_view: diff --git a/addons/board/static/tests/dashboard_tests.js b/addons/board/static/tests/dashboard_tests.js index 4e62f5adab7..43a0d72e9d8 100644 --- a/addons/board/static/tests/dashboard_tests.js +++ b/addons/board/static/tests/dashboard_tests.js @@ -365,7 +365,7 @@ odoo.define("board.dashboard_tests", function (require) { ], }); } - if (args.method === "load_views") { + if (args.method === "get_views") { assert.deepEqual( args.kwargs.context, { a: 1, b: 2 }, @@ -656,8 +656,8 @@ odoo.define("board.dashboard_tests", function (require) { "" + "", intercepts: { - load_views: function () { - throw new Error("load_views should not be called"); + get_views: function () { + throw new Error("get_views should not be called"); }, }, mockRPC: function (route) { @@ -1048,7 +1048,7 @@ odoo.define("board.dashboard_tests", function (require) { "" + "", mockRPC: function (route, args) { - if (args.method === "load_views") { + if (args.method === "get_views") { assert.deepEqual( pyUtils.eval("context", args.kwargs.context), { lang: "fr_FR" }, diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 2a4f460a4b5..d8a0802fad2 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -862,16 +862,16 @@ class Lead(models.Model): return super(Lead, self.with_context(context)).copy(default=default) @api.model - def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + def _get_view(self, view_id=None, view_type='form', **options): if self._context.get('opportunity_id'): opportunity = self.browse(self._context['opportunity_id']) action = opportunity.get_formview_action() if action.get('views') and any(view_id for view_id in action['views'] if view_id[1] == view_type): view_id = next(view_id[0] for view_id in action['views'] if view_id[1] == view_type) - res = super(Lead, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + arch, view = super()._get_view(view_id, view_type, **options) if view_type == 'form': - res['arch'] = self._fields_view_get_address(res['arch']) - return res + arch = self._view_get_address(arch) + return arch, view @api.model def _read_group_stage_ids(self, stages, domain, order): diff --git a/addons/google_spreadsheet/models/google_drive.py b/addons/google_spreadsheet/models/google_drive.py index 10e04bbb050..c64e8e6b5f2 100644 --- a/addons/google_spreadsheet/models/google_drive.py +++ b/addons/google_spreadsheet/models/google_drive.py @@ -26,8 +26,8 @@ class GoogleDrive(models.Model): def write_config_formula(self, attachment_id, spreadsheet_key, model, domain, groupbys, view_id): access_token = self.get_access_token(scope='https://www.googleapis.com/auth/spreadsheets') - fields = self.env[model].fields_view_get(view_id=view_id, view_type='tree') - doc = etree.XML(fields.get('arch')) + arch, _view = self.env[model]._get_view(view_id, 'tree') + doc = arch display_fields = [] for node in doc.xpath("//field"): if node.get('modifiers'): diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py index fe14ec9b5db..cb58ab6f98c 100644 --- a/addons/hr/models/hr_employee.py +++ b/addons/hr/models/hr_employee.py @@ -217,10 +217,10 @@ class HrEmployeePrivate(models.Model): return self.env['hr.employee.public'].browse(self.ids).read(fields, load=load) @api.model - def load_views(self, views, options=None): + def get_view(self, view_id=None, view_type='form', **options): if self.check_access_rights('read', raise_exception=False): - return super(HrEmployeePrivate, self).load_views(views, options=options) - return self.env['hr.employee.public'].load_views(views, options=options) + return super().get_view(view_id, view_type, **options) + return self.env['hr.employee.public'].get_view(view_id, view_type, **options) @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): diff --git a/addons/hr/models/res_users.py b/addons/hr/models/res_users.py index 8123aa54bec..9a51fbed6ce 100644 --- a/addons/hr/models/res_users.py +++ b/addons/hr/models/res_users.py @@ -169,7 +169,7 @@ class User(models.Model): return super().SELF_WRITEABLE_FIELDS + HR_WRITABLE_FIELDS @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + def get_view(self, view_id=None, view_type='form', **options): # When the front-end loads the views it gets the list of available fields # for the user (according to its access rights). Later, when the front-end wants to # populate the view with data, it only asks to read those available fields. @@ -182,11 +182,11 @@ class User(models.Model): original_user = self.env.user if profile_view and view_id == profile_view.id: self = self.with_user(SUPERUSER_ID) - result = super(User, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + result = super(User, self).get_view(view_id, view_type, **options) # Due to using the SUPERUSER the result will contain action that the user may not have access too # here we filter out actions that requires special implicit rights to avoid having unusable actions # in the dropdown menu. - if toolbar and self.env.user != original_user: + if options.get('toolbar') and self.env.user != original_user: self = self.with_user(original_user.id) if not self.user_has_groups("base.group_erp_manager"): change_password_action = self.env.ref("base.change_password_wizard_action") diff --git a/addons/hr/tests/test_self_user_access.py b/addons/hr/tests/test_self_user_access.py index 539476e48b0..36213ff912a 100644 --- a/addons/hr/tests/test_self_user_access.py +++ b/addons/hr/tests/test_self_user_access.py @@ -3,6 +3,7 @@ from collections import OrderedDict from itertools import chain +from lxml import etree from odoo.addons.hr.tests.common import TestHrCommon from odoo.tests import new_test_user, tagged, Form @@ -20,8 +21,8 @@ class TestSelfAccessProfile(TestHrCommon): 'user_id': james.id, }) view = self.env.ref('hr.res_users_view_form_profile') - view_infos = james.fields_view_get(view_id=view.id) - fields = view_infos['fields'].keys() + view_infos = james.get_view(view.id) + fields = [el.get('name') for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]')] james.read(fields) def test_readonly_fields(self): @@ -35,12 +36,12 @@ class TestSelfAccessProfile(TestHrCommon): }) view = self.env.ref('hr.res_users_view_form_profile') - view_infos = james.fields_view_get(view_id=view.id) - + fields = james._fields + view_infos = james.get_view(view.id) employee_related_fields = { - field_name - for field_name, field_attrs in view_infos['fields'].items() - if field_attrs.get('related', (None,))[0] == 'employee_id' + el.get('name') + for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]') + if fields[el.get('name')].related and fields[el.get('name')].related.split('.')[0] == 'employee_id' } form = Form(james, view=view) @@ -65,16 +66,16 @@ class TestSelfAccessProfile(TestHrCommon): all_groups |= self.env.ref(xml_id.strip()) user_all_groups = new_test_user(self.env, groups='base.group_user', login='hel', name='God') user_all_groups.write({'groups_id': [(4, group.id, False) for group in all_groups]}) - view_infos = self.env['res.users'].with_user(user_all_groups).fields_view_get(view_id=view.id) - full_fields = view_infos['fields'] + view_infos = self.env['res.users'].with_user(user_all_groups).get_view(view.id) + full_fields = [el.get('name') for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]')] # Now check the view for a simple user user = new_test_user(self.env, login='gro', name='Grouillot') - view_infos = self.env['res.users'].with_user(user).fields_view_get(view_id=view.id) - fields = view_infos['fields'] + view_infos = self.env['res.users'].with_user(user).get_view(view.id) + fields = [el.get('name') for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]')] # Compare both - self.assertEqual(full_fields.keys(), fields.keys(), "View fields should not depend on user's groups") + self.assertEqual(full_fields, fields, "View fields should not depend on user's groups") def test_access_my_profile_toolbar(self): """ A simple user shouldn't have the possibilities to see the 'Change Password' action""" @@ -85,7 +86,7 @@ class TestSelfAccessProfile(TestHrCommon): 'user_id': james.id, }) view = self.env.ref('hr.res_users_view_form_profile') - available_actions = james.fields_view_get(view_id=view.id, toolbar=True)['toolbar']['action'] + available_actions = james.get_view(view.id, toolbar=True)['toolbar']['action'] change_password_action = self.env.ref("base.change_password_wizard_action") self.assertFalse(any(x['id'] == change_password_action.id for x in available_actions)) @@ -98,7 +99,7 @@ class TestSelfAccessProfile(TestHrCommon): 'user_id': john.id, }) view = self.env.ref('hr.res_users_view_form_profile') - available_actions = john.fields_view_get(view_id=view.id, toolbar=True)['toolbar']['action'] + available_actions = john.get_view(view.id, toolbar=True)['toolbar']['action'] self.assertTrue(any(x['id'] == change_password_action.id for x in available_actions)) diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py index de37dbbd150..0b2c0379a4a 100644 --- a/addons/hr_recruitment/models/hr_recruitment.py +++ b/addons/hr_recruitment/models/hr_recruitment.py @@ -52,14 +52,12 @@ class RecruitmentSource(models.Model): source.alias_id = self.env['mail.alias'].create(vals) @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - res = super().fields_view_get(view_id, view_type, toolbar, submenu) + def _get_view(self, view_id=None, view_type='form', **options): + arch, view = super()._get_view(view_id, view_type, **options) if view_type == 'tree' and not bool(self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")): - arch = etree.fromstring(res['arch']) email = arch.xpath("//field[@name='email']")[0] email.getparent().remove(email) - res['arch'] = etree.tostring(arch, encoding='unicode') - return res + return arch, view class RecruitmentStage(models.Model): _name = "hr.recruitment.stage" @@ -437,10 +435,10 @@ class Applicant(models.Model): return nocontent_body % nocontent_values @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + def get_view(self, view_id=None, view_type='form', **options): if view_type == 'form' and self.user_has_groups('hr_recruitment.group_hr_recruitment_interviewer'): view_id = self.env.ref('hr_recruitment.hr_applicant_view_form_interviewer').id - return super().fields_view_get(view_id, view_type, toolbar, submenu) + return super().get_view(view_id, view_type, **options) def _notify_compute_recipients(self, message, msg_vals): """ diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index 8986c7f245f..197f4bd39b7 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -157,26 +157,26 @@ class AccountAnalyticLine(models.Model): return result @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + def _get_view(self, view_id=None, view_type='form', **options): """ Set the correct label for `unit_amount`, depending on company UoM """ - result = super(AccountAnalyticLine, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) - result['arch'] = self._apply_timesheet_label(result['arch'], view_type=view_type) - return result + arch, view = super()._get_view(view_id, view_type, **options) + arch = self._apply_timesheet_label(arch, view_type=view_type) + return arch, view @api.model - def _apply_timesheet_label(self, view_arch, view_type='form'): - doc = etree.XML(view_arch) + def _apply_timesheet_label(self, view_node, view_type='form'): + doc = view_node encoding_uom = self.env.company.timesheet_encode_uom_id # Here, we select only the unit_amount field having no string set to give priority to # custom inheretied view stored in database. Even if normally, no xpath can be done on # 'string' attribute. for node in doc.xpath("//field[@name='unit_amount'][@widget='timesheet_uom'][not(@string)]"): node.set('string', _('%s Spent') % (re.sub(r'[\(\)]', '', encoding_uom.name or ''))) - return etree.tostring(doc, encoding='unicode') + return doc @api.model - def _apply_time_label(self, view_arch, related_model): - doc = etree.XML(view_arch) + def _apply_time_label(self, view_node, related_model): + doc = view_node Model = self.env[related_model] # Just fetch the name of the uom in `timesheet_encode_uom_id` of the current company encoding_uom_name = self.env.company.timesheet_encode_uom_id.with_context(prefetch_fields=False).sudo().name @@ -184,7 +184,7 @@ class AccountAnalyticLine(models.Model): name_with_uom = re.sub(_('Hours') + "|Hours", encoding_uom_name or '', Model._fields[node.get('name')]._description_string(self.env), flags=re.IGNORECASE) node.set('string', name_with_uom) - return etree.tostring(doc, encoding='unicode') + return doc def _timesheet_get_portal_domain(self): if self.env.user.has_group('hr_timesheet.group_hr_timesheet_user'): diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index edab491a5d1..ebb9037f2ee 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -81,11 +81,11 @@ class Project(models.Model): return [('id', operator_new, (query, ()))] @api.model - def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - result = super()._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + def _get_view(self, view_id=None, view_type='form', **options): + arch, view = super()._get_view(view_id, view_type, **options) if view_type in ['tree', 'form'] and self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day'): - result['arch'] = self.env['account.analytic.line']._apply_time_label(result['arch'], related_model=self._name) - return result + arch = self.env['account.analytic.line']._apply_time_label(arch, related_model=self._name) + return arch, view @api.depends('allow_timesheets', 'timesheet_ids') def _compute_remaining_hours(self): @@ -368,16 +368,16 @@ class Task(models.Model): return super().name_get() @api.model - def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + def _get_view(self, view_id=None, view_type='form', **options): """ Set the correct label for `unit_amount`, depending on company UoM """ - result = super(Task, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + arch, view = super()._get_view(view_id, view_type, **options) # Use of sudo as the portal user doesn't have access to uom - result['arch'] = self.env['account.analytic.line'].sudo()._apply_timesheet_label(result['arch']) + arch = self.env['account.analytic.line'].sudo()._apply_timesheet_label(arch) if view_type in ['tree', 'pivot', 'graph'] and self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day'): - result['arch'] = self.env['account.analytic.line']._apply_time_label(result['arch'], related_model=self._name) + arch = self.env['account.analytic.line']._apply_time_label(arch, related_model=self._name) - return result + return arch, view @api.ondelete(at_uninstall=False) def _unlink_except_contains_entries(self): diff --git a/addons/hr_timesheet/report/project_report.py b/addons/hr_timesheet/report/project_report.py index be4238b46db..fc40365952a 100644 --- a/addons/hr_timesheet/report/project_report.py +++ b/addons/hr_timesheet/report/project_report.py @@ -33,8 +33,8 @@ class ReportProjectTaskUser(models.Model): return super(ReportProjectTaskUser, self)._group_by() + group_by_append @api.model - def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - result = super(ReportProjectTaskUser, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + def _get_view(self, view_id=None, view_type='form', **options): + arch, view = super()._get_view(view_id, view_type, **options) if view_type in ['pivot', 'graph'] and self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day'): - result['arch'] = self.env['account.analytic.line']._apply_time_label(result['arch'], related_model=self._name) - return result + arch = self.env['account.analytic.line']._apply_time_label(arch, related_model=self._name) + return arch, view diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index cab0cf777ef..c3d31734f01 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -99,6 +99,7 @@ 'web/static/src/legacy/js/services/data_manager.js', 'web/static/src/legacy/js/services/session.js', 'web/static/src/legacy/js/widgets/date_picker.js', + 'web/static/src/legacy/legacy_load_views.js', 'web/static/src/legacy/legacy_promise_error_handler.js', 'web/static/src/legacy/legacy_rpc_error_handler.js', 'web/static/src/legacy/utils.js', diff --git a/addons/mail/models/ir_ui_view.py b/addons/mail/models/ir_ui_view.py index 90fdea597b9..fb2a5552eaf 100644 --- a/addons/mail/models/ir_ui_view.py +++ b/addons/mail/models/ir_ui_view.py @@ -6,3 +6,10 @@ class View(models.Model): _inherit = 'ir.ui.view' type = fields.Selection(selection_add=[('activity', 'Activity')]) + + def _postprocess_tag_field(self, node, name_manager, node_info): + if node.xpath("ancestor::div[hasclass('oe_chatter')]"): + # Pass the postprocessing of the mail thread fields + # The web client makes it completely custom, and this is therefore pointless. + return + return super()._postprocess_tag_field(node, name_manager, node_info) diff --git a/addons/mail/static/src/js/basic_view.js b/addons/mail/static/src/js/basic_view.js index 82e2b836f5b..aaaf356734b 100644 --- a/addons/mail/static/src/js/basic_view.js +++ b/addons/mail/static/src/js/basic_view.js @@ -4,6 +4,8 @@ import BasicView from 'web.BasicView'; const mailWidgets = ['kanban_activity']; +const chatterFields = ["message_ids", "message_follower_ids", "activity_ids"]; + BasicView.include({ init: function () { this._super.apply(this, arguments); @@ -62,4 +64,29 @@ BasicView.include({ _hasField(fieldName) { return !!this.fieldsInfo[this.viewType][fieldName]; }, + /** + * Override to mark chatter fields inside the "oe_chatter" div such that we + * don't fetch their sub views, as we don't need them. Without this, those + * subviews would be loaded as there is no "widget" set on those fields. + * + * @override + */ + _processNode(node, fv) { + if (node.tag === "div" && node.attrs.class && node.attrs.class.includes("oe_chatter")) { + const viewType = fv.type; + const fieldsInfo = fv.fieldsInfo[viewType]; + const fields = fv.viewFields; + for (const child of node.children) { + if (child.tag === 'field' && chatterFields.includes(child.attrs.name)) { + const attrs = { ...child.attrs, modifiers: {}, __no_fetch: true }; + const fieldName = attrs.name; + fieldsInfo[fieldName] = this._processField(viewType, fields[fieldName], attrs); + } else { + this._processNode(child, fv); + } + } + return false; + } + return this._super(...arguments); + }, }); diff --git a/addons/membership/models/product.py b/addons/membership/models/product.py index 6107ad3b3a0..b766df1240f 100644 --- a/addons/membership/models/product.py +++ b/addons/membership/models/product.py @@ -18,10 +18,10 @@ class Product(models.Model): ] @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + def get_view(self, view_id=None, view_type='form', **options): if self._context.get('product') == 'membership_product': if view_type == 'form': view_id = self.env.ref('membership.membership_products_form').id else: view_id = self.env.ref('membership.membership_products_tree').id - return super(Product, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + return super().get_view(view_id, view_type, **options) diff --git a/addons/project/tests/test_project_sharing_portal_access.py b/addons/project/tests/test_project_sharing_portal_access.py index f7cfed90e29..e3cf97fc017 100644 --- a/addons/project/tests/test_project_sharing_portal_access.py +++ b/addons/project/tests/test_project_sharing_portal_access.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import OrderedDict +from lxml import etree from odoo import Command from odoo.exceptions import AccessError from odoo.tests import tagged @@ -49,10 +50,11 @@ class TestProjectSharingPortalAccess(TestProjectSharingCommon): def test_readonly_fields(self): """ The fields are not writeable should not be editable by the portal user. """ - view_infos = self.task_portal.fields_view_get(view_id=self.env.ref(self.project_sharing_form_view_xml_id).id) + view_infos = self.task_portal.get_view(self.env.ref(self.project_sharing_form_view_xml_id).id) + fields = [el.get('name') for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]')] project_task_fields = { field_name - for field_name, field_attrs in view_infos['fields'].items() + for field_name in fields if field_name not in self.write_protected_fields_task } with self.get_project_sharing_form_view(self.task_portal, self.user_portal) as form: diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index ee051576aba..bc78ba3efe5 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -428,35 +428,33 @@ class Product(models.Model): return res @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - res = super(Product, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + def fields_get(self, allfields=None, attributes=None): + res = super().fields_get(allfields, attributes) if self._context.get('location') and isinstance(self._context['location'], int): location = self.env['stock.location'].browse(self._context['location']) - fields = res.get('fields') - if fields: - if location.usage == 'supplier': - if fields.get('virtual_available'): - res['fields']['virtual_available']['string'] = _('Future Receipts') - if fields.get('qty_available'): - res['fields']['qty_available']['string'] = _('Received Qty') - elif location.usage == 'internal': - if fields.get('virtual_available'): - res['fields']['virtual_available']['string'] = _('Forecasted Quantity') - elif location.usage == 'customer': - if fields.get('virtual_available'): - res['fields']['virtual_available']['string'] = _('Future Deliveries') - if fields.get('qty_available'): - res['fields']['qty_available']['string'] = _('Delivered Qty') - elif location.usage == 'inventory': - if fields.get('virtual_available'): - res['fields']['virtual_available']['string'] = _('Future P&L') - if fields.get('qty_available'): - res['fields']['qty_available']['string'] = _('P&L Qty') - elif location.usage == 'production': - if fields.get('virtual_available'): - res['fields']['virtual_available']['string'] = _('Future Productions') - if fields.get('qty_available'): - res['fields']['qty_available']['string'] = _('Produced Qty') + if location.usage == 'supplier': + if res.get('virtual_available'): + res['virtual_available']['string'] = _('Future Receipts') + if res.get('qty_available'): + res['qty_available']['string'] = _('Received Qty') + elif location.usage == 'internal': + if res.get('virtual_available'): + res['virtual_available']['string'] = _('Forecasted Quantity') + elif location.usage == 'customer': + if res.get('virtual_available'): + res['virtual_available']['string'] = _('Future Deliveries') + if res.get('qty_available'): + res['qty_available']['string'] = _('Delivered Qty') + elif location.usage == 'inventory': + if res.get('virtual_available'): + res['virtual_available']['string'] = _('Future P&L') + if res.get('qty_available'): + res['qty_available']['string'] = _('P&L Qty') + elif location.usage == 'production': + if res.get('virtual_available'): + res['virtual_available']['string'] = _('Future Productions') + if res.get('qty_available'): + res['qty_available']['string'] = _('Produced Qty') return res def action_view_orderpoints(self): diff --git a/addons/test_mail/static/tests/activity_tests.js b/addons/test_mail/static/tests/activity_tests.js index 02fb895f1f2..5bad8e284f1 100644 --- a/addons/test_mail/static/tests/activity_tests.js +++ b/addons/test_mail/static/tests/activity_tests.js @@ -562,7 +562,7 @@ QUnit.test("Schedule activity dialog uses the same search view as activity view" }); function mockRPC(route, args) { - if (args.method === "load_views") { + if (args.method === "get_views") { assert.step(JSON.stringify(args.kwargs.views)); } } diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index 9e87df8b27b..342045a3f85 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -145,6 +145,7 @@ This module provides the core of the Odoo Web Client. 'web/static/src/legacy/legacy_service_provider.js', 'web/static/src/legacy/legacy_client_actions.js', 'web/static/src/legacy/legacy_dialog.js', + 'web/static/src/legacy/legacy_load_views.js', 'web/static/src/legacy/legacy_views.js', 'web/static/src/legacy/legacy_promise_error_handler.js', 'web/static/src/legacy/legacy_rpc_error_handler.js', diff --git a/addons/web/models/models.py b/addons/web/models/models.py index d42e0f4c452..318768a1d91 100644 --- a/addons/web/models/models.py +++ b/addons/web/models/models.py @@ -237,15 +237,15 @@ class Base(models.AbstractModel): }) @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - r = super().fields_view_get(view_id, view_type, toolbar, submenu) + def _get_view(self, view_id=None, view_type='form', **options): + arch, view = super()._get_view(view_id, view_type, **options) # avoid leaking the raw (un-rendered) template, also avoids bloating # the response payload for no reason. Only send the root node, # to send attributes such as `js_class`. - if r['type'] == 'qweb': - root = etree.fromstring(r['arch']) - r['arch'] = etree.tostring(etree.Element('qweb', root.attrib)) - return r + if view_type == 'qweb': + root = arch + arch = etree.Element('qweb', root.attrib) + return arch, view @api.model def _search_panel_field_image(self, field_name, **kwargs): diff --git a/addons/web/static/src/core/py_js/py_builtin.js b/addons/web/static/src/core/py_js/py_builtin.js index 9a16bcd57bc..bc57a9b4e21 100644 --- a/addons/web/static/src/core/py_js/py_builtin.js +++ b/addons/web/static/src/core/py_js/py_builtin.js @@ -58,4 +58,7 @@ export const BUILTINS = { }, relativedelta: PyRelativeDelta, + + true: true, + false: false, }; diff --git a/addons/web/static/src/core/utils/arrays.js b/addons/web/static/src/core/utils/arrays.js index ded0b886d4a..f73d4cc121c 100644 --- a/addons/web/static/src/core/utils/arrays.js +++ b/addons/web/static/src/core/utils/arrays.js @@ -52,6 +52,17 @@ function _getExtractorFrom(criterion) { } } +/** + * Returns the array of elements contained in both arrays. + * + * @param {any[]} array1 + * @param {any[]} array2 + * @returns {any[]} + */ +export function intersection(array1, array2) { + return array1.filter((v) => array2.includes(v)); +} + /** * Returns an object holding different groups defined by a given criterion * or a default one. Each group is a subset of the original given list. diff --git a/addons/web/static/src/legacy/js/services/data_manager.js b/addons/web/static/src/legacy/js/services/data_manager.js index 0efcc2ecf2d..bf7c2b82a56 100644 --- a/addons/web/static/src/legacy/js/services/data_manager.js +++ b/addons/web/static/src/legacy/js/services/data_manager.js @@ -5,7 +5,7 @@ var config = require('web.config'); var core = require('web.core'); var rpc = require('web.rpc'); var session = require('web.session'); -var utils = require('web.utils'); +const { generateLegacyLoadViewsResult } = require("@web/legacy/legacy_load_views"); return core.Class.extend({ init: function () { @@ -89,15 +89,20 @@ return core.Class.extend({ if (shouldLoadViews) { // Views info should be loaded options.load_filters = shouldLoadFilters; + if (config.device.isMobile) { + options.mobile = config.device.isMobile; + } this._cache.views[viewsKey] = rpc.query({ args: [], kwargs: { context, options, views: views_descr }, model, - method: 'load_views', + method: 'get_views', }).then(result => { + const { models, views } = result; + result = generateLegacyLoadViewsResult(model, views, models); // Freeze the fields dict as it will be shared between views and // no one should edit it - utils.deepFreeze(result.fields); + // utils.deepFreeze(result.fields); // OWL issue regarding proxies and frozen objects https://github.com/odoo/owl/issues/1158 for (const [ /* viewId */ , viewType] of views_descr) { const fvg = result.fields_views[viewType]; fvg.viewFields = fvg.fields; diff --git a/addons/web/static/src/legacy/legacy_load_views.js b/addons/web/static/src/legacy/legacy_load_views.js new file mode 100644 index 00000000000..fa50c822799 --- /dev/null +++ b/addons/web/static/src/legacy/legacy_load_views.js @@ -0,0 +1,116 @@ +/** @odoo-module **/ + +const xmlSerializer = new XMLSerializer(); +const domParser = new DOMParser(); + +function traverse(tree, f) { + if (f(tree)) { + _.each(tree.children, function (c) { + traverse(c, f); + }); + } +} + +/** + * Given the result of a get_views, produces the equivalent of what was previously + * returned, such that legacy views continue working as before without any adaptations. + * + * @param {string} resModel + * @param {Object} views "views" key of the result of a get_views + * @param {Object} models maps of model name -> fields_get + * @returns {Object} similar to what was previously returned by load_views + */ +export function generateLegacyLoadViewsResult(resModel, views, models) { + const legacyFieldViews = { + fields: models[resModel], + fields_views: {}, + }; + if (views.search && views.search.filters) { + legacyFieldViews.filters = views.search.filters; + } + for (const viewType in views) { + const { arch, toolbar, id } = views[viewType]; + const { arch: processedArch, viewFields } = processArch(arch, viewType, resModel, models); + legacyFieldViews.fields_views[viewType] = { + arch: processedArch, + fields: viewFields, + model: resModel, + toolbar, + view_id: id, + }; + } + return legacyFieldViews; +} + +/** + * Given an arch with inline x2many subviews, generates the object of fields + * appearing in the arch, with a "views" key that contains the descriptions of + * their subviews (whose archs are removed from the main arch). + * + * @param {string} arch + * @param {string} viewType + * @param {string} resModel + * @param {Object} models + * @returns {Object} the processed arch and the (view) fields description + */ +export function processArch(arch, viewType, resModel, models) { + const viewFields = {}; + const archDoc = domParser.parseFromString(arch, "text/xml").documentElement; + traverse(archDoc, function (node) { + if (node.nodeType === 3) { + return false; + } + if (node.tagName === "field") { + const fieldName = node.getAttribute("name"); + viewFields[fieldName] = viewFields[fieldName] || { + ...models[resModel][fieldName], + views: {}, + }; + // extract subviews + // note: this should only be done for form views, because x2many subviews doesn't make + // any sense in any other view type, but sometimes in the codebase, people inline + // subviews in list views (e.g. sale.order.form.inherit.sale.product.configurator) so + // we remove them from the main arch anyway, to mock the previous behavior + if (viewType === "form" || viewType === "list") { + const coModel = models[resModel][fieldName].relation; + const views = {}; + for (const childNode of [...node.children]) { + const viewType = childNode.tagName === "tree" ? "list" : childNode.tagName; + const { arch: subViewArch, viewFields: subViewFields } = processArch( + xmlSerializer.serializeToString(childNode), + viewType, + coModel, + models + ); + views[viewType] = { arch: subViewArch, fields: subViewFields }; + node.removeChild(childNode); + } + viewFields[fieldName].views = views; + } + return false; + } + // list: extract groupby informations + if (viewType === "list" && node.tagName === "groupby" && node !== archDoc) { + const fieldName = node.getAttribute("name"); + const coModel = models[resModel][fieldName].relation; + const { arch: subViewArch, viewFields: subViewFields } = processArch( + xmlSerializer.serializeToString(node), + "groupby", + coModel, + models + ); + const groupbyView = { arch: subViewArch, fields: subViewFields }; + viewFields[fieldName] = viewFields[fieldName] || { + ...models[resModel][fieldName], + views: {}, + }; + viewFields[fieldName].views.groupby = groupbyView; + for (const child of [...node.children]) { + node.removeChild(child); + } + return false; + } + return true; + }); + return { arch: xmlSerializer.serializeToString(archDoc), viewFields }; +} diff --git a/addons/web/static/src/legacy/legacy_views.js b/addons/web/static/src/legacy/legacy_views.js index e2f0e4d5994..b8c167c87e6 100644 --- a/addons/web/static/src/legacy/legacy_views.js +++ b/addons/web/static/src/legacy/legacy_views.js @@ -116,6 +116,7 @@ function registerView(name, LegacyView) { this.viewInfo = Object.assign({}, fieldsInfo, { fields: result.fields, viewFields: fieldsInfo.fields, + type: this.props.type, }); let controlPanelFieldsView; if (result.fields_views.search) { diff --git a/addons/web/static/src/search/search_arch_parser.js b/addons/web/static/src/search/search_arch_parser.js index 9e71ba747f4..09bdb370079 100644 --- a/addons/web/static/src/search/search_arch_parser.js +++ b/addons/web/static/src/search/search_arch_parser.js @@ -36,10 +36,10 @@ function reduceType(type) { } export class SearchArchParser extends XMLParser { - constructor(searchViewDescription, searchDefaults = {}, searchPanelDefaults = {}) { + constructor(searchViewDescription, fields, searchDefaults = {}, searchPanelDefaults = {}) { super(); - const { fields, irFilters, arch } = searchViewDescription; + const { irFilters, arch } = searchViewDescription; this.fields = fields || {}; this.irFilters = irFilters || []; diff --git a/addons/web/static/src/search/search_model.js b/addons/web/static/src/search/search_model.js index f03724152f4..b8995f199c2 100644 --- a/addons/web/static/src/search/search_model.js +++ b/addons/web/static/src/search/search_model.js @@ -229,7 +229,7 @@ export class SearchModel extends EventBus { const searchViewDescription = {}; if (loadSearchView) { - const viewDescriptions = await this.viewService.loadViews( + const result = await this.viewService.loadViews( { context: this.globalContext, resModel, @@ -240,23 +240,20 @@ export class SearchModel extends EventBus { loadIrFilters: loadIrFilters || false, } ); - Object.assign(searchViewDescription, viewDescriptions.search); + Object.assign(searchViewDescription, result.views.search); + searchViewFields = searchViewFields || result.fields; } if (searchViewArch) { searchViewDescription.arch = searchViewArch; } - if (searchViewFields) { - searchViewDescription.fields = searchViewFields; - } if (irFilters) { searchViewDescription.irFilters = irFilters; } if (searchViewId !== undefined) { searchViewDescription.viewId = searchViewId; } - this.searchViewArch = searchViewDescription.arch || ""; - this.searchViewFields = searchViewDescription.fields || {}; + this.searchViewFields = searchViewFields || {}; if (searchViewDescription.irFilters) { this.irFilters = searchViewDescription.irFilters; } @@ -266,7 +263,7 @@ export class SearchModel extends EventBus { if (config.state) { this._importState(config.state); - this.__legacyParseSearchPanelArchAnyway(searchViewDescription); + this.__legacyParseSearchPanelArchAnyway(searchViewDescription, searchViewFields); this.domainParts = {}; this.display = this._getDisplay(config.display); @@ -307,6 +304,7 @@ export class SearchModel extends EventBus { const parser = new SearchArchParser( searchViewDescription, + searchViewFields, searchDefaults, searchPanelDefaults ); @@ -2084,13 +2082,14 @@ export class SearchModel extends EventBus { * extension doesn't include the arch information, i.e. the class name and * view types. We have to extract those if they are not given. * @param {Object} searchViewDescription + * @param {Object} searchViewFields */ - __legacyParseSearchPanelArchAnyway(searchViewDescription) { + __legacyParseSearchPanelArchAnyway(searchViewDescription, searchViewFields) { if (this.searchPanelInfo) { return; } - const parser = new SearchArchParser(searchViewDescription); + const parser = new SearchArchParser(searchViewDescription, searchViewFields); const { searchPanelInfo } = parser.parse(); this.searchPanelInfo = { ...searchPanelInfo, shouldReload: false }; diff --git a/addons/web/static/src/views/view.js b/addons/web/static/src/views/view.js index 453e75385a2..f860389e43f 100644 --- a/addons/web/static/src/views/view.js +++ b/addons/web/static/src/views/view.js @@ -199,32 +199,31 @@ export class View extends Component { if (loadView || loadSearchView) { // view description (or search view description if required) is incomplete // a loadViews is done to complete the missing information - const viewDescriptions = await this.viewService.loadViews( + const result = await this.viewService.loadViews( { context, resModel, views }, { actionId: this.env.config.actionId, loadActionMenus, loadIrFilters } ); // Note: if this.props.views is different from views, the cached descriptions // will certainly not be reused! (but for the standard flow this will work as // before) - viewDescription = viewDescriptions[type]; - view[0] = viewDescription.viewId; - searchViewDescription = viewDescriptions.search; + viewDescription = result.views[type]; + searchViewDescription = result.views.search; if (loadSearchView) { - searchViewId = searchViewId || searchViewDescription.viewId; + searchViewId = searchViewId || searchViewDescription.id; if (!searchViewArch) { searchViewArch = searchViewDescription.arch; - searchViewFields = searchViewDescription.fields; + searchViewFields = result.fields; } if (!irFilters) { irFilters = searchViewDescription.irFilters; } } this.env.config.views = views; + fields = fields || result.fields; } if (!arch) { arch = viewDescription.arch; - fields = viewDescription.fields; } if (!actionMenus) { actionMenus = viewDescription.actionMenus; @@ -244,7 +243,7 @@ export class View extends Component { } Object.assign(this.env.config, { - viewId: viewDescription.viewId, + viewId: viewDescription.id, viewType: type, viewSubType: subType, bannerRoute, diff --git a/addons/web/static/src/views/view_service.js b/addons/web/static/src/views/view_service.js index cf8a0668551..b25e26bb7f5 100644 --- a/addons/web/static/src/views/view_service.js +++ b/addons/web/static/src/views/view_service.js @@ -1,6 +1,8 @@ /** @odoo-module **/ +import { deepCopy } from "@web/core/utils/objects"; import { registry } from "@web/core/registry"; +import { generateLegacyLoadViewsResult } from "@web/legacy/legacy_load_views"; /** * @typedef {Object} IrFilter @@ -63,43 +65,48 @@ export const viewService = { * @returns {Promise} */ async function loadViews(params, options) { - const key = JSON.stringify([params.resModel, params.views, params.context, options]); + const loadViewsOptions = { + action_id: options.actionId || false, + load_filters: options.loadIrFilters || false, + toolbar: options.loadActionMenus || false, + }; + if (env.isSmall) { + loadViewsOptions.mobile = true; + } + const { context, resModel, views } = params; + const key = JSON.stringify([resModel, views, context, loadViewsOptions]); if (!cache[key]) { cache[key] = orm - .call(params.resModel, "load_views", [], { - views: params.views, - options: { - action_id: options.actionId || false, - load_filters: options.loadIrFilters || false, - toolbar: options.loadActionMenus || false, - }, - context: params.context, - }) + .call(resModel, "get_views", [], { context, views, options: loadViewsOptions }) .then((result) => { + const { models, views } = result; + const modelsCopy = deepCopy(models); // for legacy views + const fields = models[resModel]; + // add relatedFields for relational fields in view + function setRelatedFields(fields, models) { + for (const field of Object.values(fields)) { + if (field.relation && !field.relatedFields) { + field.relatedFields = models[field.relation] || {}; + setRelatedFields(field.relatedFields, models); + } + } + } + setRelatedFields(models[resModel], models); const viewDescriptions = { - __legacy__: result, - }; // for legacy purpose, keys in result are left in viewDescriptions - for (const [, viewType] of params.views) { - const viewDescription = JSON.parse( - JSON.stringify(result.fields_views[viewType]) - ); - viewDescription.viewId = viewDescription.view_id; - delete viewDescription.view_id; - if (viewDescription.toolbar) { - viewDescription.actionMenus = viewDescription.toolbar; - delete viewDescription.toolbar; + __legacy__: generateLegacyLoadViewsResult(resModel, views, modelsCopy), + fields, + views: {}, + }; + for (const viewType in views) { + const { arch, toolbar, id, filters } = views[viewType]; + const viewDescription = { arch, id }; + if (toolbar) { + viewDescription.actionMenus = toolbar; } - viewDescription.fields = Object.assign( - {}, - result.fields, - viewDescription.fields - ); // before a deep freeze was done. - delete viewDescription.base_model; // unused - delete viewDescription.field_parent; // unused - if (viewType === "search" && options.loadIrFilters) { - viewDescription.irFilters = result.filters; + if (filters) { + viewDescription.irFilters = filters; } - viewDescriptions[viewType] = viewDescription; + viewDescriptions.views[viewType] = viewDescription; } return viewDescriptions; }) diff --git a/addons/web/static/tests/core/py_js/py_interpreter_tests.js b/addons/web/static/tests/core/py_js/py_interpreter_tests.js index d9d3b9c115a..6d795c41375 100644 --- a/addons/web/static/tests/core/py_js/py_interpreter_tests.js +++ b/addons/web/static/tests/core/py_js/py_interpreter_tests.js @@ -145,6 +145,11 @@ QUnit.module("py", {}, () => { } ); + QUnit.test("true and false available in context", (assert) => { + assert.strictEqual(evaluateExpr("true"), true); + assert.strictEqual(evaluateExpr("false"), false); + }); + QUnit.test("throw error if name is not defined", (assert) => { assert.throws(() => evaluateExpr("a")); }); diff --git a/addons/web/static/tests/core/utils/arrays_tests.js b/addons/web/static/tests/core/utils/arrays_tests.js index 7a52bc13483..d78bfc2098a 100644 --- a/addons/web/static/tests/core/utils/arrays_tests.js +++ b/addons/web/static/tests/core/utils/arrays_tests.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { cartesian, groupBy, sortBy, unique } from "@web/core/utils/arrays"; +import { cartesian, groupBy, intersection, sortBy, unique } from "@web/core/utils/arrays"; QUnit.module("utils", () => { QUnit.module("Arrays"); @@ -153,6 +153,14 @@ QUnit.module("utils", () => { ]); }); + QUnit.test("intersection of arrays", function (assert) { + assert.deepEqual(intersection([], [1, 2]), []); + assert.deepEqual(intersection([1, 2], []), []); + assert.deepEqual(intersection([1], [2]), []); + assert.deepEqual(intersection([1, 2], [2, 3]), [2]); + assert.deepEqual(intersection([1, 2, 3], [1, 2, 3]), [1, 2, 3]); + }); + QUnit.test("cartesian product of zero arrays", function (assert) { assert.deepEqual(cartesian(), [undefined], "the unit of the product is a singleton"); }); diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index f3689d437e9..15be09d7c87 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -3,6 +3,8 @@ import { browser } from "@web/core/browser/browser"; import { Domain } from "@web/core/domain"; import { evaluateExpr } from "@web/core/py_js/py"; +import { deepCopy } from "@web/core/utils/objects"; +import { intersection } from "@web/core/utils/arrays"; import { registry } from "@web/core/registry"; import { makeFakeRPCService, makeMockFetch } from "./mock_services"; import { patchWithCleanup } from "./utils"; @@ -11,6 +13,9 @@ import { parseDateTime } from "@web/core/l10n/dates"; const { DateTime } = luxon; const serviceRegistry = registry.category("services"); +const domParser = new DOMParser(); +const xmlSerializer = new XMLSerializer(); + // ----------------------------------------------------------------------------- // Utils // ----------------------------------------------------------------------------- @@ -21,197 +26,6 @@ function traverseElementTree(tree, cb) { } } -export function _fieldsViewGet(params) { - let processedNodes = params.processedNodes || []; - const { arch, context, fields, modelName } = params; - function isNodeProcessed(node) { - return processedNodes.findIndex((n) => n.isSameNode(node)) > -1; - } - const modifiersNames = ["invisible", "readonly", "required"]; - const onchanges = params.models[modelName].onchanges || {}; - const fieldNodes = {}; - const groupbyNodes = {}; - let doc; - if (typeof arch === "string") { - const domParser = new DOMParser(); - doc = domParser.parseFromString(arch, "text/xml").documentElement; - } else { - doc = arch; - } - const inTreeView = doc.tagName === "tree"; - // mock _postprocess_access_rights - const isBaseModel = !context.base_model_name || modelName === context.base_model_name; - const views = ["kanban", "tree", "form", "gantt", "activity"]; - if (isBaseModel && views.indexOf(doc.tagName) !== -1) { - for (const action of ["create", "delete", "edit", "write"]) { - if (!doc.getAttribute(action) && action in context && !context[action]) { - doc.setAttribute(action, "false"); - } - } - } - - traverseElementTree(doc, (node) => { - if (node.nodeType === Node.TEXT_NODE) { - return false; - } - const modifiers = {}; - const isField = node.tagName === "field"; - const isGroupby = node.tagName === "groupby"; - if (isField) { - const fieldName = node.getAttribute("name"); - fieldNodes[fieldName] = node; - // 'transfer_field_to_modifiers' simulation - const field = fields[fieldName]; - if (!field) { - throw new Error("Field " + fieldName + " does not exist"); - } - const defaultValues = {}; - const stateExceptions = {}; // what is this ? - modifiersNames.forEach((attr) => { - stateExceptions[attr] = []; - defaultValues[attr] = !!field[attr]; - }); - // LPE: what is this ? - /* _.each(field.states || {}, function (modifs, state) { - _.each(modifs, function (modif) { - if (defaultValues[modif[0]] !== modif[1]) { - stateExceptions[modif[0]].append(state); - } - }); - });*/ - Object.entries(defaultValues).forEach(([attr, defaultValue]) => { - if (stateExceptions[attr].length) { - modifiers[attr] = [ - ["state", defaultValue ? "not in" : "in", stateExceptions[attr]], - ]; - } else { - modifiers[attr] = defaultValue; - } - }); - } else if (isGroupby && !isNodeProcessed(node)) { - const groupbyName = node.getAttribute("name"); - fieldNodes[groupbyName] = node; - groupbyNodes[groupbyName] = node; - } - // 'transfer_node_to_modifiers' simulation - let attrs = node.getAttribute("attrs"); - if (attrs) { - attrs = evaluateExpr(attrs); - Object.assign(modifiers, attrs); - } - const states = node.getAttribute("states"); - if (states) { - if (!modifiers.invisible) { - modifiers.invisible = []; - } - modifiers.invisible.push(["state", "not in", states.split(",")]); - } - const inListHeader = inTreeView && node.closest("header"); - modifiersNames.forEach((attr) => { - const mod = node.getAttribute(attr); - if (mod) { - // TODO - // const pyevalContext = window.py.dict.fromJSON(context || {}); - // var v = pyUtils.py_eval(mod, {context: pyevalContext}) ? true : false; - console.info( - "MockServer: naive parse of modifier value in", - QUnit.config.current.testName - ); - const v = JSON.parse(mod); - if (inTreeView && !inListHeader && attr === "invisible") { - modifiers.column_invisible = v; - } else if (v || !(attr in modifiers) || !Array.isArray(modifiers[attr])) { - modifiers[attr] = v; - } - } - }); - modifiersNames.forEach((attr) => { - if ( - attr in modifiers && - (!!modifiers[attr] === false || - (Array.isArray(modifiers[attr]) && !modifiers[attr].length)) - ) { - delete modifiers[attr]; - } - }); - if (Object.keys(modifiers).length) { - node.setAttribute("modifiers", JSON.stringify(modifiers)); - } - if (isGroupby && !isNodeProcessed(node)) { - return false; - } - return !isField; - }); - let relModel, relFields; - Object.entries(fieldNodes).forEach(([name, node]) => { - const field = fields[name]; - if (field.type === "many2one" || field.type === "many2many") { - const canCreate = node.getAttribute("can_create"); - node.setAttribute("can_create", canCreate || "true"); - const canWrite = node.getAttribute("can_write"); - node.setAttribute("can_write", canWrite || "true"); - } - if (field.type === "one2many" || field.type === "many2many") { - field.views = {}; - Array.from(node.children).forEach((children) => { - if (children.tagName) { - // skip text nodes - relModel = field.relation; - relFields = Object.assign({}, params.models[relModel].fields); - field.views[children.tagName] = _fieldsViewGet({ - models: params.models, - arch: children, - modelName: relModel, - fields: relFields, - context: Object.assign({}, context, { base_model_name: modelName }), - processedNodes, - }); - } - }); - } - // add onchanges - if (name in onchanges) { - node.setAttribute("on_change", "1"); - } - }); - Object.entries(groupbyNodes).forEach(([name, node]) => { - const field = fields[name]; - if (field.type !== "many2one") { - throw new Error("groupby can only target many2one"); - } - field.views = {}; - relModel = field.relation; - relFields = Object.assign({}, params.models[relModel].fields); - processedNodes.push(node); - // postprocess simulation - field.views.groupby = _fieldsViewGet({ - models: params.models, - arch: node, - modelName: relModel, - fields: relFields, - context, - processedNodes, - }); - while (node.firstChild) { - node.removeChild(node.firstChild); - } - }); - const xmlSerializer = new XMLSerializer(); - const processedArch = xmlSerializer.serializeToString(doc); - const fieldsInView = {}; - Object.entries(fields).forEach(([fname, field]) => { - if (fname in fieldNodes) { - fieldsInView[fname] = field; - } - }); - return { - arch: processedArch, - fields: fieldsInView, - model: modelName, - type: doc.tagName === "tree" ? "list" : doc.tagName, - }; -} - // ----------------------------------------------------------------------------- // MockServer // ----------------------------------------------------------------------------- @@ -298,7 +112,7 @@ export class MockServer { // def.abort = abort; } - fieldsViewGet(modelName, args, kwargs) { + getView(modelName, args, kwargs) { if (!(modelName in this.models)) { throw new Error(`Model ${modelName} was not defined in mock server data`); } @@ -331,7 +145,7 @@ export class MockServer { fields[fieldName].name = fieldName; } // var viewOptions = params.viewOptions || {}; - const fvg = _fieldsViewGet({ + const view = this._getView({ arch, modelName, fields, @@ -339,13 +153,231 @@ export class MockServer { models: this.models, }); if (kwargs.options.toolbar) { - fvg.toolbar = this.models[modelName].toolbar || {}; + view.toolbar = this.models[modelName].toolbar || {}; } if (viewId !== undefined) { - fvg.view_id = viewId; - fvg.name = key; + view.id = viewId; } - return fvg; + return view; + } + + _getView(params) { + let processedNodes = params.processedNodes || []; + const { arch, context, modelName } = params; + const level = params.level || 0; + const fields = deepCopy(params.fields); + function isNodeProcessed(node) { + return processedNodes.findIndex((n) => n.isSameNode(node)) > -1; + } + const modifiersNames = ["invisible", "readonly", "required"]; + const onchanges = params.models[modelName].onchanges || {}; + const fieldNodes = {}; + const groupbyNodes = {}; + const relatedModels = new Set([modelName]); + let doc; + if (typeof arch === "string") { + doc = domParser.parseFromString(arch, "text/xml").documentElement; + } else { + doc = arch; + } + const inTreeView = doc.tagName === "tree"; + const inFormView = doc.tagName === "form"; + // mock _postprocess_access_rights + const isBaseModel = !context.base_model_name || modelName === context.base_model_name; + const views = ["kanban", "tree", "form", "gantt", "activity"]; + if (isBaseModel && views.indexOf(doc.tagName) !== -1) { + for (const action of ["create", "delete", "edit", "write"]) { + if (!doc.getAttribute(action) && action in context && !context[action]) { + doc.setAttribute(action, "false"); + } + } + } + + traverseElementTree(doc, (node) => { + if (node.nodeType === Node.TEXT_NODE) { + return false; + } + const modifiers = {}; + const isField = node.tagName === "field"; + const isGroupby = node.tagName === "groupby"; + if (isField) { + const fieldName = node.getAttribute("name"); + fieldNodes[fieldName] = node; + // 'transfer_field_to_modifiers' simulation + const field = fields[fieldName]; + if (!field) { + throw new Error("Field " + fieldName + " does not exist"); + } + const defaultValues = {}; + const stateExceptions = {}; // what is this ? + modifiersNames.forEach((attr) => { + stateExceptions[attr] = []; + defaultValues[attr] = !!field[attr]; + }); + // LPE: what is this ? + /* _.each(field.states || {}, function (modifs, state) { + _.each(modifs, function (modif) { + if (defaultValues[modif[0]] !== modif[1]) { + stateExceptions[modif[0]].append(state); + } + }); + });*/ + Object.entries(defaultValues).forEach(([attr, defaultValue]) => { + if (stateExceptions[attr].length) { + modifiers[attr] = [ + ["state", defaultValue ? "not in" : "in", stateExceptions[attr]], + ]; + } else { + modifiers[attr] = defaultValue; + } + }); + } else if (isGroupby && !isNodeProcessed(node)) { + const groupbyName = node.getAttribute("name"); + fieldNodes[groupbyName] = node; + groupbyNodes[groupbyName] = node; + } + // 'transfer_node_to_modifiers' simulation + let attrs = node.getAttribute("attrs"); + if (attrs) { + attrs = evaluateExpr(attrs); + Object.assign(modifiers, attrs); + } + const states = node.getAttribute("states"); + if (states) { + if (!modifiers.invisible) { + modifiers.invisible = []; + } + modifiers.invisible.push(["state", "not in", states.split(",")]); + } + const inListHeader = inTreeView && node.closest("header"); + modifiersNames.forEach((attr) => { + const mod = node.getAttribute(attr); + if (mod) { + const v = evaluateExpr(mod, { context }) ? true : false; + if (inTreeView && !inListHeader && attr === "invisible") { + modifiers.column_invisible = v; + } else if (v || !(attr in modifiers) || !Array.isArray(modifiers[attr])) { + modifiers[attr] = v; + } + } + }); + modifiersNames.forEach((attr) => { + if ( + attr in modifiers && + (!!modifiers[attr] === false || + (Array.isArray(modifiers[attr]) && !modifiers[attr].length)) + ) { + delete modifiers[attr]; + } + }); + if (Object.keys(modifiers).length) { + node.setAttribute("modifiers", JSON.stringify(modifiers)); + } + if (isGroupby && !isNodeProcessed(node)) { + return false; + } + return !isField; + }); + let relModel, relFields; + Object.entries(fieldNodes).forEach(([name, node]) => { + const field = fields[name]; + if (field.type === "many2one" || field.type === "many2many") { + const canCreate = node.getAttribute("can_create"); + node.setAttribute("can_create", canCreate || "true"); + const canWrite = node.getAttribute("can_write"); + node.setAttribute("can_write", canWrite || "true"); + } + if (field.type === "one2many" || field.type === "many2many") { + relModel = field.relation; + relatedModels.add(relModel); + // inline subviews: in forms if field is visible and has no widget (1st level only) + if ( + inFormView && + level === 0 && + !node.getAttribute("widget") && + !node.getAttribute("invisible") + ) { + const inlineViewTypes = Array.from(node.children).map((c) => c.tagName); + const missingViewtypes = inlineViewTypes.includes("form") ? [] : ["form"]; + const mode = node.getAttribute("mode") || "kanban,tree"; + if (!intersection(inlineViewTypes, mode.split(",")).length) { + // TODO: use a kanban view by default in mobile + missingViewtypes.push((node.getAttribute("mode") || "list").split(",")[0]); + } + for (let type of missingViewtypes) { + type = type === "tree" ? "list" : type; + let key = `${field.relation},false,${type}`; + if (!this.archs[key]) { + const regexp = new RegExp(`${field.relation},[a-z._0-9]+,${type}`); + key = Object.keys(this.archs).find((k) => regexp.test(k)); + } + // in a lot of tests, we don't need the form view, so it doesn't even exist + const arch = this.archs[key] || (type === "form" ? "
" : null); + if (!arch) { + throw new Error(`Can't find ${type} view to inline`); + } + node.appendChild( + domParser.parseFromString(arch, "text/xml").documentElement + ); + } + } + Array.from(node.children).forEach((childNode) => { + if (childNode.tagName) { + relFields = Object.assign({}, params.models[relModel].fields); + // this is hackhish, but _getView modifies the subview document in place, + // especially to generate the "modifiers" attribute + const { models } = this._getView({ + models: params.models, + arch: childNode, + modelName: relModel, + fields: relFields, + context, + processedNodes, + level: level + 1, + }); + [...models].forEach((modelName) => relatedModels.add(modelName)); + } + }); + } + // add onchanges + if (name in onchanges) { + node.setAttribute("on_change", "1"); + } + }); + Object.entries(groupbyNodes).forEach(([name, node]) => { + const field = fields[name]; + if (field.type !== "many2one") { + throw new Error("groupby can only target many2one"); + } + field.views = {}; + relModel = field.relation; + relatedModels.add(relModel); + relFields = Object.assign({}, params.models[relModel].fields); + processedNodes.push(node); + // postprocess simulation + const { models } = this._getView({ + models: params.models, + arch: node, + modelName: relModel, + fields: relFields, + context, + processedNodes, + }); + [...models].forEach((modelName) => relatedModels.add(modelName)); + }); + const processedArch = xmlSerializer.serializeToString(doc); + const fieldsInView = {}; + Object.entries(fields).forEach(([fname, field]) => { + if (fname in fieldNodes) { + fieldsInView[fname] = field; + } + }); + return { + arch: processedArch, + model: modelName, + type: doc.tagName === "tree" ? "list" : doc.tagName, + models: relatedModels, + }; } /** @@ -419,8 +451,8 @@ export class MockServer { return this.mockCreate(args.model, args.args[0]); case "fields_get": return this.mockFieldsGet(args.model); - case "load_views": - return this.mockLoadViews(args.model, args.kwargs); + case "get_views": + return this.mockGetViews(args.model, args.kwargs); case "name_create": return this.mockNameCreate(args.model, args.args[0]); case "name_get": @@ -536,19 +568,20 @@ export class MockServer { return menus; } - mockLoadViews(modelName, kwargs) { - const fieldsViews = {}; + mockGetViews(modelName, kwargs) { + const views = {}; + const models = {}; + models[modelName] = this.mockFieldsGet(modelName); kwargs.views.forEach(([viewId, viewType]) => { - fieldsViews[viewType] = this.fieldsViewGet(modelName, [viewId, viewType], kwargs); + views[viewType] = this.getView(modelName, [viewId, viewType], kwargs); + if (kwargs.options.load_filters && viewType === "search") { + views[viewType].filters = this.models[modelName].filters || []; + } + for (const modelName of views[viewType].models) { + models[modelName] = models[modelName] || this.mockFieldsGet(modelName); + } }); - const result = { - fields: this.mockFieldsGet(modelName), - fields_views: fieldsViews, - }; - if (kwargs.options.load_filters) { - result.filters = this.models[modelName].filters || []; - } - return result; + return { models, views }; } /** diff --git a/addons/web/static/tests/legacy/fields/relational_fields/field_many2many_tests.js b/addons/web/static/tests/legacy/fields/relational_fields/field_many2many_tests.js index ff6ce352664..4dbb411b0d4 100644 --- a/addons/web/static/tests/legacy/fields/relational_fields/field_many2many_tests.js +++ b/addons/web/static/tests/legacy/fields/relational_fields/field_many2many_tests.js @@ -446,7 +446,7 @@ QUnit.module('fields', {}, function () { }, res_id: 1, mockRPC: function (route, args) { - if (args.method !== 'load_views') { + if (args.method !== 'get_views') { assert.step(_.last(route.split('/'))); } if (args.method === 'write' && args.model === 'partner') { @@ -550,7 +550,7 @@ QUnit.module('fields', {}, function () { 'partner_type,false,search': '', }, mockRPC: function (route, args) { - if (args.method !== 'load_views') { + if (args.method !== 'get_views') { assert.step(_.last(route.split('/'))); } if (args.method === 'write') { @@ -1139,7 +1139,7 @@ QUnit.module('fields', {}, function () { '', }, mockRPC: function (route, args) { - if (args.method !== 'load_views') { + if (args.method !== 'get_views') { assert.step(_.last(route.split('/')) + ' on ' + args.model); } if (args.model === 'turtle') { @@ -1264,7 +1264,7 @@ QUnit.module('fields', {}, function () { assert.verifySteps([ 'read', // read initial record (on partner) 'read', // read many2many turtles - 'load_views', // load arch of turtles form view + 'get_views', // load arch of turtles form view 'read', // read missing field when opening record in modal form view 'write', // when saving the modal 'onchange', // onchange should be triggered on partner diff --git a/addons/web/static/tests/legacy/fields/relational_fields/field_many2one_tests.js b/addons/web/static/tests/legacy/fields/relational_fields/field_many2one_tests.js index 03bfa5ee421..7f99381c812 100644 --- a/addons/web/static/tests/legacy/fields/relational_fields/field_many2one_tests.js +++ b/addons/web/static/tests/legacy/fields/relational_fields/field_many2one_tests.js @@ -629,7 +629,7 @@ QUnit.module('fields', {}, function () { // save the modal and make sure an onchange is triggered await testUtils.dom.click($('.modal .modal-footer .btn-primary').first()); - assert.verifySteps(['read', 'get_formview_id', 'load_views', 'read', 'write', 'read', 'onchange']); + assert.verifySteps(['read', 'get_formview_id', 'get_views', 'read', 'write', 'read', 'onchange']); // save the main record, and check that no extra rpcs are done (record // is not dirty, only a related record was modified) @@ -3117,7 +3117,7 @@ QUnit.module('fields', {}, function () { assert.verifySteps([ 'onchange', 'name_search', // to display results in the dropdown - 'load_views', // list view in dialog + 'get_views', // list view in dialog '/web/dataset/search_read', // to display results in the dialog ]); @@ -3173,7 +3173,7 @@ QUnit.module('fields', {}, function () { 'name_search', // empty search, triggered when the user clicks in the input 'name_search', // to display results in the dropdown 'name_search', // to get preselected ids matching the search - 'load_views', // list view in dialog + 'get_views', // list view in dialog '/web/dataset/search_read', // to display results in the dialog '/web/dataset/search_read', // after removal of dynamic filter ]); @@ -3339,7 +3339,7 @@ QUnit.module('fields', {}, function () { assert.verifySteps([ 'onchange', 'name_search', // to display results in the dropdown - 'load_views', // list view in dialog + 'get_views', // list view in dialog '/web/dataset/search_read', // to display results in the dialog '/web/dataset/resequence', // resequencing lines 'read', diff --git a/addons/web/static/tests/legacy/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/legacy/fields/relational_fields/field_one2many_tests.js index aada8b45683..06593a76727 100644 --- a/addons/web/static/tests/legacy/fields/relational_fields/field_one2many_tests.js +++ b/addons/web/static/tests/legacy/fields/relational_fields/field_one2many_tests.js @@ -6233,7 +6233,7 @@ QUnit.module('fields', {}, function () { await testUtils.owlCompatibilityExtraNextTick(); assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "0", "int_field should still be 0 (no onchange should have been done yet)"); - assert.verifySteps(['load_views', 'read', 'onchange']); + assert.verifySteps(['get_views', 'read', 'onchange']); // fill turtle_foo field await testUtils.fields.editInput(form.$('.o_field_widget[name="turtle_foo"]'), "some text"); @@ -7964,7 +7964,7 @@ QUnit.module('fields', {}, function () { // swap 2 lines in the one2many await testUtils.dom.dragAndDrop(form.$('.ui-sortable-handle:eq(1)'), form.$('tbody tr').first(), { position: 'top' }); - assert.verifySteps(['load_views', 'read', 'read', 'onchange', 'onchange']); + assert.verifySteps(['get_views', 'read', 'read', 'onchange', 'onchange']); form.destroy(); }); @@ -8004,7 +8004,7 @@ QUnit.module('fields', {}, function () { "should have triggered the onchange"); assert.verifySteps([ - 'load_views', // load sub list + 'get_views', // load sub list 'onchange', // main record 'onchange', // sub record 'onchange', // edition of display_name of sub record diff --git a/addons/web/static/tests/legacy/helpers/mock_server.js b/addons/web/static/tests/legacy/helpers/mock_server.js index d9cbf4c756a..055e6646067 100644 --- a/addons/web/static/tests/legacy/helpers/mock_server.js +++ b/addons/web/static/tests/legacy/helpers/mock_server.js @@ -79,7 +79,7 @@ var MockServer = Class.extend({ }, /** * helper: read a string describing an arch, and returns a simulated - * 'field_view_get' call to the server. Calls processViews() of data_manager + * 'view_get' call to the server. Calls processViews() of data_manager * to mimick the real behavior of a call to loadViews(). * * @param {Object} params @@ -90,7 +90,7 @@ var MockServer = Class.extend({ * @param {Object} [params.viewOptions] the view options set in the test (optional) * @returns {Object} an object with 2 keys: arch and fields */ - fieldsViewGet: function (params) { + getView: function (params) { var model = params.model; var toolbar = params.toolbar; var viewId = params.view_id; @@ -99,14 +99,14 @@ var MockServer = Class.extend({ throw new Error('Model ' + model + ' was not defined in mock server data'); } var fields = $.extend(true, {}, this.data[model].fields); - var fvg = this._fieldsViewGet(params.arch, model, fields, viewOptions.context || {}); + var view = this._getView(params.arch, model, fields, viewOptions.context || {}); if (toolbar) { - fvg.toolbar = toolbar; + view.toolbar = toolbar; } if (viewId) { - fvg.view_id = viewId; + view.id = viewId; } - return fvg; + return view; }, /** * Simulates a complete fetch call. @@ -266,7 +266,7 @@ var MockServer = Class.extend({ }, /** * helper: read a string describing an arch, and returns a simulated - * 'fields_view_get' call to the server. + * 'get_view' call to the server. * * @private * @param {string} arch a string OR a parsed xml document @@ -276,12 +276,13 @@ var MockServer = Class.extend({ * @returns {Object} an object with 2 keys: arch and fields (the fields * appearing in the views) */ - _fieldsViewGet: function (arch, model, fields, context) { + _getView: function (arch, model, fields, context) { var self = this; var modifiersNames = ['invisible', 'readonly', 'required']; var onchanges = this.data[model].onchanges || {}; var fieldNodes = {}; var groupbyNodes = {}; + const relatedModels = new Set([model]); var doc; if (typeof arch === 'string') { @@ -394,7 +395,7 @@ var MockServer = Class.extend({ return !isField; }); - var relModel, relFields; + let relModel, relFields; _.each(fieldNodes, function (node, name) { var field = fields[name]; if (field.type === "many2one" || field.type === "many2many") { @@ -404,13 +405,16 @@ var MockServer = Class.extend({ node.setAttribute('can_write', canWrite || "true"); } if (field.type === "one2many" || field.type === "many2many") { - field.views = {}; - _.each(node.childNodes, function (children) { - if (children.tagName) { // skip text nodes - relModel = field.relation; + relModel = field.relation; + relatedModels.add(relModel); + _.each(node.childNodes, function (childNode) { + if (childNode.tagName) { // skip text nodes relFields = $.extend(true, {}, self.data[relModel].fields); - field.views[children.tagName] = self._fieldsViewGet(children, relModel, + // this is hackhish, but _getView modifies the subview document in place, + // especially to generate the "modifiers" attribute + const { models } = self._getView(childNode, relModel, relFields, _.extend({}, context, {base_model_name: model})); + [...models].forEach((modelName) => relatedModels.add(modelName)); } }); } @@ -427,22 +431,21 @@ var MockServer = Class.extend({ } field.views = {}; relModel = field.relation; + relatedModels.add(relModel); relFields = $.extend(true, {}, self.data[relModel].fields); node._isProcessed = true; // postprocess simulation - field.views.groupby = self._fieldsViewGet(node, relModel, relFields, context); - while (node.firstChild) { - node.removeChild(node.firstChild); - } + const { models } = self._getView(node, relModel, relFields, context); + [...models].forEach((modelName) => relatedModels.add(modelName)); }); var xmlSerializer = new XMLSerializer(); var processedArch = xmlSerializer.serializeToString(doc); return { arch: processedArch, - fields: _.pick(fields, _.keys(fieldNodes)), model: model, type: doc.tagName === 'tree' ? 'list' : doc.tagName, + models: relatedModels, }; }, /** @@ -1180,7 +1183,7 @@ var MockServer = Class.extend({ return action || false; }, /** - * Simulate a 'load_views' operation + * Simulate a 'get_views' operation * * @param {string} model * @param {Array} args @@ -1190,7 +1193,7 @@ var MockServer = Class.extend({ * @param {Object} kwargs.context * @returns {Object} */ - _mockLoadViews: function (model, kwargs) { + _mockGetViews: function (model, kwargs) { var self = this; var views = {}; _.each(kwargs.views, function (view_descr) { @@ -1199,7 +1202,7 @@ var MockServer = Class.extend({ if (!viewID) { var contextKey = (viewType === 'list' ? 'tree' : viewType) + '_view_ref'; if (contextKey in kwargs.context) { - viewID = kwargs.context[contextKey]; + viewID = parseInt(kwargs.context[contextKey]); } } var key = [model, viewID, viewType].join(','); @@ -1989,8 +1992,8 @@ var MockServer = Class.extend({ case 'search_panel_select_multi_range': return this._mockSearchPanelSelectMultiRange(args.model, args.args, args.kwargs); - case 'load_views': - return this._mockLoadViews(args.model, args.kwargs); + case 'get_views': + return this._mockGetViews(args.model, args.kwargs); case 'name_get': return this._mockNameGet(args.model, args.args); diff --git a/addons/web/static/tests/legacy/helpers/test_utils.js b/addons/web/static/tests/legacy/helpers/test_utils.js index e8b5d23250c..cd021609026 100644 --- a/addons/web/static/tests/legacy/helpers/test_utils.js +++ b/addons/web/static/tests/legacy/helpers/test_utils.js @@ -141,7 +141,7 @@ odoo.define('web.test_utils', async function (require) { patch: testUtilsMock.patch, patchDate: testUtilsMock.patchDate, unpatch: testUtilsMock.unpatch, - fieldsViewGet: testUtilsMock.fieldsViewGet, + getView: testUtilsMock.getView, patchSetTimeout: testUtilsMock.patchSetTimeout, }, controlPanel: { @@ -269,7 +269,7 @@ odoo.define('web.test_utils', async function (require) { // backward-compatibility addMockEnvironment: deprecated(testUtilsMock.addMockEnvironment, 'mock'), dragAndDrop: deprecated(testUtilsDom.dragAndDrop, 'dom'), - fieldsViewGet: deprecated(testUtilsMock.fieldsViewGet, 'mock'), + getView: deprecated(testUtilsMock.getView, 'mock'), intercept: deprecated(testUtilsMock.intercept, 'mock'), openDatepicker: deprecated(testUtilsDom.openDatepicker, 'dom'), patch: deprecated(testUtilsMock.patch, 'mock'), diff --git a/addons/web/static/tests/legacy/helpers/test_utils_create.js b/addons/web/static/tests/legacy/helpers/test_utils_create.js index f1b31c26ccb..e93e2161c4a 100644 --- a/addons/web/static/tests/legacy/helpers/test_utils_create.js +++ b/addons/web/static/tests/legacy/helpers/test_utils_create.js @@ -142,13 +142,13 @@ odoo.define('web.test_utils_create', function (require) { new Widget(), serverParams, ); - const { arch, fields } = testUtilsMock.fieldsViewGet(mockServer, { + const { arch } = testUtilsMock.getView(mockServer, { arch: globalConfig.arch, fields: globalConfig.fields, model, viewOptions: { context: globalConfig.context }, }); - Object.assign(globalConfig, { arch, fields }); + Object.assign(globalConfig, { arch }); } globalConfig.env = env; @@ -283,7 +283,7 @@ odoo.define('web.test_utils_create', function (require) { // add mock environment: mock server, session, fieldviewget, ... const mockServer = await testUtilsMock.addMockEnvironment(widget, params); - const viewInfo = testUtilsMock.fieldsViewGet(mockServer, params); + const viewInfo = testUtilsMock.getView(mockServer, params); params.server = mockServer; @@ -297,7 +297,7 @@ odoo.define('web.test_utils_create', function (require) { }; const viewOptions = Object.assign({ action: Object.assign(defaultAction, params.action), - view: viewInfo, + view: { ...viewInfo, fields: mockServer.fieldsGet(params.model) }, modelName: modelName, ids: 'res_id' in params ? [params.res_id] : undefined, currentId: 'res_id' in params ? params.res_id : undefined, @@ -323,7 +323,7 @@ odoo.define('web.test_utils_create', function (require) { // TODO: probably needs to create an helper just for that view = new params.View({ viewInfo, modelName }); } else { - viewOptions.controlPanelFieldsView = Object.assign(testUtilsMock.fieldsViewGet(mockServer, { + viewOptions.controlPanelFieldsView = Object.assign(testUtilsMock.getView(mockServer, { arch: params.archs && params.archs[params.model + ',false,search'] || '', fields: viewInfo.fields, model: params.model, diff --git a/addons/web/static/tests/legacy/helpers/test_utils_mock.js b/addons/web/static/tests/legacy/helpers/test_utils_mock.js index 9e7b941ffc3..d866c8e3e9d 100644 --- a/addons/web/static/tests/legacy/helpers/test_utils_mock.js +++ b/addons/web/static/tests/legacy/helpers/test_utils_mock.js @@ -23,6 +23,7 @@ const MockServer = require('web.MockServer'); const RamStorage = require('web.RamStorage'); const session = require('web.session'); const { patchDate } = require("@web/../tests/helpers/utils"); +const { processArch } = require("@web/legacy/legacy_load_views"); const { Component } = owl; const DebouncedField = basic_fields.DebouncedField; @@ -80,11 +81,11 @@ async function _getMockedOwlEnv(params, mockServer) { options: options, views: params.views_descr, }, - method: 'load_views', + method: 'get_views', model: params.model, }).then(function (views) { views = _.mapObject(views, viewParams => { - return fieldsViewGet(mockServer, viewParams); + return getView(mockServer, viewParams); }); if (favoriteFilters && 'search' in views) { views.search.favoriteFilters = favoriteFilters; @@ -247,7 +248,7 @@ function _observe(widget) { //------------------------------------------------------------------------------ /** - * performs a fields_view_get, and mocks the postprocessing done by the + * performs a get_view, and mocks the postprocessing done by the * data_manager to return an equivalent structure. * * @param {MockServer} server @@ -255,12 +256,24 @@ function _observe(widget) { * @param {string} params.model * @returns {Object} an object with 3 keys: arch, fields and viewFields */ -function fieldsViewGet(server, params) { - var fieldsView = server.fieldsViewGet(params); +function getView(server, params) { + var view = server.getView(params); + const fields = server.fieldsGet(params.model); // mock the structure produced by the DataManager - fieldsView.viewFields = fieldsView.fields; - fieldsView.fields = server.fieldsGet(params.model); - return fieldsView; + const models = { [params.model]: fields }; + for (const modelName of view.models) { + models[modelName] = models[modelName] || server.fieldsGet(modelName); + } + const { arch, viewFields } = processArch(view.arch, view.type, params.model, models); + return { + arch, + fields, + model: view.model, + toolbar: view.toolbar, + type: view.type, + viewFields, + view_id: view.id, + }; } /** @@ -681,7 +694,7 @@ function patchSetTimeout() { return { addMockEnvironment: addMockEnvironment, - fieldsViewGet: fieldsViewGet, + getView: getView, addMockEnvironmentOwl: addMockEnvironmentOwl, intercept: intercept, patchDate: legacyPatchDate, diff --git a/addons/web/static/tests/legacy/services/data_manager_tests.js b/addons/web/static/tests/legacy/services/data_manager_tests.js index 38ee8d71970..5e36b1a093c 100644 --- a/addons/web/static/tests/legacy/services/data_manager_tests.js +++ b/addons/web/static/tests/legacy/services/data_manager_tests.js @@ -23,17 +23,20 @@ odoo.define('web.data_manager_tests', function (require) { const server = new MockServer(data, { archs }); const serverMethods = { - async load_views({ kwargs, model }) { - const { options, views } = kwargs; - const fields = server.fieldsGet(model); - const fields_views = {}; - for (const [viewId, viewType] of views) { + async get_views({ kwargs, model }) { + const models = {}; + models[model] = server.fieldsGet(model); + const views = {}; + for (const [viewId, viewType] of kwargs.views) { const arch = archs[[model, viewId || false, viewType].join()]; - fields_views[viewType] = server.fieldsViewGet({ arch, model, viewId }); + views[viewType] = server.getView({ arch, model, viewId }); + for (const modelName of views[viewType].models) { + models[modelName] = server.fieldsGet(modelName); + } } - const result = { fields, fields_views }; - if (options.load_filters) { - result.filters = data['ir.filters'].records.filter(r => r.model_id === model); + const result = { models, views }; + if (kwargs.options.load_filters && views.search) { + views.search.filters = data['ir.filters'].records.filter(r => r.model_id === model); } return result; }, @@ -133,7 +136,7 @@ odoo.define('web.data_manager_tests', function (require) { "query with same params and options should yield the same results"); assert.deepEqual(firstLoad.search.favoriteFilters, filters, "load filters should yield the same result as the first load_views' filters"); - assert.verifySteps(['oui.load_views'], + assert.verifySteps(['oui.get_views'], "only load once when not in assets debugging"); }); @@ -165,8 +168,8 @@ odoo.define('web.data_manager_tests', function (require) { assert.deepEqual(firstLoad.search.favoriteFilters, filters, "load filters should yield the same result as the first load_views' filters"); assert.verifySteps([ - 'oui.load_views', - 'oui.load_views', + 'oui.get_views', + 'oui.get_views', 'ir.filters.get_filters', ], "reload each time when in assets debugging"); }); @@ -228,7 +231,7 @@ odoo.define('web.data_manager_tests', function (require) { "Filters loaded by the load_views should be equal to the result of a load_filters"); assert.verifySteps([ - 'oui.load_views', + 'oui.get_views', 'ir.filters.create_or_replace', 'ir.filters.get_filters', 'ir.filters.unlink', diff --git a/addons/web/static/tests/legacy/views/basic_model_tests.js b/addons/web/static/tests/legacy/views/basic_model_tests.js index 3bf60f59333..6678c47f7af 100644 --- a/addons/web/static/tests/legacy/views/basic_model_tests.js +++ b/addons/web/static/tests/legacy/views/basic_model_tests.js @@ -154,7 +154,7 @@ odoo.define('web.basic_model_tests', function (require) { }); assert.verifySteps([ - 'load_views', + 'get_views', 'onchange', ]); assert.containsOnce(form, '.o_field_x2many_list', 'should have rendered a x2many list'); diff --git a/addons/web/static/tests/legacy/views/calendar_tests.js b/addons/web/static/tests/legacy/views/calendar_tests.js index 778bbc65715..8775881af10 100644 --- a/addons/web/static/tests/legacy/views/calendar_tests.js +++ b/addons/web/static/tests/legacy/views/calendar_tests.js @@ -3295,7 +3295,7 @@ QUnit.module('Views', { initialDate: initialDate, }, mockRPC: function (route, args) { - if (args.method === "load_views") { + if (args.method === "get_views") { assert.strictEqual(args.kwargs.views[0][0], 1, "should load view with id 1"); } diff --git a/addons/web/static/tests/legacy/views/form_tests.js b/addons/web/static/tests/legacy/views/form_tests.js index 654e932099b..e3725bd817a 100644 --- a/addons/web/static/tests/legacy/views/form_tests.js +++ b/addons/web/static/tests/legacy/views/form_tests.js @@ -521,10 +521,14 @@ QUnit.module('Views', { serverData.models.product.records = [{id: 1, name: 'Tromblon', partner_type_ids: [12,14]}]; serverData.models.partner.records[0].product_id = 1; + // This is an old test, written before "get_views" (formerly "load_views") automatically + // inlines x2many subviews. As the purpose of this test is to assert that the js fetches + // the correct sub view when it is not inline (which can still happen in nested form views), + // we bypass the inline mecanism of "get_views" by setting widget="one2many" on the field. serverData.views = { 'product,false,form': ''+ ''+ - '' + + '' + '', 'partner_type,false,list': ''+ @@ -539,7 +543,7 @@ QUnit.module('Views', { }; const mockRPC = (route, args) => { - if (args.method === 'load_views') { + if (args.method === 'get_views') { var context = args.kwargs.context; if (args.model === 'product') { assert.strictEqual(context.tree_view_ref, 'some_tree_view', @@ -6192,7 +6196,7 @@ QUnit.module('Views', { "the row should contains the 2 fields defined in the form view"); assert.strictEqual($(modal_row).text(), "gold2", "the value of the fields should be fetched and displayed"); - assert.verifySteps(['read', 'read', 'load_views', 'read', 'read'], + assert.verifySteps(['read', 'read', 'get_views', 'read', 'read'], "there should be 4 read rpcs"); form.destroy(); }); @@ -8222,10 +8226,10 @@ QUnit.module('Views', { assert.verifySteps([ "read", // main record "get_formview_id", // id of first form view opened in a dialog - "load_views", // arch of first form view opened in a dialog + "get_views", // arch of first form view opened in a dialog "read", // first dialog "get_formview_id", // id of second form view opened in a dialog - "load_views", // arch of second form view opened in a dialog + "get_views", // arch of second form view opened in a dialog "read", // second dialog "write", // save second dialog "read", // reload first dialog @@ -8799,7 +8803,7 @@ QUnit.module('Views', { } }); - assert.verifySteps(['partner_type:load_views', 'partner:read', 'partner_type:read']); + assert.verifySteps(['partner_type:get_views', 'partner:read', 'partner_type:read']); form.destroy(); }); diff --git a/addons/web/static/tests/legacy/views/kanban_tests.js b/addons/web/static/tests/legacy/views/kanban_tests.js index 54e19fcb9b7..a79e008acef 100644 --- a/addons/web/static/tests/legacy/views/kanban_tests.js +++ b/addons/web/static/tests/legacy/views/kanban_tests.js @@ -745,11 +745,11 @@ QUnit.module('Views', { 'web_read_group', // initial read_group '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) - 'load_views', // form view in quick create + 'get_views', // form view in quick create 'onchange', // quick create 'create', // should perform a create to create the record 'read', // read the created record - 'load_views', // form view in quick create (is actually in cache) + 'get_views', // form view in quick create (is actually in cache) 'onchange', // reopen the quick create automatically ]); @@ -870,11 +870,11 @@ QUnit.module('Views', { 'web_read_group', // initial read_group '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) - 'load_views', // form view in quick create + 'get_views', // form view in quick create 'onchange', // quick create 'create', // should perform a create to create the record 'read', // read the created record - 'load_views', // form view in quick create (is actually in cache) + 'get_views', // form view in quick create (is actually in cache) 'onchange', // reopen the quick create automatically ]); @@ -934,7 +934,7 @@ QUnit.module('Views', { 'web_read_group', // initial read_group '/web/dataset/search_read', // initial search_read (first column) '/web/dataset/search_read', // initial search_read (second column) - 'load_views', // form view in quick create + 'get_views', // form view in quick create 'onchange', // quick create 'onchange', // onchange due to 'foo' field change ]); @@ -2600,7 +2600,7 @@ QUnit.module('Views', { groupBy: ['product_id'], async mockRPC(route, args) { const result = this._super(...arguments); - if (args.method === 'load_views') { + if (args.method === 'get_views') { await def; } return result; diff --git a/addons/web/static/tests/search/search_panel_tests.js b/addons/web/static/tests/search/search_panel_tests.js index 3699f340ae2..b354e7f3c81 100644 --- a/addons/web/static/tests/search/search_panel_tests.js +++ b/addons/web/static/tests/search/search_panel_tests.js @@ -1060,7 +1060,7 @@ QUnit.module("Search", (hooks) => { promise.resolve(); await compPromise; - assert.verifySteps(["load_views", "search_panel_select_range"]); + assert.verifySteps(["get_views", "search_panel_select_range"]); // Case 2: search domain changed so we wait for the search panel once again promise = makeDeferred(); @@ -1123,7 +1123,7 @@ QUnit.module("Search", (hooks) => { await compPromise; assert.verifySteps([ - "load_views", + "get_views", "search_panel_select_range", "search_panel_select_multi_range", ]); @@ -1160,7 +1160,7 @@ QUnit.module("Search", (hooks) => { await compPromise; assert.verifySteps([ - "load_views", + "get_views", "search_panel_select_range", "search_panel_select_multi_range", ]); diff --git a/addons/web/static/tests/search/with_search_tests.js b/addons/web/static/tests/search/with_search_tests.js index 2224c9314ce..a881ee97b7e 100644 --- a/addons/web/static/tests/search/with_search_tests.js +++ b/addons/web/static/tests/search/with_search_tests.js @@ -137,8 +137,8 @@ QUnit.module("Search", (hooks) => { await makeWithSearch({ serverData, mockRPC: function (_, args) { - if (args.method === "load_views") { - throw new Error("No load_views should be done"); + if (args.method === "get_views") { + throw new Error("No get_views should be done"); } }, resModel: "animal", @@ -159,7 +159,7 @@ QUnit.module("Search", (hooks) => { await makeWithSearch({ serverData, mockRPC: function (_, args) { - if (args.method === "load_views") { + if (args.method === "get_views") { assert.deepEqual(args.kwargs, { context: { lang: "en", @@ -193,8 +193,8 @@ QUnit.module("Search", (hooks) => { await makeWithSearch({ serverData, mockRPC: function (_, args) { - if (args.method === "load_views") { - throw new Error("No load_views should be done"); + if (args.method === "get_views") { + throw new Error("No get_views should be done"); } }, resModel: "animal", @@ -218,7 +218,7 @@ QUnit.module("Search", (hooks) => { await makeWithSearch({ serverData, mockRPC: function (_, args) { - if (args.method === "load_views") { + if (args.method === "get_views") { assert.deepEqual(args.kwargs.options, { action_id: false, load_filters: true, @@ -253,7 +253,7 @@ QUnit.module("Search", (hooks) => { await makeWithSearch({ serverData, mockRPC: function (_, args) { - if (args.method === "load_views") { + if (args.method === "get_views") { assert.deepEqual(args.kwargs.views, [[1, "search"]]); } }, diff --git a/addons/web/static/tests/views/helpers.js b/addons/web/static/tests/views/helpers.js index fa8d0a4271a..8cc6021d233 100644 --- a/addons/web/static/tests/views/helpers.js +++ b/addons/web/static/tests/views/helpers.js @@ -3,7 +3,6 @@ import { makeTestEnv } from "@web/../tests/helpers/mock_env"; import { getFixture, mount } from "@web/../tests/helpers/utils"; import { getDefaultConfig, View } from "@web/views/view"; -import { _fieldsViewGet } from "../helpers/mock_server"; import { addLegacyMockEnvironment } from "../webclient/helpers"; /** @@ -35,30 +34,19 @@ export const makeView = async (params) => { delete props.legacyParams; delete props.config; - const env = await makeTestEnv({ serverData, mockRPC, config }); - if (props.arch) { - const defaultFields = serverData.models[props.resModel].fields; - if (!props.fields) { - props.fields = Object.assign({}, defaultFields); - // write the field name inside the field description (as done by fields_get) - for (const fieldName in props.fields) { - props.fields[fieldName].name = fieldName; - } - } - const fvg = _fieldsViewGet({ - arch: props.arch, - modelName: props.resModel, - fields: props.fields, - context: props.context || {}, - models: serverData.models, - }); - props.arch = fvg.arch; - props.fields = Object.assign({}, props.fields, fvg.fields); - props.searchViewArch = props.searchViewArch || ""; - props.searchViewFields = props.searchViewFields || Object.assign({}, props.fields); + serverData.views = serverData.views || {}; + props.viewId = 100000001; // hopefully will not conflict with an id already in views + serverData.views[`${props.resModel},${props.viewId},${props.type}`] = props.arch; + delete props.arch; + props.searchViewId = 100000002; // hopefully will not conflict with an id already in views + const searchViewArch = props.searchViewArch || ""; + serverData.views[`${props.resModel},${props.searchViewId},search`] = searchViewArch; + delete props.searchViewArch; } + const env = await makeTestEnv({ serverData, mockRPC, config }); + /** Legacy Environment, for compatibility sakes * Remove this as soon as we drop the legacy support */ diff --git a/addons/web/static/tests/views/pivot_view_tests.js b/addons/web/static/tests/views/pivot_view_tests.js index 8e3dee7299a..46b5e519f4b 100644 --- a/addons/web/static/tests/views/pivot_view_tests.js +++ b/addons/web/static/tests/views/pivot_view_tests.js @@ -211,11 +211,13 @@ QUnit.module("Views", (hooks) => { `, mockRPC(route, args) { - assert.strictEqual( - args.kwargs.lazy, - false, - "the read_group should be done with the lazy=false option" - ); + if (args.method === "read_group") { + assert.strictEqual( + args.kwargs.lazy, + false, + "the read_group should be done with the lazy=false option" + ); + } }, }); @@ -404,6 +406,11 @@ QUnit.module("Views", (hooks) => { QUnit.test("clicking on a cell triggers a doAction", async function (assert) { assert.expect(2); + serverData.views = { + "partner,2,form": "
", + "partner,false,list": "", + "partner,5,kanban": "", + }; patchWithCleanup(session, { user_context: { userContextKey: true }, }); @@ -748,14 +755,16 @@ QUnit.module("Views", (hooks) => { `, - mockRPC(route, params) { - const wrongFields = params.kwargs.fields.filter((field) => { - return !(field.split(":")[0] in serverData.models.partner.fields); - }); - assert.ok( - !wrongFields.length, - "fields given to read_group should exist on the model" - ); + mockRPC(route, args) { + if (args.method === "read_group") { + const wrongFields = args.kwargs.fields.filter((field) => { + return !(field.split(":")[0] in serverData.models.partner.fields); + }); + assert.ok( + !wrongFields.length, + "fields given to read_group should exist on the model" + ); + } }, }); }); @@ -850,7 +859,6 @@ QUnit.module("Views", (hooks) => { assert.expect(7); let rpcCount = 0; - await makeView({ type: "pivot", resModel: "partner", @@ -860,8 +868,10 @@ QUnit.module("Views", (hooks) => { `, - mockRPC() { - rpcCount++; + mockRPC(route, args) { + if (args.method === "read_group") { + rpcCount++; + } }, }); assert.containsN( @@ -4571,23 +4581,25 @@ QUnit.module("Views", (hooks) => { `, mockRPC(route, args) { - if (args.method === "read_group" && isSecondReadGroup) { - return Promise.resolve([ - { - customer: [2, "Second"], - foo: 18, - __count: 2, - __domain: [["customer", "=", 2]], - }, - { - customer: [1, "First"], - foo: 14, - __count: 2, - __domain: [["customer", "=", 1]], - }, - ]); + if (args.method === "read_group") { + if (isSecondReadGroup) { + return Promise.resolve([ + { + customer: [2, "Second"], + foo: 18, + __count: 2, + __domain: [["customer", "=", 2]], + }, + { + customer: [1, "First"], + foo: 14, + __count: 2, + __domain: [["customer", "=", 1]], + }, + ]); + } + isSecondReadGroup = true; } - isSecondReadGroup = true; }, }); @@ -4780,7 +4792,7 @@ QUnit.module("Views", (hooks) => { assert.containsOnce(target, ".o_pivot_view"); assert.strictEqual(getCurrentValues(target), ["4", "2", "2"].join(",")); - assert.verifySteps(["/web/webclient/load_menus", "load_views", "read_group", "read_group"]); + assert.verifySteps(["/web/webclient/load_menus", "get_views", "read_group", "read_group"]); // switch to list view await click(target.querySelector(".o_control_panel .o_switch_view.o_list")); diff --git a/addons/web/static/tests/views/view_service_tests.js b/addons/web/static/tests/views/view_service_tests.js index 904c037c77d..f4e78e36d01 100644 --- a/addons/web/static/tests/views/view_service_tests.js +++ b/addons/web/static/tests/views/view_service_tests.js @@ -20,16 +20,29 @@ QUnit.module("View service", (hooks) => { }, }; + const fakeUiService = { + start(env) { + Object.defineProperty(env, "isSmall", { + get() { + return false; + }, + }); + }, + }; serverData = { models, views }; - registry.category("services").add("views", viewService).add("orm", ormService); + registry + .category("services") + .add("views", viewService) + .add("orm", ormService) + .add("ui", fakeUiService); }); QUnit.test("stores calls in cache in success", async (assert) => { assert.expect(2); const mockRPC = (route, args) => { - if (route.includes("load_views")) { - assert.step("load_views"); + if (route.includes("get_views")) { + assert.step("get_views"); } }; @@ -51,15 +64,15 @@ QUnit.module("View service", (hooks) => { {} ); - assert.verifySteps(["load_views"]); + assert.verifySteps(["get_views"]); }); QUnit.test("stores calls in cache when failed", async (assert) => { assert.expect(5); const mockRPC = (route, args) => { - if (route.includes("load_views")) { - assert.step("load_views"); + if (route.includes("get_views")) { + assert.step("get_views"); return Promise.reject("my little error"); } }; @@ -91,6 +104,6 @@ QUnit.module("View service", (hooks) => { assert.strictEqual(error, "my little error"); } - assert.verifySteps(["load_views", "load_views"]); + assert.verifySteps(["get_views", "get_views"]); }); }); diff --git a/addons/web/static/tests/views/view_tests.js b/addons/web/static/tests/views/view_tests.js index 22f3e57e57e..fa15569ae21 100644 --- a/addons/web/static/tests/views/view_tests.js +++ b/addons/web/static/tests/views/view_tests.js @@ -10,13 +10,12 @@ import { patchWithCleanup, } from "@web/../tests/helpers/utils"; import { setupControlPanelServiceRegistry } from "@web/../tests/search/helpers"; -import { makeView } from "@web/../tests/views/helpers"; import { registry } from "@web/core/registry"; import { OnboardingBanner } from "@web/views/onboarding_banner"; import { View } from "@web/views/view"; import { actionService } from "@web/webclient/actions/action_service"; -const { Component, useState, xml } = owl; +const { Component, onWillStart, onWillUpdateProps, useState, xml } = owl; const serviceRegistry = registry.category("services"); const viewRegistry = registry.category("views"); @@ -110,7 +109,7 @@ QUnit.module("Views", (hooks) => { QUnit.module("View component"); //////////////////////////////////////////////////////////////////////////// - // load_views + // get_views //////////////////////////////////////////////////////////////////////////// QUnit.test("simple rendering", async function (assert) { @@ -128,21 +127,22 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: (_, args) => { - assert.strictEqual(args.model, "animal"); - assert.strictEqual(args.method, "load_views"); - assert.deepEqual(args.kwargs.views, [[false, "toy"]]); - assert.deepEqual(args.kwargs.options, { - action_id: false, - load_filters: false, - toolbar: false, - }); - }, + const mockRPC = (_, args) => { + assert.strictEqual(args.model, "animal"); + assert.strictEqual(args.method, "get_views"); + assert.deepEqual(args.kwargs.views, [[false, "toy"]]); + assert.deepEqual(args.kwargs.options, { + action_id: false, + load_filters: false, + toolbar: false, + }); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view.o_view_controller"); assert.strictEqual( target.querySelector(".o_toy_view.toy").innerHTML, @@ -165,20 +165,21 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: (_, args) => { - assert.deepEqual(args.kwargs.views, [[1, "toy"]]); - assert.deepEqual(args.kwargs.options, { - action_id: false, - load_filters: false, - toolbar: false, - }); - }, + const mockRPC = (_, args) => { + assert.deepEqual(args.kwargs.views, [[1, "toy"]]); + assert.deepEqual(args.kwargs.options, { + action_id: false, + load_filters: false, + toolbar: false, + }); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", viewId: 1, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view.toy").innerHTML, @@ -201,23 +202,23 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: (_, args) => { - console.log(_); - assert.deepEqual(args.kwargs.views, [[1, "toy"]]); - assert.deepEqual(args.kwargs.options, { - action_id: false, - load_filters: false, - toolbar: false, - }); - }, + const mockRPC = (_, args) => { + assert.deepEqual(args.kwargs.views, [[1, "toy"]]); + assert.deepEqual(args.kwargs.options, { + action_id: false, + load_filters: false, + toolbar: false, + }); + }; + const config = { + views: [[1, "toy"]], + }; + const env = await makeTestEnv({ serverData, mockRPC, config }); + const props = { resModel: "animal", type: "toy", - config: { - views: [[1, "toy"]], - }, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view.toy").innerHTML, @@ -242,25 +243,26 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: (_, args) => { - assert.deepEqual(args.kwargs.views, [ - [false, "other"], - [false, "toy"], - ]); - assert.deepEqual(args.kwargs.options, { - action_id: false, - load_filters: false, - toolbar: false, - }); - }, + const mockRPC = (_, args) => { + assert.deepEqual(args.kwargs.views, [ + [false, "other"], + [false, "toy"], + ]); + assert.deepEqual(args.kwargs.options, { + action_id: false, + load_filters: false, + toolbar: false, + }); + }; + const config = { + views: [[false, "other"]], + }; + const env = await makeTestEnv({ serverData, mockRPC, config }); + const props = { resModel: "animal", type: "toy", - config: { - views: [[false, "other"]], - }, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view.toy").innerHTML, @@ -284,29 +286,30 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: (_, args) => { - assert.deepEqual(args.kwargs.views, [ - [1, "toy"], - [false, "other"], - ]); - assert.deepEqual(args.kwargs.options, { - action_id: false, - load_filters: false, - toolbar: false, - }); - }, + const mockRPC = (_, args) => { + assert.deepEqual(args.kwargs.views, [ + [1, "toy"], + [false, "other"], + ]); + assert.deepEqual(args.kwargs.options, { + action_id: false, + load_filters: false, + toolbar: false, + }); + }; + const config = { + views: [ + [3, "toy"], + [false, "other"], + ], + }; + const env = await makeTestEnv({ serverData, mockRPC, config }); + const props = { resModel: "animal", type: "toy", viewId: 1, - config: { - views: [ - [3, "toy"], - [false, "other"], - ], - }, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view.toy").innerHTML, @@ -329,16 +332,17 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: () => { - throw new Error("no RPC expected"); - }, + const mockRPC = () => { + throw new Error("no RPC expected"); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", arch: `Specific arch content`, fields: {}, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view.toy").innerHTML, @@ -361,21 +365,22 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: (_, args) => { - // the rpc is done for fields - assert.deepEqual(args.kwargs.views, [[false, "toy"]]); - assert.deepEqual(args.kwargs.options, { - action_id: false, - load_filters: false, - toolbar: true, - }); - }, + const mockRPC = (_, args) => { + // the rpc is done for fields + assert.deepEqual(args.kwargs.views, [[false, "toy"]]); + assert.deepEqual(args.kwargs.options, { + action_id: false, + load_filters: false, + toolbar: true, + }); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", loadActionMenus: true, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view.toy").innerHTML, @@ -400,23 +405,24 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: (_, args) => { - // the rpc is done for fields - assert.deepEqual(args.kwargs.views, [[false, "toy"]]); - assert.deepEqual(args.kwargs.options, { - action_id: false, - load_filters: false, - toolbar: true, - }); - }, + const mockRPC = (_, args) => { + // the rpc is done for fields + assert.deepEqual(args.kwargs.views, [[false, "toy"]]); + assert.deepEqual(args.kwargs.options, { + action_id: false, + load_filters: false, + toolbar: true, + }); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", arch: `Specific arch content`, fields: {}, loadActionMenus: true, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view.toy").innerHTML, @@ -442,11 +448,11 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: () => { - throw new Error("no RPC expected"); - }, + const mockRPC = () => { + throw new Error("no RPC expected"); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", arch: `Specific arch content`, @@ -455,7 +461,8 @@ QUnit.module("Views", (hooks) => { /** ... */ }, loadActionMenus: true, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view.toy").innerHTML, @@ -484,24 +491,25 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: (_, args) => { - // the rpc is done for fields - assert.deepEqual(args.kwargs.views, [ - [false, "toy"], - [false, "search"], - ]); - assert.deepEqual(args.kwargs.options, { - action_id: false, - load_filters: false, - toolbar: false, - }); - }, + const mockRPC = (_, args) => { + // the rpc is done for fields + assert.deepEqual(args.kwargs.views, [ + [false, "toy"], + [false, "search"], + ]); + assert.deepEqual(args.kwargs.options, { + action_id: false, + load_filters: false, + toolbar: false, + }); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", searchViewId: false, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view").innerText, @@ -531,11 +539,11 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: () => { - throw new Error("no RPC expected"); - }, + const mockRPC = () => { + throw new Error("no RPC expected"); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", arch: `Specific arch content`, @@ -543,7 +551,8 @@ QUnit.module("Views", (hooks) => { searchViewId: false, searchViewArch: ``, searchViewFields: {}, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view").innerText, @@ -574,18 +583,19 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: () => { - throw new Error("no RPC expected"); - }, + const mockRPC = () => { + throw new Error("no RPC expected"); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", arch: `Specific arch content`, fields: {}, searchViewArch: ``, searchViewFields: {}, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view").innerText, @@ -616,20 +626,20 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: (_, args) => { - // the rpc is done for fields - assert.deepEqual(args.kwargs.views, [ - [false, "toy"], - [false, "search"], - ]); - assert.deepEqual(args.kwargs.options, { - action_id: false, - load_filters: true, - toolbar: false, - }); - }, + const mockRPC = (_, args) => { + // the rpc is done for fields + assert.deepEqual(args.kwargs.views, [ + [false, "toy"], + [false, "search"], + ]); + assert.deepEqual(args.kwargs.options, { + action_id: false, + load_filters: true, + toolbar: false, + }); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", arch: `Specific arch content`, @@ -638,7 +648,8 @@ QUnit.module("Views", (hooks) => { searchViewArch: ``, searchViewFields: {}, loadIrFilters: true, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view").innerText, @@ -680,11 +691,11 @@ QUnit.module("Views", (hooks) => { }, }); - await makeView({ - serverData, - mockRPC: () => { - throw new Error("no RPC expected"); - }, + const mockRPC = () => { + throw new Error("no RPC expected"); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", arch: `Specific arch content`, @@ -693,7 +704,8 @@ QUnit.module("Views", (hooks) => { searchViewFields: {}, loadIrFilters: true, irFilters, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view"); assert.strictEqual( target.querySelector(".o_toy_view").innerText, @@ -741,16 +753,15 @@ QUnit.module("Views", (hooks) => { }; } }; - - await makeView({ - serverData, - mockRPC, + const config = { + views: [[1, "toy"]], + }; + const env = await makeTestEnv({ serverData, mockRPC, config }); + const props = { resModel: "animal", type: "toy", - config: { - views: [[1, "toy"]], - }, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, "a"); await click(target.querySelector("a")); @@ -788,14 +799,15 @@ QUnit.module("Views", (hooks) => { `; - await makeView({ - serverData, + const config = { + views: [[1, "toy"]], + }; + const env = await makeTestEnv({ serverData, config }); + const props = { resModel: "animal", type: "toy", - config: { - views: [[1, "toy"]], - }, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, "a"); await click(target.querySelector("a")); @@ -840,14 +852,15 @@ QUnit.module("Views", (hooks) => { `; - await makeView({ - serverData, + const config = { + views: [[1, "toy"]], + }; + const env = await makeTestEnv({ serverData, config }); + const props = { resModel: "animal", type: "toy", - config: { - views: [[1, "toy"]], - }, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, "a"); await click(target.querySelector("a")); @@ -866,16 +879,15 @@ QUnit.module("Views", (hooks) => { return { html: `
myBanner
` }; } }; - - await makeView({ - serverData, - mockRPC, + const config = { + views: [[1, "toy"]], + }; + const env = await makeTestEnv({ serverData, mockRPC, config }); + const props = { resModel: "animal", type: "toy", - config: { - views: [[1, "toy"]], - }, - }); + }; + await mount(View, target, { env, props }); assert.verifySteps(["/mybody/isacage"]); assert.containsOnce(target, ".setmybodyfree"); @@ -929,15 +941,15 @@ QUnit.module("Views", (hooks) => { patchWithCleanup(document, { createElement }); - await makeView({ - serverData, - mockRPC, + const config = { + views: [[1, "toy"]], + }; + const env = await makeTestEnv({ serverData, mockRPC, config }); + const props = { resModel: "animal", type: "toy", - config: { - views: [[1, "toy"]], - }, - }); + }; + await mount(View, target, { env, props }); assert.verifySteps(["/mybody/isacage", "js loaded", "css loaded"]); assert.containsOnce(target, ".setmybodyfree"); @@ -946,7 +958,6 @@ QUnit.module("Views", (hooks) => { }); QUnit.test("banner can re-render with new HTML", async (assert) => { - assert.expect(10); assert.expect(8); serviceRegistry.add("action", actionService, { force: true }); @@ -975,16 +986,15 @@ QUnit.module("Views", (hooks) => { }; } }; - - await makeView({ - serverData, - mockRPC, + const config = { + views: [[1, "toy"]], + }; + const env = await makeTestEnv({ serverData, mockRPC, config }); + const props = { resModel: "animal", type: "toy", - config: { - views: [[1, "toy"]], - }, - }); + }; + await mount(View, target, { env, props }); assert.verifySteps(["/mybody/isacage"]); assert.containsOnce(target, ".banner1"); @@ -1007,22 +1017,31 @@ QUnit.module("Views", (hooks) => { myBanner `; + let toy; + const ToyView = viewRegistry.get("toy"); + class ToyViewExtended extends ToyView { + setup() { + super.setup(); + toy = this; + } + } + viewRegistry.add("toy", ToyViewExtended, { force: true }); + const mockRPC = (route) => { if (route === "/mybody/isacage") { assert.step(route); return { html: bannerArch }; } }; - - const toy = await makeView({ - serverData, - mockRPC, + const config = { + views: [[1, "toy"]], + }; + const env = await makeTestEnv({ serverData, mockRPC, config }); + const props = { resModel: "animal", type: "toy", - config: { - views: [[1, "toy"]], - }, - }); + }; + await mount(View, target, { env, props }); assert.verifySteps(["/mybody/isacage"]); assert.containsOnce(target, ".setmybodyfree"); @@ -1081,16 +1100,15 @@ QUnit.module("Views", (hooks) => { }; } }; - - await makeView({ - mockRPC, - serverData, + const config = { + views: [[1, "toy"]], + }; + const env = await makeTestEnv({ serverData, mockRPC, config }); + const props = { resModel: "animal", type: "toy", - config: { - views: [[1, "toy"]], - }, - }); + }; + await mount(View, target, { env, props }); await click(target.querySelector("a[data-method='setTheControl']")); click(target.querySelector("a[data-method='heartOfTheSun']")); @@ -1147,16 +1165,15 @@ QUnit.module("Views", (hooks) => { return true; } }; - - await makeView({ - serverData, - mockRPC, + const config = { + views: [[1, "toy"]], + }; + const env = await makeTestEnv({ serverData, mockRPC, config }); + const props = { resModel: "animal", type: "toy", - config: { - views: [[1, "toy"]], - }, - }); + }; + await mount(View, target, { env, props }); const prom = new Promise((resolve) => { const complete = (ev) => { @@ -1191,19 +1208,20 @@ QUnit.module("Views", (hooks) => { QUnit.test("rendering with given jsClass", async function (assert) { assert.expect(4); - await makeView({ - serverData, - mockRPC: (_, args) => { - assert.deepEqual(args.kwargs.views, [[false, "toy"]]); - assert.deepEqual(args.kwargs.options, { - action_id: false, - load_filters: false, - toolbar: false, - }); - }, + const mockRPC = (_, args) => { + assert.deepEqual(args.kwargs.views, [[false, "toy"]]); + assert.deepEqual(args.kwargs.options, { + action_id: false, + load_filters: false, + toolbar: false, + }); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy_imp", - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view.toy_imp"); assert.strictEqual( target.querySelector(".o_toy_view.toy_imp").innerText, @@ -1213,20 +1231,21 @@ QUnit.module("Views", (hooks) => { QUnit.test("rendering with loaded arch attribute 'js_class'", async function (assert) { assert.expect(4); - await makeView({ - serverData, - mockRPC: (_, args) => { - assert.deepEqual(args.kwargs.views, [[2, "toy"]]); - assert.deepEqual(args.kwargs.options, { - action_id: false, - load_filters: false, - toolbar: false, - }); - }, + const mockRPC = (_, args) => { + assert.deepEqual(args.kwargs.views, [[2, "toy"]]); + assert.deepEqual(args.kwargs.options, { + action_id: false, + load_filters: false, + toolbar: false, + }); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", viewId: 2, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view.toy_imp"); assert.strictEqual( target.querySelector(".o_toy_view.toy_imp").innerText, @@ -1235,17 +1254,17 @@ QUnit.module("Views", (hooks) => { }); QUnit.test("rendering with given arch attribute 'js_class'", async function (assert) { - assert.expect(2); - await makeView({ - serverData, - mockRPC: () => { - throw new Error("no RPC expected"); - }, + const mockRPC = () => { + throw new Error("no RPC expected"); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy", arch: `Specific arch content for specific class`, fields: {}, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view.toy_imp"); assert.strictEqual( target.querySelector(".o_toy_view.toy_imp").innerText, @@ -1263,20 +1282,21 @@ QUnit.module("Views", (hooks) => { ToyView2.type = "toy"; viewRegistry.add("toy_2", ToyView2); - await makeView({ - serverData, - mockRPC: (_, args) => { - assert.deepEqual(args.kwargs.views, [[2, "toy"]]); - assert.deepEqual(args.kwargs.options, { - action_id: false, - load_filters: false, - toolbar: false, - }); - }, + const mockRPC = (_, args) => { + assert.deepEqual(args.kwargs.views, [[2, "toy"]]); + assert.deepEqual(args.kwargs.options, { + action_id: false, + load_filters: false, + toolbar: false, + }); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy_2", viewId: 2, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view.toy_imp", "jsClass from arch prefered"); } ); @@ -1284,23 +1304,22 @@ QUnit.module("Views", (hooks) => { QUnit.test( "rendering with given arch attribute 'js_class' and given jsClass", async function (assert) { - assert.expect(1); - class ToyView2 extends Component {} ToyView2.template = xml`
`; ToyView2.type = "toy"; viewRegistry.add("toy_2", ToyView2); - await makeView({ - serverData, - mockRPC: () => { - throw new Error("no RPC expected"); - }, + const mockRPC = () => { + throw new Error("no RPC expected"); + }; + const env = await makeTestEnv({ serverData, mockRPC }); + const props = { resModel: "animal", type: "toy_2", arch: ``, fields: {}, - }); + }; + await mount(View, target, { env, props }); assert.containsOnce(target, ".o_toy_view.toy_imp", "jsClass from arch prefered"); } ); @@ -1310,9 +1329,10 @@ QUnit.module("Views", (hooks) => { //////////////////////////////////////////////////////////////////////////// QUnit.test("'resModel' must be passed as prop", async function (assert) { - assert.expect(2); + const env = await makeTestEnv({ serverData }); + const props = {}; try { - await makeView({ serverData }); + await mount(View, target, { env, props }); } catch (error) { assert.step(error.message); } @@ -1320,9 +1340,10 @@ QUnit.module("Views", (hooks) => { }); QUnit.test("'type' must be passed as prop", async function (assert) { - assert.expect(2); + const env = await makeTestEnv({ serverData }); + const props = { resModel: "animal" }; try { - await makeView({ serverData, resModel: "animal" }); + await mount(View, target, { env, props }); } catch (error) { assert.step(error.message); } @@ -1330,9 +1351,7 @@ QUnit.module("Views", (hooks) => { }); QUnit.test("'arch' cannot be passed as prop alone", async function (assert) { - assert.expect(2); const env = await makeTestEnv({ serverData }); - const target = getFixture(); const props = { resModel: "animal", type: "toy", arch: "" }; try { await mount(View, target, { env, props }); @@ -1343,9 +1362,10 @@ QUnit.module("Views", (hooks) => { }); QUnit.test("'fields' cannot be passed as prop alone", async function (assert) { - assert.expect(2); + const env = await makeTestEnv({ serverData }); + const props = { resModel: "animal", type: "toy", fields: {} }; try { - await makeView({ serverData, resModel: "animal", type: "toy", fields: {} }); + await mount(View, target, { env, props }); } catch (error) { assert.step(error.message); } @@ -1353,14 +1373,10 @@ QUnit.module("Views", (hooks) => { }); QUnit.test("'searchViewArch' cannot be passed as prop alone", async function (assert) { - assert.expect(2); + const env = await makeTestEnv({ serverData }); + const props = { resModel: "animal", type: "toy", searchViewArch: "" }; try { - await makeView({ - serverData, - resModel: "animal", - type: "toy", - searchViewArch: "", - }); + await mount(View, target, { env, props }); } catch (error) { assert.step(error.message); } @@ -1370,9 +1386,10 @@ QUnit.module("Views", (hooks) => { }); QUnit.test("'searchViewFields' cannot be passed as prop alone", async function (assert) { - assert.expect(2); + const env = await makeTestEnv({ serverData }); + const props = { resModel: "animal", type: "toy", searchViewFields: {} }; try { - await makeView({ serverData, resModel: "animal", type: "toy", searchViewFields: {} }); + await mount(View, target, { env, props }); } catch (error) { assert.step(error.message); } @@ -1409,15 +1426,16 @@ QUnit.module("Views", (hooks) => { viewRegistry.add("toy", ToyView, { force: true }); - await makeView({ - serverData, + const env = await makeTestEnv({ serverData }); + const props = { resModel: "animal", type: "toy", domain: [[0, "=", 1]], groupBy: ["birthday"], context: { key: "val" }, orderBy: ["bar"], - }); + }; + await mount(View, target, { env, props }); } ); @@ -1433,12 +1451,13 @@ QUnit.module("Views", (hooks) => { ToyView.type = "toy"; viewRegistry.add("toy", ToyView, { force: true }); - await makeView({ - serverData, + const env = await makeTestEnv({ serverData }); + const props = { resModel: "animal", type: "toy", noContentHelp: "
Help
", - }); + }; + await mount(View, target, { env, props }); }); QUnit.test("useSampleModel false by default", async function (assert) { @@ -1453,7 +1472,9 @@ QUnit.module("Views", (hooks) => { ToyView.type = "toy"; viewRegistry.add("toy", ToyView, { force: true }); - await makeView({ serverData, resModel: "animal", type: "toy" }); + const env = await makeTestEnv({ serverData }); + const props = { resModel: "animal", type: "toy" }; + await mount(View, target, { env, props }); }); QUnit.test("sample='1' on arch", async function (assert) { @@ -1468,13 +1489,14 @@ QUnit.module("Views", (hooks) => { ToyView.type = "toy"; viewRegistry.add("toy", ToyView, { force: true }); - await makeView({ - serverData, + const env = await makeTestEnv({ serverData }); + const props = { resModel: "animal", type: "toy", arch: ``, fields: {}, - }); + }; + await mount(View, target, { env, props }); }); QUnit.test("sample='0' on arch and useSampleModel=true", async function (assert) { @@ -1489,14 +1511,15 @@ QUnit.module("Views", (hooks) => { ToyView.type = "toy"; viewRegistry.add("toy", ToyView, { force: true }); - await makeView({ - serverData, + const env = await makeTestEnv({ serverData }); + const props = { resModel: "animal", type: "toy", useSampleModel: true, arch: ``, fields: {}, - }); + }; + await mount(View, target, { env, props }); }); QUnit.test("sample='1' on arch and useSampleModel=false", async function (assert) { @@ -1511,14 +1534,15 @@ QUnit.module("Views", (hooks) => { ToyView.type = "toy"; viewRegistry.add("toy", ToyView, { force: true }); - await makeView({ - serverData, + const env = await makeTestEnv({ serverData }); + const props = { resModel: "animal", type: "toy", useSampleModel: false, arch: ``, fields: {}, - }); + }; + await mount(View, target, { env, props }); }); QUnit.test("useSampleModel=true", async function (assert) { @@ -1533,7 +1557,9 @@ QUnit.module("Views", (hooks) => { ToyView.type = "toy"; viewRegistry.add("toy", ToyView, { force: true }); - await makeView({ serverData, resModel: "animal", type: "toy", useSampleModel: true }); + const env = await makeTestEnv({ serverData }); + const props = { resModel: "animal", type: "toy", useSampleModel: true }; + await mount(View, target, { env, props }); }); QUnit.test("rendering with given prop", async function (assert) { @@ -1548,12 +1574,9 @@ QUnit.module("Views", (hooks) => { ToyView.type = "toy"; viewRegistry.add("toy", ToyView, { force: true }); - await makeView({ - serverData, - resModel: "animal", - type: "toy", - specificProp: "specificProp", - }); + const env = await makeTestEnv({ serverData }); + const props = { resModel: "animal", type: "toy", specificProp: "specificProp" }; + await mount(View, target, { env, props }); }); QUnit.test( @@ -1578,8 +1601,8 @@ QUnit.module("Views", (hooks) => { ToyView.type = "toy"; viewRegistry.add("toy", ToyView, { force: true }); - await makeView({ - serverData, + const env = await makeTestEnv({ serverData }); + const props = { type: "toy", resModel: "animal", searchViewId: 1, @@ -1587,7 +1610,8 @@ QUnit.module("Views", (hooks) => { groupBy: ["birthday"], context: { search_default_filter: 1, search_default_group_by: 1 }, orderBy: ["bar"], - }); + }; + await mount(View, target, { env, props }); } ); @@ -1600,10 +1624,10 @@ QUnit.module("Views", (hooks) => { class ToyView extends Component { setup() { - owl.onWillStart(() => { + onWillStart(() => { assert.deepEqual(this.props.domain, [["type", "=", "carnivorous"]]); }); - owl.onWillUpdateProps((nextProps) => { + onWillUpdateProps((nextProps) => { assert.deepEqual(nextProps.domain, [["type", "=", "herbivorous"]]); }); } @@ -1626,7 +1650,6 @@ QUnit.module("Views", (hooks) => { Parent.template = xml``; Parent.components = { View }; - const target = getFixture(); const parent = await mount(Parent, target, { env }); parent.state.domain = [["type", "=", "herbivorous"]]; diff --git a/addons/web/static/tests/webclient/actions/concurrency_tests.js b/addons/web/static/tests/webclient/actions/concurrency_tests.js index 6fdaf48b963..31292228366 100644 --- a/addons/web/static/tests/webclient/actions/concurrency_tests.js +++ b/addons/web/static/tests/webclient/actions/concurrency_tests.js @@ -49,7 +49,7 @@ QUnit.module("ActionManager", (hooks) => { "/web/webclient/load_menus", "/web/action/load", "/web/action/load", - "/web/dataset/call_kw/pony/load_views", + "/web/dataset/call_kw/pony/get_views", "/web/dataset/search_read", ]); }); @@ -80,7 +80,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "/web/dataset/call_kw/partner/load_views", + "/web/dataset/call_kw/partner/get_views", "/web/dataset/search_read", "/web/dataset/search_read", "/web/dataset/search_read", @@ -187,11 +187,11 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "read", "/web/dataset/search_read", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", ]); // unblock the switch to Kanban in action 4 @@ -238,12 +238,12 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "read", "object", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", ]); // unblock the call_button request @@ -302,11 +302,11 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "read", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", ]); // unblock the switch to the form view in action 3 @@ -332,12 +332,12 @@ QUnit.module("ActionManager", (hooks) => { const def = testUtils.makeTestPromise(); const mockRPC = async function (route, args) { assert.step((args && args.method) || route); - if (args && args.method === "load_views") { + if (args && args.method === "get_views") { await def; } }; const webClient = await createWebClient({ serverData, mockRPC }); - // execute a first action (its 'load_views' RPC is blocked) + // execute a first action (its 'get_views' RPC is blocked) doAction(webClient, 3); await testUtils.nextTick(); await legacyExtraNextTick(); @@ -358,9 +358,9 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", ]); }); @@ -395,10 +395,10 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", ]); }); @@ -499,7 +499,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "read", "/web/action/load", @@ -537,19 +537,19 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "/web/action/load", "/web/dataset/search_read", ]); }); - QUnit.test("switching when doing an action -- load_views slow", async function (assert) { + QUnit.test("switching when doing an action -- get_views slow", async function (assert) { assert.expect(13); let def; const mockRPC = async (route, args) => { assert.step((args && args.method) || route); - if (args && args.method === "load_views") { + if (args && args.method === "get_views") { return Promise.resolve(def); } }; @@ -574,10 +574,10 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", ]); }); @@ -611,10 +611,10 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "/web/dataset/search_read", ]); diff --git a/addons/web/static/tests/webclient/actions/load_state_tests.js b/addons/web/static/tests/webclient/actions/load_state_tests.js index fc9233372ae..afcfd375e1a 100644 --- a/addons/web/static/tests/webclient/actions/load_state_tests.js +++ b/addons/web/static/tests/webclient/actions/load_state_tests.js @@ -216,7 +216,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", ]); }); @@ -239,7 +239,7 @@ QUnit.module("ActionManager", (hooks) => { "Second record", "should have opened the second record" ); - assert.verifySteps(["/web/webclient/load_menus", "load_views", "read"]); + assert.verifySteps(["/web/webclient/load_menus", "get_views", "read"]); }); QUnit.test("properly load records with existing first APP", async function (assert) { @@ -266,7 +266,7 @@ QUnit.module("ActionManager", (hooks) => { "should have opened the second record" ); assert.containsNone(target, ".o_menu_brand"); - assert.verifySteps(["/web/webclient/load_menus", "load_views", "read"]); + assert.verifySteps(["/web/webclient/load_menus", "get_views", "read"]); }); QUnit.test("properly load default record", async function (assert) { @@ -287,7 +287,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "onchange", ]); }); @@ -309,7 +309,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", ]); }); @@ -345,7 +345,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "read", "/web/dataset/search_read", ]); @@ -458,12 +458,12 @@ QUnit.module("ActionManager", (hooks) => { "should have opened the requested record" ); // verify steps to ensure that the whole action hasn't been re-executed - // (if it would have been, /web/action/load and load_views would appear + // (if it would have been, /web/action/load and get_views would appear // several times) assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "/web/dataset/search_read", "read", @@ -507,12 +507,12 @@ QUnit.module("ActionManager", (hooks) => { "should have switched to the requested record" ); // verify steps to ensure that the whole action hasn't been re-executed - // (if it would have been, /web/action/load and load_views would appear + // (if it would have been, /web/action/load and get_views would appear // twice) assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "read", "read", @@ -717,7 +717,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "setItem session current_action", "getItem session current_action", @@ -749,7 +749,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "/web/dataset/call_kw/partner/load_views", + "/web/dataset/call_kw/partner/get_views", "/web/dataset/search_read", ]); }); @@ -799,7 +799,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "/web/dataset/call_kw/partner/load_views", + "/web/dataset/call_kw/partner/get_views", "/web/dataset/search_read", ]); }); @@ -830,7 +830,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "/web/dataset/call_kw/partner/load_views", + "/web/dataset/call_kw/partner/get_views", "/web/dataset/call_kw/partner/read", ]); await loadState(webClient, { diff --git a/addons/web/static/tests/webclient/actions/misc_tests.js b/addons/web/static/tests/webclient/actions/misc_tests.js index 0cf598d2e7e..86e936665f1 100644 --- a/addons/web/static/tests/webclient/actions/misc_tests.js +++ b/addons/web/static/tests/webclient/actions/misc_tests.js @@ -311,7 +311,7 @@ QUnit.module("ActionManager", (hooks) => { }, }); const mockRPC = async function (route, args) { - if (args && args.method === "load_views") { + if (args && args.method === "get_views") { await Promise.resolve(def); } }; @@ -319,7 +319,7 @@ QUnit.module("ActionManager", (hooks) => { // execute action 4 to know the number of widgets it instantiates await doAction(webClient, 4); const n = delta; - // execute a first action (its 'load_views' RPC is blocked) + // execute a first action (its 'get_views' RPC is blocked) def = testUtils.makeTestPromise(); doAction(webClient, 3, { clearBreadcrumbs: true }); await testUtils.nextTick(); diff --git a/addons/web/static/tests/webclient/actions/server_action_tests.js b/addons/web/static/tests/webclient/actions/server_action_tests.js index d08bff74d79..d1322301bd1 100644 --- a/addons/web/static/tests/webclient/actions/server_action_tests.js +++ b/addons/web/static/tests/webclient/actions/server_action_tests.js @@ -32,7 +32,7 @@ QUnit.module("ActionManager", (hooks) => { "/web/action/load", "/web/action/run", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", ]); }); @@ -62,7 +62,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "onchange", "/web/action/load", "/web/action/run", diff --git a/addons/web/static/tests/webclient/actions/target_tests.js b/addons/web/static/tests/webclient/actions/target_tests.js index 6fdbc735ddf..e30850d8b98 100644 --- a/addons/web/static/tests/webclient/actions/target_tests.js +++ b/addons/web/static/tests/webclient/actions/target_tests.js @@ -56,7 +56,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "onchange", ]); }); @@ -153,7 +153,7 @@ QUnit.module("ActionManager", (hooks) => { await doAction(webClient, 4); assert.verifySteps([ "/web/action/load", - "/web/dataset/call_kw/partner/load_views", + "/web/dataset/call_kw/partner/get_views", "/web/dataset/call_kw/partner/onchange", ]); await testUtils.dom.click(`button[name="5"]`); @@ -161,7 +161,7 @@ QUnit.module("ActionManager", (hooks) => { "/web/dataset/call_kw/partner/create", "/web/dataset/call_kw/partner/read", "/web/action/load", - "/web/dataset/call_kw/partner/load_views", + "/web/dataset/call_kw/partner/get_views", "/web/dataset/call_kw/partner/onchange", ]); await legacyExtraNextTick(); @@ -471,7 +471,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "read", ]); } diff --git a/addons/web/static/tests/webclient/actions/window_action_tests.js b/addons/web/static/tests/webclient/actions/window_action_tests.js index eeeabc0248b..a786baa9890 100644 --- a/addons/web/static/tests/webclient/actions/window_action_tests.js +++ b/addons/web/static/tests/webclient/actions/window_action_tests.js @@ -55,7 +55,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", ]); }); @@ -66,7 +66,7 @@ QUnit.module("ActionManager", (hooks) => { print: [{ name: "Print that record" }], }; const mockRPC = async (route, args) => { - if (args && args.method === "load_views") { + if (args && args.method === "get_views") { assert.strictEqual( args.kwargs.options.toolbar, true, @@ -122,7 +122,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "/web/dataset/search_read", "/web/dataset/search_read", @@ -176,7 +176,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "web_read_group", "/web/dataset/search_read", "/web/dataset/search_read", @@ -679,7 +679,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "onchange", "/web/dataset/search_read", @@ -737,7 +737,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "read", "object", @@ -884,11 +884,11 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "read", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", ]); }); @@ -935,10 +935,10 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "/web/dataset/call_kw/partner/load_views", + "/web/dataset/call_kw/partner/get_views", "/web/dataset/call_kw/partner/read", "/web/action/load", - "/web/dataset/call_kw/partner/load_views", + "/web/dataset/call_kw/partner/get_views", "/web/dataset/search_read", ]); }); @@ -1050,7 +1050,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "read", "/web/dataset/search_read", @@ -1191,7 +1191,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "/web/dataset/call_kw/partner/load_views", + "/web/dataset/call_kw/partner/get_views", "/web/dataset/search_read", "/web/dataset/call_kw/partner/read", "/web/dataset/call_kw/partner/get_formview_id", @@ -1416,7 +1416,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", ]); }); @@ -1449,7 +1449,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "onchange", ]); }); @@ -1702,12 +1702,12 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "load_views", + "get_views", "/web/dataset/search_read", "onchange", "get_formview_action", "create", // FIXME: to check with mcm - "load_views", + "get_views", "read", "read", ]); @@ -1722,7 +1722,7 @@ QUnit.module("ActionManager", (hooks) => { print: [], }; const mockRPC = async (route, args) => { - if (args && args.method === "load_views" && args.model === "partner") { + if (args && args.method === "get_views" && args.model === "partner") { assert.strictEqual( args.kwargs.options.toolbar, true, @@ -1992,7 +1992,7 @@ QUnit.module("ActionManager", (hooks) => { assert.verifySteps([ "/web/webclient/load_menus", "/web/action/load", - "/web/dataset/call_kw/partner/load_views", + "/web/dataset/call_kw/partner/get_views", "/web/dataset/call_kw/partner/read", ]); assert.containsN(target, ".o_form_buttons_view button:not([disabled])", 2); @@ -2365,7 +2365,7 @@ QUnit.module("ActionManager", (hooks) => { ); }); - QUnit.test("action and load_views rpcs are cached", async function (assert) { + QUnit.test("action and get_views rpcs are cached", async function (assert) { const mockRPC = async (route, args) => { assert.step(args.method || route); }; @@ -2374,7 +2374,7 @@ QUnit.module("ActionManager", (hooks) => { await doAction(webClient, 1); assert.containsOnce(target, ".o_kanban_view"); - assert.verifySteps(["/web/action/load", "load_views", "/web/dataset/search_read"]); + assert.verifySteps(["/web/action/load", "get_views", "/web/dataset/search_read"]); await doAction(webClient, 1); assert.containsOnce(target, ".o_kanban_view"); diff --git a/addons/web/static/tests/webclient/helpers.js b/addons/web/static/tests/webclient/helpers.js index 9fd42d88e68..99c226b8354 100644 --- a/addons/web/static/tests/webclient/helpers.js +++ b/addons/web/static/tests/webclient/helpers.js @@ -12,6 +12,7 @@ import { makeLegacySessionService, } from "@web/legacy/utils"; import { makeLegacyActionManagerService } from "@web/legacy/backend_utils"; +import { generateLegacyLoadViewsResult } from "@web/legacy/legacy_load_views"; import { viewService } from "@web/views/view_service"; import { actionService } from "@web/webclient/actions/action_service"; import { effectService } from "@web/core/effects/effect_service"; @@ -118,16 +119,18 @@ export function addLegacyMockEnvironment(env, legacyParams = {}) { }); }, load_views: async (params, options) => { - const result = await env.services.rpc(`/web/dataset/call_kw/${params.model}`, { + let result = await env.services.rpc(`/web/dataset/call_kw/${params.model}`, { args: [], kwargs: { context: params.context, options: options, views: params.views_descr, }, - method: "load_views", + method: "get_views", model: params.model, }); + const { models, views: _views } = result; + result = generateLegacyLoadViewsResult(params.model, _views, models); const views = result.fields_views; for (const [, viewType] of params.views_descr) { const fvg = views[viewType]; diff --git a/addons/web_editor/models/ir_qweb_fields.py b/addons/web_editor/models/ir_qweb_fields.py index d5a6b295c76..58e4d0e49a6 100644 --- a/addons/web_editor/models/ir_qweb_fields.py +++ b/addons/web_editor/models/ir_qweb_fields.py @@ -76,7 +76,7 @@ class IrQWeb(models.AbstractModel): key = el.attrib.pop('t-snippet') el.set('t-call', key) el.set('t-options', f"{{'snippet-key': {key!r}}}") - view = self.env['ir.ui.view']._get_view(key).sudo() + view = self.env['ir.ui.view']._get(key).sudo() name = view.name thumbnail = el.attrib.pop('t-thumbnail', "oe-thumbnail") div = '
' % ( diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index 00d61296f64..01aba7e14b9 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -409,7 +409,7 @@ class View(models.Model): def _render_template(self, template, values=None): """ Render the template. If website is enabled on request, then extend rendering context with website values. """ - view = self._get_view(template).sudo() + view = self._get(template).sudo() view._handle_visibility(do_raise=True) if values is None: values = {} diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 28b41bad262..db6c8c632d0 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -1090,7 +1090,7 @@ class Website(models.Model): def get_template(self, template): if isinstance(template, str) and '.' not in template: template = 'website.%s' % template - view = self.env['ir.ui.view']._get_view(template).sudo() + view = self.env['ir.ui.view']._get(template).sudo() if not view: raise NotFound return view diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py index 2828595d525..15e1d4a67ed 100644 --- a/odoo/addons/base/models/ir_actions.py +++ b/odoo/addons/base/models/ir_actions.py @@ -218,7 +218,7 @@ class IrActionsActWindow(models.Model): @api.depends('res_model', 'search_view_id') def _compute_search_view(self): for act in self: - fvg = self.env[act.res_model].fields_view_get(act.search_view_id.id, 'search') + fvg = self.env[act.res_model].get_view(act.search_view_id.id, 'search') act.search_view = str(fvg) name = fields.Char(string='Action Name', translate=True) diff --git a/odoo/addons/base/models/ir_module.py b/odoo/addons/base/models/ir_module.py index 58af110837e..0e4a5e1cf75 100644 --- a/odoo/addons/base/models/ir_module.py +++ b/odoo/addons/base/models/ir_module.py @@ -157,8 +157,8 @@ class Module(models.Model): _order = 'application desc,sequence,name' @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - res = super(Module, self).fields_view_get(view_id, view_type, toolbar=toolbar, submenu=False) + def get_view(self, view_id=None, view_type='form', **options): + res = super().get_view(view_id, view_type, **options) if view_type == 'form' and res.get('toolbar',False): install_id = self.env.ref('base.action_server_module_immediate_install').id action = [rec for rec in res['toolbar']['action'] if rec.get('id', False) != install_id] diff --git a/odoo/addons/base/models/ir_qweb.py b/odoo/addons/base/models/ir_qweb.py index 76089bc01fb..bcb50b1888d 100644 --- a/odoo/addons/base/models/ir_qweb.py +++ b/odoo/addons/base/models/ir_qweb.py @@ -772,7 +772,7 @@ class IrQWeb(models.AbstractModel): :rtype: Tuple[Union[etree, str], Optional[str, int]] """ IrUIView = self.env['ir.ui.view'].sudo() - view = IrUIView._get_view(ref) + view = IrUIView._get(ref) template = IrUIView._read_template(view.id) etree_view = etree.fromstring(template) diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 75a24ef78a6..385fbeb6f23 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -34,6 +34,23 @@ _logger = logging.getLogger(__name__) MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath', 'data-oe-source-id'] +ref_re = re.compile(r""" +# first match 'form_view_ref' key, backrefs are used to handle single or +# double quoting of the value +(['"])(?P\w+_view_ref)\1 +# colon separator (with optional spaces around) +\s*:\s* +# open quote for value +(['"]) +(?P + # we'll just match stuff which is normally part of an xid: + # word and "." characters + [.\w]+ +) +# close with same quote as opening +\3 +""", re.VERBOSE) + def quick_eval(expr, globals_dict): """ Functionally identical to safe_eval(), but optimized with special-casing. """ @@ -967,12 +984,26 @@ actual arch. del node.attrib['attrs'] # avoid making field visible later del node.attrib['groups'] + def _get_view_refs(self, node): + """ Extract the `[view_type]_view_ref` keys and values from the node context attribute, + giving the views to use for a field node. + + :param node: the field node as an etree + :return: a dictonary mapping the `[view_type]_view_ref` key to the xmlid of the view to use for that view type. + """ + if not node.get('context'): + return {} + return { + m.group('view_type'): m.group('view_id') + for m in ref_re.finditer(node.get('context')) + } + #------------------------------------------------------ # Postprocessing: translation, groups and modifiers #------------------------------------------------------ # TODO: remove group processing from ir_qweb #------------------------------------------------------ - def postprocess_and_fields(self, node, model=None): + def postprocess_and_fields(self, node, model=None, **options): """ Return an architecture and a description of all the fields. The field description combines the result of fields_get() and @@ -987,12 +1018,18 @@ actual arch. """ self and self.ensure_one() # self is at most one view - name_manager = self._postprocess_view(node, model or self.model) + name_manager = self._postprocess_view(node, model or self.model, **options) arch = etree.tostring(node, encoding="unicode").replace('\t', '') - return arch, dict(name_manager.available_fields) - def _postprocess_view(self, node, model_name, editable=True): + models = set() + name_managers = [name_manager] + for name_manager in name_managers: + models.add(name_manager.model._name) + name_managers.extend(name_manager.children) + return arch, models + + def _postprocess_view(self, node, model_name, editable=True, parent_name_manager=None, **options): """ Process the given architecture, modifying it in-place to add and remove stuff. @@ -1010,7 +1047,12 @@ actual arch. self._postprocess_on_change(root, model) - name_manager = NameManager(model) + name_manager = NameManager(model, parent=parent_name_manager) + + root_info = { + 'view_type': root.tag, + 'mobile': options.get('mobile'), + } # use a stack to recursively traverse the tree stack = [(root, editable)] @@ -1020,10 +1062,7 @@ actual arch. # compute default tag = node.tag parent = node.getparent() - node_info = { - 'modifiers': {}, - 'editable': editable and self._editable_node(node, name_manager), - } + node_info = dict(root_info, modifiers={}, editable=editable and self._editable_node(node, name_manager)) # tag-specific postprocessing postprocessor = getattr(self, f"_postprocess_tag_{tag}", None) @@ -1120,21 +1159,41 @@ actual arch. node.getparent().remove(node) # no point processing view-level ``groups`` anymore, return return - views = {} + if ( + node_info.get('view_type') == 'form' + and field.type in ('one2many', 'many2many') + and not node.get('widget') + and not node.get('invisible') + and not name_manager.parent + ): + # Embed kanban/tree/form views for visible x2many fields in form views + # if no widget or the widget requires it. + # So the web client doesn't have to call `get_views` for x2many fields not embedding their view + # in the main form view. + current_view_types = [el.tag for el in node.xpath("./*[descendant::field]")] + missing_view_types = [] + if 'form' not in current_view_types: + missing_view_types.append('form') + if not any(view_type in current_view_types for view_type in node.get('mode', 'kanban,tree').split(',')): + missing_view_types.append( + node.get('mode', 'kanban' if node_info.get('mobile') else 'tree').split(',')[0] + ) + if missing_view_types: + comodel = self.env[field.comodel_name].sudo(False) + refs = self._get_view_refs(node) + if refs: + comodel = comodel.with_context(**refs) + for view_type in missing_view_types: + subarch, _subview = comodel._get_view(view_type=view_type) + node.append(subarch) for child in node: if child.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'): - node.remove(child) - sub_name_manager = self.with_context( + node_info['children'] = [] + self.with_context( base_model_name=name_manager.model._name, )._postprocess_view( - child, field.comodel_name, editable=node_info['editable'], + child, field.comodel_name, editable=node_info['editable'], parent_name_manager=name_manager, ) - xarch = etree.tostring(child, encoding="unicode").replace('\t', '') - views[child.tag] = { - 'arch': xarch, - 'fields': dict(sub_name_manager.available_fields), - } - attrs['views'] = views if field.type in ('many2one', 'many2many'): comodel = self.env[field.comodel_name].sudo(False) can_create = comodel.check_access_rights('create', raise_exception=False) @@ -1160,19 +1219,11 @@ actual arch. field = name_manager.model._fields.get(name) if not field or not field.comodel_name: return - # move all children nodes into a new node - groupby_node = E.groupby(*node) # post-process the node as a nested view, and associate it to the field - sub_name_manager = self.with_context( + self.with_context( base_model_name=name_manager.model._name, - )._postprocess_view(groupby_node, field.comodel_name, editable=False) - xarch = etree.tostring(groupby_node, encoding="unicode").replace('\t', '') - name_manager.has_field(name, {'views': { - 'groupby': { - 'arch': xarch, - 'fields': dict(sub_name_manager.available_fields), - } - }}) + )._postprocess_view(node, field.comodel_name, editable=False, parent_name_manager=name_manager) + name_manager.has_field(name) def _postprocess_tag_label(self, node, name_manager, node_info): if node.get('for'): @@ -1186,7 +1237,7 @@ actual arch. self.with_context( base_model_name=name_manager.model._name, )._postprocess_view( - searchpanel[0], name_manager.model._name, editable=False, + searchpanel[0], name_manager.model._name, editable=False, parent_name_manager=name_manager ) node_info['children'] = [child for child in node if child.tag != 'searchpanel'] @@ -1839,7 +1890,7 @@ actual arch. return res_id @api.model - def _get_view(self, view_ref): + def _get(self, view_ref): """ Return the view corresponding to ``view_ref``, which may be a view ID or an XML ID. """ @@ -1933,7 +1984,7 @@ actual arch. @api.model def render_public_asset(self, template, values=None): - template_sudo = self._get_view(template).sudo() + template_sudo = self._get(template).sudo() template_sudo._check_view_access() return self.env['ir.qweb'].sudo()._render(template, values) @@ -2129,7 +2180,7 @@ class ResetViewArchWizard(models.TransientModel): class NameManager: """ An object that manages all the named elements in a view. """ - def __init__(self, model): + def __init__(self, model, parent=None): self.model = model self.available_fields = collections.defaultdict(dict) # {field_name: field_info} self.available_actions = set() @@ -2137,6 +2188,10 @@ class NameManager: self.mandatory_fields = dict() # {field_name: use} self.mandatory_parent_fields = dict() # {field_name: use} self.mandatory_names = dict() # {name: use} + self.parent = parent + self.children = [] + if self.parent: + self.parent.children.append(self) @lazy_property def field_info(self): diff --git a/odoo/addons/base/models/res_config.py b/odoo/addons/base/models/res_config.py index f6c05de588d..6e848ce8b4e 100644 --- a/odoo/addons/base/models/res_config.py +++ b/odoo/addons/base/models/res_config.py @@ -384,29 +384,22 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) raise UserError(_("Cannot duplicate configuration!")) @api.model - def fields_view_get(self, view_id=None, view_type='form', - toolbar=False, submenu=False): - ret_val = super(ResConfigSettings, self).fields_view_get( - view_id=view_id, view_type=view_type, - toolbar=toolbar, submenu=submenu) + def _get_view(self, view_id=None, view_type='form', **options): + arch, view = super()._get_view(view_id, view_type, **options) can_install_modules = self.env['ir.module.module'].check_access_rights( 'write', raise_exception=False) - doc = etree.XML(ret_val['arch']) - - for field in ret_val['fields']: - if not field.startswith("module_"): + for node in arch.xpath("//field[@name]"): + if not node.get('name').startswith("module_"): continue - for node in doc.xpath("//field[@name='%s']" % field): - if not can_install_modules: - node.set("readonly", "1") - modifiers = json.loads(node.get("modifiers")) - modifiers['readonly'] = True - node.set("modifiers", json.dumps(modifiers)) + if not can_install_modules: + node.set("readonly", "1") + modifiers = json.loads(node.get("modifiers")) + modifiers['readonly'] = True + node.set("modifiers", json.dumps(modifiers)) - ret_val['arch'] = etree.tostring(doc, encoding='unicode') - return ret_val + return arch, view def onchange_module(self, field_value, module_name): module_sudo = self.env['ir.module.module']._get(module_name[7:]) diff --git a/odoo/addons/base/models/res_currency.py b/odoo/addons/base/models/res_currency.py index 3b4e8766b5b..bd39b6f51b5 100644 --- a/odoo/addons/base/models/res_currency.py +++ b/odoo/addons/base/models/res_currency.py @@ -285,18 +285,16 @@ class Currency(models.Model): """ @api.model - def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - result = super(Currency, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + def _get_view(self, view_id=None, view_type='form', **options): + arch, view = super()._get_view(view_id, view_type, **options) if view_type in ('tree', 'form'): currency_name = (self.env['res.company'].browse(self._context.get('company_id')) or self.env.company).currency_id.name - doc = etree.XML(result['arch']) for field in [['company_rate', _('Unit per %s', currency_name)], ['inverse_company_rate', _('%s per Unit', currency_name)]]: - node = doc.xpath("//tree//field[@name='%s']" % field[0]) + node = arch.xpath("//tree//field[@name='%s']" % field[0]) if node: node[0].set('string', field[1]) - result['arch'] = etree.tostring(doc, encoding='unicode') - return result + return arch, view class CurrencyRate(models.Model): @@ -417,18 +415,16 @@ class CurrencyRate(models.Model): return super()._name_search(parse_date(self.env, name), args, operator, limit, name_get_uid) @api.model - def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - result = super(CurrencyRate, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + def _get_view(self, view_id=None, view_type='form', **options): + arch, view = super()._get_view(view_id, view_type, **options) if view_type in ('tree'): names = { 'company_currency_name': (self.env['res.company'].browse(self._context.get('company_id')) or self.env.company).currency_id.name, 'rate_currency_name': self.env['res.currency'].browse(self._context.get('active_id')).name or 'Unit', } - doc = etree.XML(result['arch']) for field in [['company_rate', _('%(rate_currency_name)s per %(company_currency_name)s', **names)], ['inverse_company_rate', _('%(company_currency_name)s per %(rate_currency_name)s', **names)]]: - node = doc.xpath("//tree//field[@name='%s']" % field[0]) + node = arch.xpath("//tree//field[@name='%s']" % field[0]) if node: node[0].set('string', field[1]) - result['arch'] = etree.tostring(doc, encoding='unicode') - return result + return arch, view diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index bb8df0697fb..54bb10d51ac 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -44,26 +44,22 @@ class FormatAddressMixin(models.AbstractModel): _name = "format.address.mixin" _description = 'Address Format' - def _fields_view_get_address(self, arch): + def _view_get_address(self, arch): # consider the country of the user, not the country of the partner we want to display address_view_id = self.env.company.country_id.address_view_id.sudo() if address_view_id and not self._context.get('no_address_format') and (not address_view_id.model or address_view_id.model == self._name): #render the partner address accordingly to address_view_id - doc = etree.fromstring(arch) - for address_node in doc.xpath("//div[hasclass('o_address_format')]"): + for address_node in arch.xpath("//div[hasclass('o_address_format')]"): Partner = self.env['res.partner'].with_context(no_address_format=True) - sub_view = Partner.fields_view_get( - view_id=address_view_id.id, view_type='form', toolbar=False, submenu=False) - sub_view_node = etree.fromstring(sub_view['arch']) + sub_arch, _sub_view = Partner._get_view(address_view_id.id, 'form') #if the model is different than res.partner, there are chances that the view won't work #(e.g fields not present on the model). In that case we just return arch if self._name != 'res.partner': try: - self.env['ir.ui.view'].postprocess_and_fields(sub_view_node, model=self._name) + self.env['ir.ui.view'].postprocess_and_fields(sub_arch, model=self._name) except ValueError: return arch - address_node.getparent().replace(address_node, sub_view_node) - arch = etree.tostring(doc, encoding='unicode') + address_node.getparent().replace(address_node, sub_arch) return arch class PartnerCategory(models.Model): @@ -345,13 +341,13 @@ class Partner(models.Model): partner.commercial_company_name = p.is_company and p.name or partner.company_name @api.model - def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + def _get_view(self, view_id=None, view_type='form', **options): if (not view_id) and (view_type == 'form') and self._context.get('force_email'): view_id = self.env.ref('base.view_partner_simple_form').id - res = super(Partner, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + arch, view = super()._get_view(view_id, view_type, **options) if view_type == 'form': - res['arch'] = self._fields_view_get_address(res['arch']) - return res + arch = self._view_get_address(arch) + return arch, view @api.constrains('parent_id') def _check_parent_id(self): diff --git a/odoo/addons/base/static/tests/base_settings_tests.js b/odoo/addons/base/static/tests/base_settings_tests.js index b278b54b59b..1ee83575a37 100644 --- a/odoo/addons/base/static/tests/base_settings_tests.js +++ b/odoo/addons/base/static/tests/base_settings_tests.js @@ -206,11 +206,11 @@ QUnit.module('base_settings_tests', { await legacyExtraNextTick(); assert.hasClass($(target).find(".o_form_view"), "o_form_editable"); assert.verifySteps([ - "load_views", // initial setting action + "get_views", // initial setting action "onchange", // this is a setting view => create new record "create", // when we click on action button => save "read", // with save, we have a reload... (not necessary actually) - "load_views", // for other action in breadcrumb, + "get_views", // for other action in breadcrumb, // with a searchread (not shown here since it is a route) "onchange", // when we come back, we want to restart from scratch ]); diff --git a/odoo/addons/base/tests/test_acl.py b/odoo/addons/base/tests/test_acl.py index 0f7f38f1c5e..9d2b8c120eb 100644 --- a/odoo/addons/base/tests/test_acl.py +++ b/odoo/addons/base/tests/test_acl.py @@ -48,7 +48,7 @@ class TestACL(TransactionCaseWithUserDemo): # Verify the test environment first original_fields = currency.fields_get([]) - form_view = currency.fields_view_get(False, 'form') + form_view = currency.get_view(False, 'form') view_arch = etree.fromstring(form_view.get('arch')) has_group_system = self.user_demo.has_group(GROUP_SYSTEM) self.assertFalse(has_group_system, "`demo` user should not belong to the restricted group before the test") @@ -62,7 +62,7 @@ class TestACL(TransactionCaseWithUserDemo): self._set_field_groups(currency, 'decimal_places', GROUP_SYSTEM) fields = currency.fields_get([]) - form_view = currency.fields_view_get(False, 'form') + form_view = currency.get_view(False, 'form') view_arch = etree.fromstring(form_view.get('arch')) self.assertNotIn('decimal_places', fields, "'decimal_places' field should be gone") self.assertEqual(view_arch.xpath("//field[@name='decimal_places']"), [], @@ -74,7 +74,7 @@ class TestACL(TransactionCaseWithUserDemo): self.erp_system_group.users += self.user_demo has_group_system = self.user_demo.has_group(GROUP_SYSTEM) fields = currency.fields_get([]) - form_view = currency.fields_view_get(False, 'form') + form_view = currency.get_view(False, 'form') view_arch = etree.fromstring(form_view.get('arch')) self.assertTrue(has_group_system, "`demo` user should now belong to the restricted group") self.assertIn('decimal_places', fields, "'decimal_places' field must be properly visible again") @@ -130,7 +130,7 @@ class TestACL(TransactionCaseWithUserDemo): """ Test form view Create, Edit, Delete button visibility based on access right of model""" methods = ['create', 'edit', 'delete'] company = self.env['res.company'].with_user(self.user_demo) - company_view = company.fields_view_get(False, 'form') + company_view = company.get_view(False, 'form') view_arch = etree.fromstring(company_view['arch']) for method in methods: self.assertEqual(view_arch.get(method), 'false') @@ -140,7 +140,7 @@ class TestACL(TransactionCaseWithUserDemo): self.erp_system_group.users += self.user_demo methods = ['create', 'edit', 'delete'] company = self.env['res.company'].with_user(self.user_demo) - company_view = company.fields_view_get(False, 'form') + company_view = company.get_view(False, 'form') view_arch = etree.fromstring(company_view['arch']) for method in methods: self.assertIsNone(view_arch.get(method)) @@ -149,7 +149,7 @@ class TestACL(TransactionCaseWithUserDemo): """ Test many2one field Create and Edit option visibility based on access rights of relation field""" methods = ['create', 'write'] company = self.env['res.company'].with_user(self.user_demo) - company_view = company.fields_view_get(False, 'form') + company_view = company.get_view(False, 'form') view_arch = etree.fromstring(company_view['arch']) field_node = view_arch.xpath("//field[@name='currency_id']") self.assertTrue(len(field_node), "currency_id field should be in company from view") @@ -161,7 +161,7 @@ class TestACL(TransactionCaseWithUserDemo): self.erp_system_group.users += self.user_demo methods = ['create', 'write'] company = self.env['res.company'].with_user(self.user_demo) - company_view = company.fields_view_get(False, 'form') + company_view = company.get_view(False, 'form') view_arch = etree.fromstring(company_view['arch']) field_node = view_arch.xpath("//field[@name='currency_id']") self.assertTrue(len(field_node), "currency_id field should be in company from view") diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py index a8586eeaaa1..2f50c1faac6 100644 --- a/odoo/addons/base/tests/test_translate.py +++ b/odoo/addons/base/tests/test_translate.py @@ -638,8 +638,8 @@ class TestTranslationWrite(TransactionCase): self.assertEqual(fg['state']['selection'], [('manual', 'Custo'), ('base', 'Pas touche!')]) - def test_fields_view_get(self): - """ Test translations of field descriptions in fields_view_get(). """ + def test_load_views(self): + """ Test translations of field descriptions in get_view(). """ self.env['res.lang']._activate_lang('fr_FR') # add translation for the string of field ir.model.name @@ -659,9 +659,9 @@ class TestTranslationWrite(TransactionCase): info = model.fields_get(['name']) self.assertEqual(info['name']['string'], LABEL) - # check that fields_view_get() also returns the expected label - info = model.fields_view_get()['fields'] - self.assertEqual(info['name']['string'], LABEL) + # check that get_views() also returns the expected label + info = model.get_views([(False, 'form')]) + self.assertEqual(info['models'][model._name]['name']['string'], LABEL) class TestXMLTranslation(TransactionCase): diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index b0d6f2ce91a..74bd758eafa 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -1256,9 +1256,7 @@ class TestViews(ViewCase): """ }) - view = self.View.with_context(check_view_ids=[view2.id, view3.id]) \ - .fields_view_get(view2.id, view_type='form') - self.assertEqual(view['type'], 'form') + view = self.View.with_context(check_view_ids=[view2.id, view3.id]).get_view(view2.id, 'form') self.assertEqual( etree.fromstring( view['arch'], @@ -1285,8 +1283,7 @@ class TestViews(ViewCase): 'inherit_id': view1.id, 'arch': '
a

b

c

', }) - view = self.View.with_context(check_view_ids=view2.ids).fields_view_get(view1.id) - self.assertEqual(view['type'], 'form') + view = self.View.with_context(check_view_ids=view2.ids).get_view(view1.id) self.assertEqual( view['arch'], '(
a

b

c

)', @@ -1305,8 +1302,7 @@ class TestViews(ViewCase): 'inherit_id': view1.id, 'arch': '
a

b

c

', }) - view = self.View.with_context(check_view_ids=view2.ids).fields_view_get(view1.id) - self.assertEqual(view['type'], 'form') + view = self.View.with_context(check_view_ids=view2.ids).get_view(view1.id) self.assertEqual( view['arch'], '
(
a

b

c)

', @@ -1325,8 +1321,7 @@ class TestViews(ViewCase): 'inherit_id': view1.id, 'arch': '
a

b

c

', }) - view = self.View.with_context(check_view_ids=view2.ids).fields_view_get(view1.id) - self.assertEqual(view['type'], 'form') + view = self.View.with_context(check_view_ids=view2.ids).get_view(view1.id) self.assertEqual( view['arch'], '
(a

b

c

)', @@ -1379,8 +1374,7 @@ class TestViews(ViewCase): }) view = self.View.with_context(check_view_ids=[view2.id, view3.id]) \ - .fields_view_get(view2.id, view_type='form') - self.assertEqual(view['type'], 'form') + .get_view(view2.id, view_type='form') self.assertEqual( etree.fromstring( view['arch'], @@ -2330,13 +2324,13 @@ class TestViews(ViewCase): }) # default view, no address_view defined - arch = self.env['res.partner'].fields_view_get(view_id=partner_view.id)['arch'] + arch = self.env['res.partner'].get_view(partner_view.id)['arch'] self.assertIn('"street"', arch) self.assertNotIn('"parent_name"', arch) # custom view, address_view defined self.env.company.country_id.address_view_id = address_view - arch = self.env['res.partner'].fields_view_get(view_id=partner_view.id)['arch'] + arch = self.env['res.partner'].get_view(partner_view.id)['arch'] self.assertNotIn('"street"', arch) self.assertIn('"parent_name"', arch) # weird result:
inside a @@ -3110,12 +3104,12 @@ class TestAccessRights(common.TransactionCase): with self.assertRaises(AccessError): self.env['ir.ui.view'].search([("model", '=', "res.partner"), ('type', '=', 'form')]) - # but can call fields_view_get - self.env['res.partner'].fields_view_get(view_type='form') + # but can call view_get + self.env['res.partner'].get_view(view_type='form') # unless he does not have access to the model with self.assertRaises(AccessError): - self.env['ir.ui.view'].fields_view_get(view_type='form') + self.env['ir.ui.view'].get_view(view_type='form') @common.tagged('post_install', '-at_install', '-standard', 'migration') class TestAllViews(common.TransactionCase): @@ -3142,7 +3136,7 @@ class TestRenderAllViews(common.TransactionCase): for _ in range(5): model.invalidate_cache() before = time.perf_counter() - model.fields_view_get() + model.get_view() times.append(time.perf_counter() - before) count += 1 elapsed += min(times) diff --git a/odoo/addons/base/wizard/base_module_upgrade.py b/odoo/addons/base/wizard/base_module_upgrade.py index 5f0552bcdfd..cc8a93c5424 100644 --- a/odoo/addons/base/wizard/base_module_upgrade.py +++ b/odoo/addons/base/wizard/base_module_upgrade.py @@ -23,8 +23,8 @@ class BaseModuleUpgrade(models.TransientModel): module_info = fields.Text('Apps to Update', readonly=True, default=_default_module_info) @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - res = super(BaseModuleUpgrade, self).fields_view_get(view_id, view_type, toolbar=toolbar,submenu=False) + def get_view(self, view_id=None, view_type='form', **options): + res = super().get_view(view_id, view_type, **options) if view_type != 'form': return res diff --git a/odoo/addons/test_action_bindings/tests/test_bindings.py b/odoo/addons/test_action_bindings/tests/test_bindings.py index 21be64c27bf..cb9ba4561ed 100644 --- a/odoo/addons/test_action_bindings/tests/test_bindings.py +++ b/odoo/addons/test_action_bindings/tests/test_bindings.py @@ -53,19 +53,19 @@ class TestBindingViewFilters(common.TransactionCase): def test_act_window(self): A = self.env['tab.a'] - form_act = A.fields_view_get(toolbar=True)['toolbar']['action'] + form_act = A.get_view(toolbar=True)['toolbar']['action'] self.assertEqual( [a['name'] for a in form_act], ['Action 1', 'Action 2', 'Action 3'], "forms should have all actions") - list_act = A.fields_view_get(view_type='tree', toolbar=True)['toolbar']['action'] + list_act = A.get_view(view_type='tree', toolbar=True)['toolbar']['action'] self.assertEqual( [a['name'] for a in list_act], ['Action 1', 'Action 3'], "lists should not have the form-only action") - kanban_act = A.fields_view_get(view_type='kanban', toolbar=True)['toolbar']['action'] + kanban_act = A.get_view(view_type='kanban', toolbar=True)['toolbar']['action'] self.assertEqual( [a['name'] for a in kanban_act], ['Action 1'], @@ -74,19 +74,19 @@ class TestBindingViewFilters(common.TransactionCase): def test_act_record(self): B = self.env['tab.b'] - form_act = B.fields_view_get(toolbar=True)['toolbar']['action'] + form_act = B.get_view(toolbar=True)['toolbar']['action'] self.assertEqual( [a['name'] for a in form_act], ['Record 1', 'Record 2', 'Record 3'], "forms should have all actions") - list_act = B.fields_view_get(view_type='tree', toolbar=True)['toolbar']['action'] + list_act = B.get_view(view_type='tree', toolbar=True)['toolbar']['action'] self.assertEqual( [a['name'] for a in list_act], ['Record 1', 'Record 3'], "lists should not have the form-only action") - kanban_act = B.fields_view_get(view_type='kanban', toolbar=True)['toolbar']['action'] + kanban_act = B.get_view(view_type='kanban', toolbar=True)['toolbar']['action'] self.assertEqual( [a['name'] for a in kanban_act], ['Record 1'], diff --git a/odoo/addons/test_new_api/tests/test_onchange.py b/odoo/addons/test_new_api/tests/test_onchange.py index 472ff6de6d0..51939ddb15e 100644 --- a/odoo/addons/test_new_api/tests/test_onchange.py +++ b/odoo/addons/test_new_api/tests/test_onchange.py @@ -424,9 +424,7 @@ class TestOnChange(SavepointCaseWithUserDemo): # mimic UI behaviour, so we get subfields # (we need at least subfield: 'important_emails.important') - view_info = self.Discussion.fields_view_get( - view_id=self.env.ref('test_new_api.discussion_form').id, - view_type='form') + view_info = self.Discussion.get_view(self.env.ref('test_new_api.discussion_form').id, 'form') field_onchange = self.Discussion._onchange_spec(view_info=view_info) self.assertEqual(field_onchange.get('messages'), '1') diff --git a/odoo/addons/test_new_api/views/test_new_api_views.xml b/odoo/addons/test_new_api/views/test_new_api_views.xml index 56e6a773b28..36ee073f0c3 100644 --- a/odoo/addons/test_new_api/views/test_new_api_views.xml +++ b/odoo/addons/test_new_api/views/test_new_api_views.xml @@ -93,7 +93,7 @@ - + diff --git a/odoo/addons/test_testing_utilities/tests/test_form_impl.py b/odoo/addons/test_testing_utilities/tests/test_form_impl.py index 6251cb1943e..d440e6ee40b 100644 --- a/odoo/addons/test_testing_utilities/tests/test_form_impl.py +++ b/odoo/addons/test_testing_utilities/tests/test_form_impl.py @@ -6,6 +6,7 @@ complete) intended for properly validating business "view" flows (onchanges, readonly, required, ...) and make it easier to generate sensible & coherent business objects. """ +from lxml import etree from operator import itemgetter from odoo.tests.common import TransactionCase, Form @@ -324,19 +325,18 @@ class TestO2M(TransactionCase): delegating to a separate form view """ f = Form(self.env['test_testing_utilities.parent'], view='test_testing_utilities.o2m_parent_ed') - custom_tree = self.env.ref('test_testing_utilities.editable_external').id + custom_tree = self.env.ref('test_testing_utilities.editable_external') - subs_field = f._view['fields']['subs'] - tree_view = subs_field['views']['tree'] - self.assertEqual(tree_view['type'], 'tree') self.assertEqual( - tree_view['view_id'], custom_tree, + [el.get('name') for el in f._view['tree'].xpath('//field[@name="subs"]/tree//field')], + [el.get('name') for el in etree.fromstring(custom_tree['arch']).xpath('//field')], 'check that the tree view is the one referenced by tree_view_ref' ) - self.assertIs(subs_field['views']['edition'], tree_view, "check that the edition view is the tree view") + subs_field = f._view['fields']['subs'] + self.assertIs(subs_field['edition_view']['tree'], f._view['tree'].xpath('//field[@name="subs"]/tree')[0], "check that the edition view is the tree view") self.assertEqual( - subs_field['views']['edition']['view_id'], - custom_tree + [el.get('name') for el in subs_field['edition_view']['tree'].xpath('.//field')], + [el.get('name') for el in etree.fromstring(custom_tree['arch']).xpath('//field')], ) with f.subs.new() as s: diff --git a/odoo/models.py b/odoo/models.py index 7f470c29dbc..ab16f1c40f7 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -1428,7 +1428,7 @@ class BaseModel(metaclass=MetaModel): @api.model def fields_get_keys(self): warnings.warn( - 'fields_get_keys() method is deprecated, use `_fields` or `fields_view_get` instead', + 'fields_get_keys() method is deprecated, use `_fields` or `get_views` instead', DeprecationWarning ) return list(self._fields) @@ -1606,7 +1606,7 @@ class BaseModel(metaclass=MetaModel): return view @api.model - def load_views(self, views, options=None): + def get_views(self, views, options=None): """ Returns the fields_views of given views, along with the fields of the current model, and optionally its filters for the given action. @@ -1626,27 +1626,40 @@ class BaseModel(metaclass=MetaModel): options = options or {} result = {} - toolbar = options.get('toolbar') - result['fields_views'] = { - v_type: self.fields_view_get(v_id, v_type if v_type != 'list' else 'tree', - toolbar=toolbar if v_type != 'search' else False) + result['views'] = { + v_type: self.get_view( + v_id, v_type if v_type != 'list' else 'tree', + **options + ) for [v_id, v_type] in views } - result['fields'] = self.fields_get() - - if options.get('load_filters'): - result['filters'] = self.env['ir.filters'].get_filters(self._name, options.get('action_id')) + models = set(model for info in result['views'].values() for model in info.pop('models')) + result['models'] = {model: self.env[model].fields_get() for model in models} return result @api.model - def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + def _get_view(self, view_id=None, view_type='form', **options): + """_get_view([view_id | view_type='form']) + + Get the model view combined architecture (the view along all its inheriting views). + + :param int view_id: id of the view or None + :param str view_type: type of the view to return if view_id is None ('form', 'tree', ...) + :param dict options: bool options to return additional features: + - bool load_filters: returns the model's filters (for search views) + - bool mobile: true if the web client is currently using the responsive mobile view + (to use kanban views instead of list views for x2many fields) + - bool toolbar: true to include contextual actions + :return: architecture of the view as an etree node, and the browse record of the view used + :rtype: tuple + :raise AttributeError: + + * if no view exists for that model, and no method `_get_default_[view_type]_view` exists for the view type + + """ View = self.env['ir.ui.view'].sudo() - result = { - 'model': self._name, - 'field_parent': False, - } # try to find a view_id if none provided if not view_id: @@ -1654,6 +1667,11 @@ class BaseModel(metaclass=MetaModel): view_ref_key = view_type + '_view_ref' view_ref = self._context.get(view_ref_key) if view_ref: + # Do not propagate _view_ref + # so these context keys are not used when fetching the subviews of one2many/many2many fields + context = dict(self._context) + context.pop(view_ref_key) + View = View.with_context(context) if '.' in view_ref: module, view_ref = view_ref.split('.', 1) query = "SELECT res_id FROM ir_model_data WHERE model='ir.ui.view' AND module=%s AND name=%s" @@ -1673,33 +1691,29 @@ class BaseModel(metaclass=MetaModel): if view_id: # read the view with inherited views applied view = View.browse(view_id) - result['arch'] = view.get_combined_arch() - result['name'] = view.name - result['type'] = view.type - result['view_id'] = view.id - result['field_parent'] = view.field_parent - result['base_model'] = view.model + arch = view._get_combined_arch() else: # fallback on default views methods if no ir.ui.view could be found + view = View.browse() try: - arch_etree = getattr(self, '_get_default_%s_view' % view_type)() - result['arch'] = etree.tostring(arch_etree, encoding='unicode') - result['type'] = view_type - result['name'] = 'default' + arch = getattr(self, '_get_default_%s_view' % view_type)() except AttributeError: raise UserError(_("No default view of type '%s' could be found !", view_type)) - return result + return arch, view @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - """ fields_view_get([view_id | view_type='form']) + def get_view(self, view_id=None, view_type='form', **options): + """ get_view([view_id | view_type='form']) - Get the detailed composition of the requested view like fields, model, view architecture + Get the detailed composition of the requested view like model, view architecture :param int view_id: id of the view or None :param str view_type: type of the view to return if view_id is None ('form', 'tree', ...) - :param bool toolbar: true to include contextual actions - :param submenu: deprecated + :param dict options: bool options to return additional features: + - bool load_filters: returns the model's filters (for search views) + - bool mobile: true if the web client is currently using the responsive mobile view + (to use kanban views instead of list views for x2many fields) + - bool toolbar: true to include contextual actions :return: composition of the requested view (including inherited views and extensions) :rtype: dict :raise AttributeError: @@ -1710,22 +1724,28 @@ class BaseModel(metaclass=MetaModel): :raise Invalid ArchitectureError: if there is view type other than form, tree, calendar, search etc... defined on the structure """ self.check_access_rights('read') - view = self.env['ir.ui.view'].sudo().browse(view_id) # Get the view arch and all other attributes describing the composition of the view - result = self._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + arch, view = self._get_view(view_id, view_type, **options) # Override context for postprocessing - if view_id and result.get('base_model', self._name) != self._name: - view = view.with_context(base_model_name=result['base_model']) + if view and (view.model or self._name) != self._name: + view = view.with_context(base_model_name=view.model) # Apply post processing, groups and modifiers etc... - xarch, xfields = view.postprocess_and_fields(etree.fromstring(result['arch']), model=self._name) - result['arch'] = xarch - result['fields'] = xfields + arch, models = view.postprocess_and_fields(arch, model=self._name, **options) + result = { + 'arch': arch, + # TODO: only `web_studio` seems to require this. I guess this is acceptable to keep it. + 'id': view.id, + # TODO: only `web_studio` seems to require this. But this one on the other hand should be eliminated: + # you just called `get_views` for that model, so obviously the web client already knows the model. + 'model': self._name, + 'models': models, + } # Add related action information if asked - if toolbar: + if options.get('toolbar') and view_type != 'search': vt = 'list' if view_type == 'tree' else view_type bindings = self.env['ir.actions.actions'].get_bindings(self._name) resreport = [action @@ -1739,6 +1759,56 @@ class BaseModel(metaclass=MetaModel): 'print': resreport, 'action': resaction, } + + if options.get('load_filters') and view_type == 'search': + result['filters'] = self.env['ir.filters'].get_filters(self._name, options.get('action_id')) + + return result + + @api.model + def load_views(self, views, options=None): + warnings.warn('`load_views` method is deprecated, use `get_views` instead', DeprecationWarning) + return self.get_views(views, options=options) + + @api.model + def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + warnings.warn('Method `_fields_view_get` is deprecated, use `_get_view` instead', DeprecationWarning) + arch, view = self._get_view(view_id, view_type, toolbar=toolbar, submenu=submenu) + result = { + 'arch': etree.tostring(arch, encoding='unicode'), + 'model': self._name, + 'field_parent': False, + } + if view: + result['name'] = view.name + result['type'] = view.type + result['view_id'] = view.id + result['field_parent'] = view.field_parent + result['base_model'] = view.model + else: + result['type'] = view_type + result['name'] = 'default' + return result + + @api.model + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + warnings.warn('Method `fields_view_get` is deprecated, use `get_view` instead', DeprecationWarning) + result = self.get_view(view_id, view_type, toolbar=toolbar, submenu=submenu) + node = etree.fromstring(result['arch']) + view_fields = set(el.get('name') for el in node.xpath('.//field[not(ancestor::field)]')) + result['fields'] = self.fields_get(view_fields) + result.pop('models', None) + if 'id' in result: + view = self.env['ir.ui.view'].sudo(result.pop('id')) + result['name'] = view.name + result['type'] = view.type + result['view_id'] = view.id + result['field_parent'] = view.field_parent + result['base_model'] = view.model + else: + result['type'] = view_type + result['name'] = 'default' + result['field_parent'] = False return result def get_formview_id(self, access_uid=None): @@ -6223,7 +6293,7 @@ Fields: @api.model def _onchange_spec(self, view_info=None): """ Return the onchange spec from a view description; if not given, the - result of ``self.fields_view_get()`` is used. + result of ``self.get_view()`` is used. """ result = {} @@ -6235,14 +6305,14 @@ Fields: if not result.get(names): result[names] = node.attrib.get('on_change') # traverse the subviews included in relational fields - for subinfo in info['fields'][name].get('views', {}).values(): - process(etree.fromstring(subinfo['arch']), subinfo, names) + for child_view in node.xpath("./*[descendant::field]"): + process(child_view, None, names) else: for child in node: process(child, info, prefix) if view_info is None: - view_info = self.fields_view_get() + view_info = self.get_view() process(etree.fromstring(view_info['arch']), view_info, '') return result diff --git a/odoo/tests/common.py b/odoo/tests/common.py index 83ee1479e05..f2bb653cf1e 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -1766,24 +1766,7 @@ def can_import(module): else: return True -# TODO: sub-views (o2m, m2m) -> sub-form? -# TODO: domains -ref_re = re.compile(r""" -# first match 'form_view_ref' key, backrefs are used to handle single or -# double quoting of the value -(['"])(?P\w+_view_ref)\1 -# colon separator (with optional spaces around) -\s*:\s* -# open quote for value -(['"]) -(?P - # we'll just match stuff which is normally part of an xid: - # word and "." characters - [.\w]+ -) -# close with same quote as opening -\3 -""", re.VERBOSE) + class Form(object): """ Server-side form view implementation (partial) @@ -1871,8 +1854,9 @@ class Form(object): view_id = env.ref(view).id else: view_id = view or False - fvg = recordp.fields_view_get(view_id, 'form') + fvg = recordp.get_view(view_id, 'form') fvg['tree'] = etree.fromstring(fvg['arch']) + fvg['fields'] = self._get_view_fields(fvg['tree'], recordp) object.__setattr__(self, '_view', fvg) @@ -1891,37 +1875,40 @@ class Form(object): else: self._init_from_defaults(self._model) + def _get_view_fields(self, node, model): + level = node.xpath('count(ancestor::field)') + fnames = set(el.get('name') for el in node.xpath('.//field[count(ancestor::field) = %s]' % level)) + fields = {fname: info for fname, info in model.fields_get().items() if fname in fnames} + return fields + def _o2m_set_edition_view(self, descr, node, level): default_view = next( (m for m in node.get('mode', 'tree').split(',') if m != 'form'), 'tree' ) - refs = { - m.group('view_type'): m.group('view_id') - for m in ref_re.finditer(node.get('context', '')) - } + refs = self._env['ir.ui.view']._get_view_refs(node) # always fetch for simplicity, ensure we always have a tree and # a form view submodel = self._env[descr['relation']] - views = submodel.with_context(**refs) \ - .load_views([(False, 'tree'), (False, 'form')])['fields_views'] - # embedded views should take the priority on externals - views.update(descr['views']) - # re-set all resolved views on the descriptor - descr['views'] = views + views = {view.tag: view for view in node.xpath('./*[descendant::field]')} + for view_type in ['tree', 'form']: + # embedded views should take the priority on externals + if view_type not in views: + sub_fvg = submodel.with_context(**refs).get_view(view_type=view_type) + sub_node = etree.fromstring(sub_fvg['arch']) + views[view_type] = sub_node + node.append(sub_node) # if the default view is a kanban or a non-editable list, the # "edition controller" is the form view - edition = views['form'] - edition['tree'] = etree.fromstring(edition['arch']) - if default_view == 'tree': - subarch = etree.fromstring(views['tree']['arch']) - if subarch.get('editable'): - edition = views['tree'] - edition['tree'] = subarch + edition_view = 'tree' if default_view == 'tree' and views['tree'].get('editable') else 'form' + edition = { + 'fields': self._get_view_fields(views[edition_view], submodel), + 'tree': views[edition_view], + } # don't recursively process o2ms in o2ms self._process_fvg(submodel, edition, level=level-1) - descr['views']['edition'] = edition + descr['edition_view'] = edition def __str__(self): return "<%s %s(%s)>" % ( @@ -1931,8 +1918,7 @@ class Form(object): ) def _process_fvg(self, model, fvg, level=2): - """ Post-processes to augment the fields_view_get with: - + """ Post-processes to augment the view_get with: * an id field (may not be present if not in the view but needed) * pre-processed modifiers (map of modifier name to json-loaded domain) * pre-processed onchanges list @@ -1942,7 +1928,7 @@ class Form(object): modifiers = fvg['modifiers'] = {'id': {'required': False, 'readonly': True}} contexts = fvg['contexts'] = {} order = fvg['fields_ordered'] = [] - for f in fvg['tree'].xpath('//field[not(ancestor::field)]'): + for f in fvg['tree'].xpath('.//field[count(ancestor::field) = %s]' % fvg['tree'].xpath('count(ancestor::field)')): fname = f.get('name') order.append(fname) @@ -1962,7 +1948,7 @@ class Form(object): if level and descr['type'] == 'one2many': self._o2m_set_edition_view(descr, f, level) - fvg['onchange'] = model._onchange_spec(fvg) + fvg['onchange'] = model._onchange_spec({'arch': etree.tostring(fvg['tree'])}) def _init_from_defaults(self, model): vals = self._values @@ -2207,7 +2193,7 @@ class Form(object): continue if descr['type'] == 'one2many': - subview = descr['views']['edition'] + subview = descr['edition_view'] fields_ = subview['fields'] oldvals = v v = [] @@ -2293,7 +2279,7 @@ class Form(object): values = {} for k, v in record.items(): if fields[k]['type'] == 'one2many': - subfields = fields[k]['views']['edition']['fields'] + subfields = fields[k]['edition_view']['fields'] it = values[k] = [] for (c, rid, vs) in v: if c == 1 and isinstance(vs, UpdateDict): @@ -2317,7 +2303,7 @@ class Form(object): return value[0] elif descr['type'] == 'one2many': # ignore o2ms nested in o2ms - if not descr['views']: + if not descr['edition_view']: return [] if current is None: @@ -2326,7 +2312,7 @@ class Form(object): c = {t[1] for t in current if t[0] in (1, 2)} current_values = {c[1]: c[2] for c in current if c[0] == 1} # which view should this be??? - subfields = descr['views']['edition']['fields'] + subfields = descr['edition_view']['fields'] # TODO: simplistic, unlikely to work if e.g. there's a 5 inbetween other commands for command in value: if command[0] == 0: @@ -2402,7 +2388,7 @@ class O2MForm(Form): object.__setattr__(self, '_model', m) # copy so we don't risk breaking it too much (?) - fvg = dict(proxy._descr['views']['edition']) + fvg = dict(proxy._descr['edition_view']) object.__setattr__(self, '_view', fvg) self._process_fvg(m, fvg) @@ -2503,7 +2489,7 @@ class O2MProxy(X2MProxy): # reify records to a list so they can be manipulated easily? self._records = [] model = self._model - fields = self._descr['views']['edition']['fields'] + fields = self._descr['edition_view']['fields'] for (command, rid, values) in self._parent._values[self._field]: if command == 0: self._records.append(values) diff --git a/odoo/tools/test_reports.py b/odoo/tools/test_reports.py index 06ceccea33e..0938e666744 100644 --- a/odoo/tools/test_reports.py +++ b/odoo/tools/test_reports.py @@ -10,6 +10,7 @@ import logging import os import tempfile +from lxml import etree from subprocess import Popen, PIPE from .. import api @@ -170,21 +171,24 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None, log_test("will emulate a %s view: %s#%s", view_type, datas['res_model'], view_id or '?') - view_res = env[datas['res_model']].fields_view_get(view_id, view_type=view_type) + model = env[datas['res_model']] + view_res = model.get_view(view_id, view_type) assert view_res and view_res.get('arch'), "Did not return any arch for the view" view_data = {} - if view_res.get('fields'): - view_data = env[datas['res_model']].default_get(list(view_res['fields'])) + arch = etree.fromstring(view_res['arch']) + fields = [el.get('name') for el in arch.xpath('//field[not(ancestor::field)]')] + if fields: + view_data = model.default_get(fields) if datas.get('form'): view_data.update(datas.get('form')) if wiz_data: view_data.update(wiz_data) _logger.debug("View data is: %r", view_data) - for fk, field in view_res.get('fields',{}).items(): + for fk in fields: # Default fields returns list of int, while at create() # we need to send a [(6,0,[int,..])] - if field['type'] in ('one2many', 'many2many') \ + if model._fields[fk].type in ('one2many', 'many2many') \ and view_data.get(fk, False) \ and isinstance(view_data[fk], list) \ and not isinstance(view_data[fk][0], tuple) :