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/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/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/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/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/legacy/js/services/data_manager.js b/addons/web/static/src/legacy/js/services/data_manager.js index 0efcc2ecf2d..2a8fc5b425b 100644 --- a/addons/web/static/src/legacy/js/services/data_manager.js +++ b/addons/web/static/src/legacy/js/services/data_manager.js @@ -89,6 +89,9 @@ 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 }, diff --git a/addons/web/static/src/views/view_service.js b/addons/web/static/src/views/view_service.js index cf8a0668551..423bc8b317c 100644 --- a/addons/web/static/src/views/view_service.js +++ b/addons/web/static/src/views/view_service.js @@ -1,6 +1,7 @@ /** @odoo-module **/ import { registry } from "@web/core/registry"; +import { device } from 'web.config'; /** * @typedef {Object} IrFilter @@ -65,14 +66,18 @@ export const viewService = { async function loadViews(params, options) { const key = JSON.stringify([params.resModel, params.views, params.context, options]); if (!cache[key]) { + var load_views_options = { + action_id: options.actionId || false, + load_filters: options.loadIrFilters || false, + toolbar: options.loadActionMenus || false, + } + if (device.isMobile){ + load_views_options.mobile = device.isMobile; + } 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, - }, + options: load_views_options, context: params.context, }) .then((result) => { 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/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) :