Skip to content

Commit

Permalink
[REF] models: refactor fields_view_get, load_views
Browse files Browse the repository at this point in the history
Refactor the `load_views` API so it no longer sends multiple times the same
fields description.

e.g.
When `load_views` is called to get the kanban, tree and form views,
the list of fields of the model was sent 4 times:
- Once for each view, with only the fields used in the view,
  in `['fields_views']['kanban']['fields']` for instance
- Once globally, with all the fields of the model, in `['fields']`

The goal of this revision is to change that so it sends the list of all fields
only once.

In addition, if a view contains x2many fields,
the fields description of the comodel is also sent.
It was sent in the `views` key of the view fields dict.
e.g.
When calling `load_views` of `res.partner` to get the kanban,
tree and form views,
the `res.partner` fields description was actually sent 6 times:
- Once for each view
- Once globally
- Once for each view of the many2many field `child_ids` of the form view, in
  - `['fields_views']['form']['fields']['child_ids']['views']['kanban']['fields']`
  - `['fields_views']['form']['fields']['child_ids']['views']['form']['fields']`

The change suggested in this revision is to:
- Remove the fields description for each view in `['fields_views']`.
  As it no longer contains the fields,
  the key becomes `['views']` instead of `['fields_views']`.
- Replace the dict key `['fields']` by `['models']`,
  which is a dict with as key the model name and as values
  the model fields description. It contains the fields description
  for all models implied in the view:
  the model of the main view and the model of all one2many and many2many fields.

With this change, the fields description will only be sent once by model
implied in the view.

In addition, the web client was getting the information about the fields
sometimes in the global fields description list (e.g. `['fields']`),
sometimes in the fields description list of the view type
(e.g. `['fields_views']['form']['fields']`),
making it a pain to try to make changes / performance gain
in these field description dictionaries, because you never knew in which dict
the web client was getting its info.
Now, as there is only one place to get the fields description from,
it's clearer and cleaner.

- one2many and many2many fields views are passed directly in the main view
  architecture rather than being put in the `views` key
  of the field description.
  This is actually easier to treat by the web client,
  and this will allow in a future work to cache an entire view in one block
  of text rather than having to combine multiple cached blocks of text
  to return one view.
- one2many and many2many fields which do not have directly embedded views
  have their views directly injected in the architecture,
  so the web client doesn't have to do RPC calls to `load_views`
  for each one2many and many2many fields not having embedded views.
  For instance, this allow to reduce the number of RPC calls to `load_views`
  from 8 to 1 when loading the form of `product.product`.
  Currently, this behavior is limited to 1 level deep but we consider making it
  go all the way down in future works. We did not do it for the moment because
  in certain cases it rises the processing time and the size (bytes) too much.
  e.g. the sale.order view can be 5 levels deep,
  meaning you can reach 4 dialogs on top the main view.
  ```
  sale.order form > order_line > sale.order.line form > invoice_lines >
  account.move.line form > asset_ids > account.asset form >
  depreciation_move_ids > account.move form.
  ```
  This will also benefit in future works to cache an entire view in one block
  of text rather to having to combine multiple cached block of text
  to get one view.
- `fields_view_get` becomes `get_view`.
  As it no longer returns the fields description,
  keeping the `fields` in the name `fields_view_get` no longer makes sense.
  Hence removing `fields` from the method name, it becomes `view_get`.
  As it gets renamed anyway, we take the opportunity to rename it `get_view`,
  which is more in line with the general getter/setter guidelines
  in the model object world.
- `_fields_view_get` becomes `_get_view`. For the same reasons than above.
- `load_views` becomes `get_views`.
  This is not mandatory, there is no technical reason to rename `load_views` as
  it practically sends the same info as before,
  the view architectures and their fields description. Just in another way.
  We just take the opportunity of this pull request to suggest a cleaner API:
  `_get_view`, `get_view` and `get_views`.
- Arguments `toolbar=False, submenu=False` fo the methods
  `_fields_view_get` and `fields_view_get` are converted to a kwargs `**options`
  in `_get_view` and `get_view`.
  The rationale is that submenu was already no longer used (deprecated)
  and the mobile options is introduced.
  The mobile options is necessary to tell the server to send the mobile views
  for x2many fields (kanban instead of tree).
  Instead of adding a new argument each time we add a new option to
  `fields_view_get`, it seems wiser to have a kwargs `**options` to avoid
  to re-write all overrides each time a new option is introduced.
- `_fields_view_get` returned a dict containing the arch in text and some of the
  view information. Now, `get_view` returns a tuple with the view architecture
  as an `etree` node, and the view as a browse record. The rationale is that all
  overrides of `_fields_view_get` were about modifying the arch only
  (e.g. changing the address format/re-organizing the address related field
  nodes of the partner according to the company country).
  To do so, all these overrides were doing `etree.fromstring` to parse the arch
  which was sent in text to convert it to an `etree`,
  then operations were done on the `etree`,
  and then `etree.tostring` was called to convert back the arch to string.
  With this change of signature to send the arch as an `etree`,
  all these back and forth `etree.fromstring` -> `etree.tostring` are avoided,
  allowing some performance gain and less code in the end.
- A cleanup of the keys returned in the dict of `fields_view_get`
  has been performed in `get_view`:
  - `fields` is removed, as explained above,
  - `view_id` is renamed `id`,
  - `name` is removed, it was unused by the web client,
  - `type` is removed, it was unused by the web client,
  - `field_parent` is removed, it was unused by the web client,
  - `base_model` is removed, it was unused by the web client.
