[REF] models: refactor fields_view_get, load_views

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: odoo/odoo#87522
This commit is contained in:
Denis Ledoux
2022-04-29 09:57:44 +02:00
parent 89e3d9559f
commit b03c227e88
39 changed files with 437 additions and 332 deletions
+4 -5
View File
@@ -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,
+2 -2
View File
@@ -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:
+4 -4
View File
@@ -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):
@@ -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'):
+3 -3
View File
@@ -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):
+3 -3
View File
@@ -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")
+15 -14
View File
@@ -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))
@@ -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):
"""
+10 -10
View File
@@ -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'):
+9 -9
View File
@@ -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):
+4 -4
View File
@@ -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
+7
View File
@@ -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)
+2 -2
View File
@@ -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)
@@ -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:
+25 -27
View File
@@ -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):
+6 -6
View File
@@ -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):
@@ -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 },
+10 -5
View File
@@ -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) => {
+1 -1
View File
@@ -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 = '<div name="%s" data-oe-type="snippet" data-oe-thumbnail="%s" data-oe-snippet-id="%s" data-oe-keywords="%s">' % (
+1 -1
View File
@@ -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 = {}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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]
+1 -1
View File
@@ -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)
+89 -34
View File
@@ -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<view_type>\w+_view_ref)\1
# colon separator (with optional spaces around)
\s*:\s*
# open quote for value
(['"])
(?P<view_id>
# 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>
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):
+10 -17
View File
@@ -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:])
+8 -12
View File
@@ -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
+9 -13
View File
@@ -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):
+7 -7
View File
@@ -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")
+5 -5
View File
@@ -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):
+11 -17
View File
@@ -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': '<div position="inside">a<p/>b<p/>c</div>',
})
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'],
'<form string="F">(<div>a<p/>b<p/>c</div>)</form>',
@@ -1305,8 +1302,7 @@ class TestViews(ViewCase):
'inherit_id': view1.id,
'arch': '<div position="after">a<p/>b<p/>c</div>',
})
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'],
'<form string="F">(<div/>a<p/>b<p/>c)</form>',
@@ -1325,8 +1321,7 @@ class TestViews(ViewCase):
'inherit_id': view1.id,
'arch': '<div position="before">a<p/>b<p/>c</div>',
})
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'],
'<form string="F">(a<p/>b<p/>c<div/>)</form>',
@@ -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: <form> inside a <form>
@@ -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)
@@ -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
@@ -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'],
@@ -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')
@@ -93,7 +93,7 @@
</field>
</page>
<page string="Participants">
<field name="participants">
<field name="participants" widget="many2many">
<tree string="Participants">
<field name="display_name"/>
</tree>
@@ -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:
+113 -43
View File
@@ -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_type>_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
+33 -47
View File
@@ -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<view_type>\w+_view_ref)\1
# colon separator (with optional spaces around)
\s*:\s*
# open quote for value
(['"])
(?P<view_id>
# 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)
+9 -5
View File
@@ -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) :