diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 09eb21a4ac6..e0454a8fa0c 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -3,6 +3,7 @@ import collections import datetime import fnmatch +import json import logging import re import time @@ -22,7 +23,6 @@ from odoo import api, fields, models, tools, _ from odoo.exceptions import ValidationError from odoo.http import request from odoo.modules.module import get_resource_from_path, get_resource_path -from odoo.osv import orm from odoo.tools import config, graph, ConstantMapping, pycompat, apply_inheritance_specs, locate_node from odoo.tools.convert import _fix_multiple_roots from odoo.tools.json import scriptsafe as json_scriptsafe @@ -40,6 +40,63 @@ MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpa INHERIT_ORDER = 'priority,id' +def transfer_field_to_modifiers(field, modifiers): + default_values = {} + state_exceptions = {} + for attr in ('invisible', 'readonly', 'required'): + state_exceptions[attr] = [] + default_values[attr] = bool(field.get(attr)) + for state, modifs in field.get("states",{}).items(): + for modif in modifs: + if default_values[modif[0]] != modif[1]: + state_exceptions[modif[0]].append(state) + + for attr, default_value in default_values.items(): + if state_exceptions[attr]: + modifiers[attr] = [("state", "not in" if default_value else "in", state_exceptions[attr])] + else: + modifiers[attr] = default_value + + +def transfer_node_to_modifiers(node, modifiers, context=None, in_tree_view=False): + # Don't deal with groups, it is done by check_group(). + # Need the context to evaluate the invisible attribute on tree views. + # For non-tree views, the context shouldn't be given. + if node.get('attrs'): + modifiers.update(safe_eval(node.get('attrs'))) + + if node.get('states'): + if 'invisible' in modifiers and isinstance(modifiers['invisible'], list): + # TODO combine with AND or OR, use implicit AND for now. + modifiers['invisible'].append(('state', 'not in', node.get('states').split(','))) + else: + modifiers['invisible'] = [('state', 'not in', node.get('states').split(','))] + + for a in ('invisible', 'readonly', 'required'): + if node.get(a): + v = bool(safe_eval(node.get(a), {'context': context or {}})) + if in_tree_view and a == 'invisible': + # Invisible in a tree view has a specific meaning, make it a + # new key in the modifiers attribute. + modifiers['column_invisible'] = v + elif v or (a not in modifiers or not isinstance(modifiers[a], list)): + # Don't set the attribute to False if a dynamic value was + # provided (i.e. a domain from attrs or states). + modifiers[a] = v + + +def simplify_modifiers(modifiers): + for a in ('invisible', 'readonly', 'required'): + if a in modifiers and not modifiers[a]: + del modifiers[a] + + +def transfer_modifiers_to_node(modifiers, node): + if modifiers: + simplify_modifiers(modifiers) + node.set('modifiers', json.dumps(modifiers)) + + def keep_query(*keep_params, **additional_params): """ Generate a query string keeping the current request querystring's parameters specified @@ -742,7 +799,7 @@ actual arch. field = model_fields.get(node.get('name')) if field: - orm.transfer_field_to_modifiers(field, modifiers) + transfer_field_to_modifiers(field, modifiers) elif node.tag == 'groupby': # groupby nodes should be considered as nested view because they may @@ -797,7 +854,7 @@ actual arch. # The view architeture overrides the python model. # Get the attrs before they are (possibly) deleted by check_group below - orm.transfer_node_to_modifiers(node, modifiers, self._context, in_tree_view) + transfer_node_to_modifiers(node, modifiers, self._context, in_tree_view) for f in node: if node.tag == 'search' and f.tag == 'searchpanel': @@ -806,7 +863,7 @@ actual arch. if children or (node.tag == 'field' and f.tag in ('filter', 'separator')): fields.update(self.postprocess(model, f, view_id, in_tree_view, model_fields)) - orm.transfer_modifiers_to_node(modifiers, node) + transfer_modifiers_to_node(modifiers, node) return fields def add_on_change(self, model_name, arch): diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index 6c371741855..96363428b62 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -7,10 +7,12 @@ from lxml import etree from lxml.builder import E from psycopg2 import IntegrityError -from odoo.osv.orm import modifiers_tests from odoo.exceptions import ValidationError from odoo.tests import common from odoo.tools import mute_logger +from odoo.addons.base.models.ir_ui_view import ( + transfer_field_to_modifiers, transfer_node_to_modifiers, simplify_modifiers, +) class ViewXMLID(common.TransactionCase): @@ -1104,8 +1106,36 @@ class TestViews(ViewCase): )) def test_modifiers(self): - # implemeted elsewhere... - modifiers_tests() + def _test_modifiers(what, expected): + modifiers = {} + if isinstance(what, str): + node = etree.fromstring(what) + transfer_node_to_modifiers(node, modifiers) + simplify_modifiers(modifiers) + assert modifiers == expected, "%s != %s" % (modifiers, expected) + elif isinstance(what, dict): + transfer_field_to_modifiers(what, modifiers) + simplify_modifiers(modifiers) + assert modifiers == expected, "%s != %s" % (modifiers, expected) + _test_modifiers('', {}) + _test_modifiers('', {"invisible": True}) + _test_modifiers('', {"readonly": True}) + _test_modifiers('', {"required": True}) + _test_modifiers('', {}) + _test_modifiers('', {}) + _test_modifiers('', {}) + # TODO: Order is not guaranteed + _test_modifiers('', + {"invisible": True, "required": True}) + _test_modifiers('', {"invisible": True}) + _test_modifiers('', {"required": True}) + _test_modifiers("""""", + {"invisible": [["b", "=", "c"]]}) + + # The dictionary is supposed to be the result of fields_get(). + _test_modifiers({}, {}) + _test_modifiers({"invisible": True}, {"invisible": True}) + _test_modifiers({"invisible": False}, {}) @mute_logger('odoo.addons.base.models.ir_ui_view') def test_invalid_field(self): diff --git a/odoo/osv/orm.py b/odoo/osv/orm.py deleted file mode 100644 index e316949dae1..00000000000 --- a/odoo/osv/orm.py +++ /dev/null @@ -1,157 +0,0 @@ -import json -import warnings - -from lxml import etree - -from ..exceptions import except_orm -from ..models import ( - MetaModel, - BaseModel, - Model, TransientModel, AbstractModel, - - MAGIC_COLUMNS, - LOG_ACCESS_COLUMNS, -) - -from odoo.tools.safe_eval import safe_eval - -# extra definitions for backward compatibility -browse_record_list = BaseModel - -class BRM(type): - def __instancecheck__(self, inst): - warnings.warn(DeprecationWarning( - "browse_record is a deprecated concept and should not be used " - "anymore, you can replace `isinstance(o, browse_record)` by " - "`isinstance(o, BaseModel)`" - )) - return isinstance(inst, BaseModel) and len(inst) <= 1 -browse_record = BRM('browse_record', (object,), {}) - -class NBM(type): - def __instancecheck__(self, inst): - warnings.warn(DeprecationWarning( - "browse_record is a deprecated concept and should not be used " - "anymore, you can replace `isinstance(o, browse_null)` by " - "`isinstance(o, BaseModel) and not o`" - )) - return isinstance(inst, BaseModel) and not inst -browse_null = NBM('browse_null', (object,), {}) - - -def transfer_field_to_modifiers(field, modifiers): - default_values = {} - state_exceptions = {} - for attr in ('invisible', 'readonly', 'required'): - state_exceptions[attr] = [] - default_values[attr] = bool(field.get(attr)) - for state, modifs in field.get("states",{}).items(): - for modif in modifs: - if default_values[modif[0]] != modif[1]: - state_exceptions[modif[0]].append(state) - - for attr, default_value in default_values.items(): - if state_exceptions[attr]: - modifiers[attr] = [("state", "not in" if default_value else "in", state_exceptions[attr])] - else: - modifiers[attr] = default_value - - -# Don't deal with groups, it is done by check_group(). -# Need the context to evaluate the invisible attribute on tree views. -# For non-tree views, the context shouldn't be given. -def transfer_node_to_modifiers(node, modifiers, context=None, in_tree_view=False): - if node.get('attrs'): - modifiers.update(safe_eval(node.get('attrs'))) - - if node.get('states'): - if 'invisible' in modifiers and isinstance(modifiers['invisible'], list): - # TODO combine with AND or OR, use implicit AND for now. - modifiers['invisible'].append(('state', 'not in', node.get('states').split(','))) - else: - modifiers['invisible'] = [('state', 'not in', node.get('states').split(','))] - - for a in ('invisible', 'readonly', 'required'): - if node.get(a): - v = bool(safe_eval(node.get(a), {'context': context or {}})) - if in_tree_view and a == 'invisible': - # Invisible in a tree view has a specific meaning, make it a - # new key in the modifiers attribute. - modifiers['column_invisible'] = v - elif v or (a not in modifiers or not isinstance(modifiers[a], list)): - # Don't set the attribute to False if a dynamic value was - # provided (i.e. a domain from attrs or states). - modifiers[a] = v - - -def simplify_modifiers(modifiers): - for a in ('invisible', 'readonly', 'required'): - if a in modifiers and not modifiers[a]: - del modifiers[a] - - -def transfer_modifiers_to_node(modifiers, node): - if modifiers: - simplify_modifiers(modifiers) - node.set('modifiers', json.dumps(modifiers)) - -def setup_modifiers(node, field=None, context=None, in_tree_view=False): - """ Processes node attributes and field descriptors to generate - the ``modifiers`` node attribute and set it on the provided node. - - Alters its first argument in-place. - - :param node: ``field`` node from an OpenERP view - :type node: lxml.etree._Element - :param dict field: field descriptor corresponding to the provided node - :param dict context: execution context used to evaluate node attributes - :param bool in_tree_view: triggers the ``column_invisible`` code - path (separate from ``invisible``): in - tree view there are two levels of - invisibility, cell content (a column is - present but the cell itself is not - displayed) with ``invisible`` and column - invisibility (the whole column is - hidden) with ``column_invisible``. - :returns: nothing - """ - modifiers = {} - if field is not None: - transfer_field_to_modifiers(field, modifiers) - transfer_node_to_modifiers( - node, modifiers, context=context, in_tree_view=in_tree_view) - transfer_modifiers_to_node(modifiers, node) - -def test_modifiers(what, expected): - modifiers = {} - if isinstance(what, str): - node = etree.fromstring(what) - transfer_node_to_modifiers(node, modifiers) - simplify_modifiers(modifiers) - assert modifiers == expected, "%s != %s" % (modifiers, expected) - elif isinstance(what, dict): - transfer_field_to_modifiers(what, modifiers) - simplify_modifiers(modifiers) - assert modifiers == expected, "%s != %s" % (modifiers, expected) - - -# To use this test: -# import odoo -# odoo.osv.orm.modifiers_tests() -def modifiers_tests(): - test_modifiers('', {}) - test_modifiers('', {"invisible": True}) - test_modifiers('', {"readonly": True}) - test_modifiers('', {"required": True}) - test_modifiers('', {}) - test_modifiers('', {}) - test_modifiers('', {}) - test_modifiers('', {"invisible": True, "required": True}) # TODO order is not guaranteed - test_modifiers('', {"invisible": True}) - test_modifiers('', {"required": True}) - test_modifiers("""""", {"invisible": [["b", "=", "c"]]}) - - # The dictionary is supposed to be the result of fields_get(). - test_modifiers({}, {}) - test_modifiers({"invisible": True}, {"invisible": True}) - test_modifiers({"invisible": False}, {}) diff --git a/odoo/osv/osv.py b/odoo/osv/osv.py index f5fbcc198ff..dc23494450b 100644 --- a/odoo/osv/osv.py +++ b/odoo/osv/osv.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from ..exceptions import except_orm -from .orm import Model, TransientModel, AbstractModel +from ..models import Model, TransientModel, AbstractModel # Deprecated, kept for backward compatibility. # openerp.exceptions.Warning should be used instead.