- `filters` is moved from the global dict returned by `load_views`
  (now `get_views`) to the dict returned by `fields_view_get` (now `get_view`)
  as it applies only to the `search` view type.
- Retro-compatible methods for the 3 methods
  `fields_view_get`, `_fields_view_get` and `load_views` are provided,
  with deprecation warnings in them.

- The web client could cache the model fields description
  (as it already caches the views),
  so it doesn't need to fetch them again if it asks for another view of a model
  for which he already has the fields description.
  If we do so, `get_views` could return only the list of models used by
  the views, without the fields description as of now,
  and the web client would then call `fields_get` independently only for
  the models for which it doesn't have yet the fields description.
  This would avoid the server to return the fields description
  and to call `fields_get`, which is costly, for each `get_views`,
  therefore gaining performances.
- Inject the views of the one2many and many2many fields all the way down,
  unlimited depth level, as explained above.
- Cache with `ormcache` the architecture of back-end views.
  This is already done for qweb views, it's not done for back-end views.
  Therefore the postprocessing of the views is performed for each `get_views`,
  which is costly, while the view architecture doesn't change for users
  belonging to the same groups, according to the groups implied by the view.

This pull request is co-authored by
Aaron Bohy (aab) for the web client part and
Denis Ledoux (dle) for the server part.

Part-of: #87522
  • Loading branch information
beledouxdenis committed Apr 29, 2022
1 parent 89e3d95 commit b03c227
Show file tree
Hide file tree
Showing 39 changed files with 438 additions and 333 deletions.
9 changes: 4 additions & 5 deletions addons/board/controllers/main.py
Expand Up @@ -16,10 +16,9 @@ def add_to_dashboard(self, action_id, context_to_save, domain, view_mode, name='
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),
Expand All @@ -29,7 +28,7 @@ def add_to_dashboard(self, action_id, context_to_save, domain, view_mode, name='
'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,
Expand Down
4 changes: 2 additions & 2 deletions addons/board/models/board.py
Expand Up @@ -20,13 +20,13 @@ def create(self, vals_list):
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:
Expand Down
8 changes: 4 additions & 4 deletions addons/crm/models/crm_lead.py
Expand Up @@ -862,16 +862,16 @@ def copy(self, default=None):
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):
Expand Down
4 changes: 2 additions & 2 deletions addons/google_spreadsheet/models/google_drive.py
Expand Up @@ -26,8 +26,8 @@ def get_google_scope(self):
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'):
Expand Down
6 changes: 3 additions & 3 deletions addons/hr/models/hr_employee.py
Expand Up @@ -217,10 +217,10 @@ def read(self, fields, load='_classic_read'):
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):
Expand Down
6 changes: 3 additions & 3 deletions addons/hr/models/res_users.py
Expand Up @@ -169,7 +169,7 @@ def SELF_WRITEABLE_FIELDS(self):
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.
Expand All @@ -182,11 +182,11 @@ def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu
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")
Expand Down
29 changes: 15 additions & 14 deletions addons/hr/tests/test_self_user_access.py
Expand Up @@ -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
Expand All @@ -20,8 +21,8 @@ def test_access_my_profile(self):
'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):
Expand All @@ -35,12 +36,12 @@ def test_readonly_fields(self):
})

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)
Expand All @@ -65,16 +66,16 @@ def test_profile_view_fields(self):
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"""
Expand All @@ -85,7 +86,7 @@ def test_access_my_profile_toolbar(self):
'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))
Expand All @@ -98,7 +99,7 @@ def test_access_my_profile_toolbar(self):
'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))


Expand Down
12 changes: 5 additions & 7 deletions addons/hr_recruitment/models/hr_recruitment.py
Expand Up @@ -52,14 +52,12 @@ def create_alias(self):
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"
Expand Down Expand Up @@ -437,10 +435,10 @@ def get_empty_list_help(self, help):
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):
"""
Expand Down
20 changes: 10 additions & 10 deletions addons/hr_timesheet/models/hr_timesheet.py
Expand Up @@ -157,34 +157,34 @@ def write(self, values):
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
for node in doc.xpath("//field[@widget='timesheet_uom'][not(@string)] | //field[@widget='timesheet_uom_no_toggle'][not(@string)]"):
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'):
Expand Down
18 changes: 9 additions & 9 deletions addons/hr_timesheet/models/project.py
Expand Up @@ -81,11 +81,11 @@ def _search_is_internal_project(self, operator, value):
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):
Expand Down Expand Up @@ -368,16 +368,16 @@ def name_get(self):
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):
Expand Down
8 changes: 4 additions & 4 deletions addons/hr_timesheet/report/project_report.py
Expand Up @@ -33,8 +33,8 @@ def _group_by(self):
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
7 changes: 7 additions & 0 deletions addons/mail/models/ir_ui_view.py
Expand Up @@ -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)
4 changes: 2 additions & 2 deletions addons/membership/models/product.py
Expand Up @@ -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)
6 changes: 4 additions & 2 deletions addons/project/tests/test_project_sharing_portal_access.py
Expand Up @@ -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
Expand Down Expand Up @@ -49,10 +50,11 @@ def setUpClass(cls):

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:
Expand Down

0 comments on commit b03c227

Please sign in to comment.