From 6412575f4bebf9ac73a0c550ac408d8488feb140 Mon Sep 17 00:00:00 2001 From: std-odoo Date: Wed, 19 Apr 2023 08:16:16 +0200 Subject: [PATCH 1/6] [IMP] base: allow to use `default_` context key for properties Purpose ======= Allow to use `default_properties.xxxxx` as a context key, to set the default value on a property. We need that because when we group by a property in a Kanban view, we can create new record in a given column, and this process will set this context key. Task-3032464 Part-of: odoo/odoo#103510 --- .../test_new_api/tests/test_properties.py | 35 +++++++++++++++++++ odoo/fields.py | 7 +++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/odoo/addons/test_new_api/tests/test_properties.py b/odoo/addons/test_new_api/tests/test_properties.py index 5bb2f5a2578..e78b6220a10 100644 --- a/odoo/addons/test_new_api/tests/test_properties.py +++ b/odoo/addons/test_new_api/tests/test_properties.py @@ -604,6 +604,41 @@ class PropertiesCase(TestPropertiesMixin): }) self.assertEqual(self._get_sql_properties(record), {'my_char': 'my char', 'test': self.partner_2.id}) + # use the context to set the default value, the default key in the definition is ignored + # (e.g. when you create a new record in a Kanban view grouped by a property) + del property_definition[0]['value'] + self.discussion_1.attributes_definition = property_definition + partner = self.env['test_new_api.partner'].create({'name': 'Test Default'}) + message = self.env['test_new_api.message'] \ + .with_context({'default_attributes.my_many2one': partner.id}) \ + .create({ + 'name': 'Test Message', + 'author': self.user.id, + 'discussion': self.discussion_1.id, + 'attributes': property_definition, + }) + + sql_values = self._get_sql_properties(message) + self.assertEqual(sql_values, {'my_many2one': partner.id}) + properties = message.read(['attributes'])[0]['attributes'] + self.assertEqual(properties[0]['value'], (partner.id, partner.display_name)) + + # "None" is a valid default value + del property_definition[0]['value'] + message = self.env['test_new_api.message'] \ + .with_context({'default_attributes.my_many2one': None}) \ + .create({ + 'name': 'Test Message', + 'author': self.user.id, + 'discussion': self.discussion_1.id, + 'attributes': property_definition, + }) + + sql_values = self._get_sql_properties(message) + self.assertEqual(sql_values, {'my_many2one': False}) + properties = message.read(['attributes'])[0]['attributes'] + self.assertEqual(properties[0]['value'], False) + def test_properties_field_read(self): """Test the behavior of the read method. diff --git a/odoo/fields.py b/odoo/fields.py index b0ede449b2f..b38dc999304 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -3545,7 +3545,12 @@ class Properties(Field): for properties_value in properties_list_values: if properties_value.get('value') is None: - default = properties_value.get('default') or False + property_name = properties_value.get('name') + context_key = f"default_{self.name}.{property_name}" + if property_name and context_key in env.context: + default = env.context[context_key] + else: + default = properties_value.get('default') or False properties_value['value'] = default return properties_list_values From 6c566c2814ff803ad75dc35ed5290cf9e213ce05 Mon Sep 17 00:00:00 2001 From: std-odoo Date: Wed, 19 Apr 2023 08:48:05 +0200 Subject: [PATCH 2/6] [IMP] base: properties, better name verification Purpose ======= Currently, it is possible to use some bad characters in a property name by writing directly on the parent (not via the child). It's possible only with RPC call and it wasn't an issue because we recheck the name anyway before using it in SQL query (but it should be cleaned). We also restrict the maximum length (there's nor reason to use huge property names). Task-3032464 Part-of: odoo/odoo#103510 --- .../test_new_api/tests/test_properties.py | 28 +++++++++++++++++++ odoo/fields.py | 3 +- odoo/models.py | 4 +-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/odoo/addons/test_new_api/tests/test_properties.py b/odoo/addons/test_new_api/tests/test_properties.py index e78b6220a10..937ad908234 100644 --- a/odoo/addons/test_new_api/tests/test_properties.py +++ b/odoo/addons/test_new_api/tests/test_properties.py @@ -186,6 +186,34 @@ class PropertiesCase(TestPropertiesMixin): 'default': 'blue', }]) + def test_properties_field_injection(self): + for c in '!#"\'- |+/\\': + with self.assertRaises(ValueError): + self.message_1.attributes = [{ + 'name': f'discussion_color_code{c}', + 'type': 'char', + 'definition_changed': True + }] + + with self.assertRaises(ValueError): + self.discussion_1.attributes_definition = [{ + 'name': f'discussion_color_code{c}', + 'type': 'char', + }] + + with self.assertRaises(ValueError): + self.message_1.attributes = [{ + 'name': 'a' * 513, + 'type': 'char', + 'definition_changed': True + }] + + with self.assertRaises(ValueError): + self.discussion_1.attributes_definition = [{ + 'name': 'a' * 513, + 'type': 'char', + }] + @mute_logger('odoo.fields') def test_properties_field_write_batch(self): """Test the behavior of the write called in batch. diff --git a/odoo/fields.py b/odoo/fields.py index b38dc999304..e6bb321c007 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -3477,7 +3477,6 @@ class Properties(Field): ] for definition in value: definition.pop('definition_changed', None) - check_property_field_value_name(definition.get('name', '')) # update the properties definition on the container container = records[self.definition_record] @@ -3946,6 +3945,8 @@ class PropertiesDefinition(Field): ', '.join(invalid_keys), ) + check_property_field_value_name(property_definition['name']) + required_keys = set(cls.REQUIRED_KEYS) - property_definition_keys if required_keys: raise ValueError( diff --git a/odoo/models.py b/odoo/models.py index 2dfb8a9dfc5..4559f3040b9 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -146,8 +146,8 @@ def check_method_name(name): def check_property_field_value_name(property_name): - if not regex_alphanumeric.match(property_name): - raise ValueError(_("Wrong property field value name %r.", property_name)) + if not regex_alphanumeric.match(property_name) or len(property_name) > 512: + raise ValueError(f"Wrong property field value name {property_name!r}.") def fix_import_export_id_paths(fieldname): From 88f4c4dd36fb0d74e8adf1ea2a9572c495dcce8c Mon Sep 17 00:00:00 2001 From: std-odoo Date: Wed, 19 Apr 2023 08:35:35 +0200 Subject: [PATCH 3/6] [IMP] base: properties, do not allow to select transient models Purpose ======= Do not allow to select transient models in relational properties. Task-3032464 Part-of: odoo/odoo#103510 --- addons/web/models/ir_model.py | 16 ++++++---- addons/web/tests/test_ir_model.py | 9 ++++++ .../test_new_api/models/test_new_api.py | 5 ++++ .../test_new_api/security/ir.model.access.csv | 1 + .../test_new_api/tests/test_properties.py | 30 +++++++++++++++++++ odoo/fields.py | 2 +- 6 files changed, 57 insertions(+), 6 deletions(-) diff --git a/addons/web/models/ir_model.py b/addons/web/models/ir_model.py index ade00f6ee89..44ce0e9c074 100644 --- a/addons/web/models/ir_model.py +++ b/addons/web/models/ir_model.py @@ -19,7 +19,7 @@ class IrModel(models.Model): accessible_models = [] not_accessible_models = [] for model in models: - if self._check_model_access(model): + if self._is_valid_for_model_selector(model): accessible_models.append(model) else: not_accessible_models.append({"display_name": model, "model": model}) @@ -34,9 +34,15 @@ class IrModel(models.Model): } for model in records] @api.model - def _check_model_access(self, model): - return (self.env.user._is_internal() and model in self.env - and self.env[model].check_access_rights("read", raise_exception=False)) + def _is_valid_for_model_selector(self, model): + model = self.env.get(model) + return ( + self.env.user._is_internal() + and model is not None + and model.check_access_rights("read", raise_exception=False) + and not model._transient + and not model._abstract + ) @api.model def get_available_models(self): @@ -44,5 +50,5 @@ class IrModel(models.Model): Return the list of models the current user has access to, with their corresponding display name. """ - accessible_models = [model for model in self.pool.keys() if self._check_model_access(model)] + accessible_models = [model for model in self.pool if self._is_valid_for_model_selector(model)] return self._display_name_for(accessible_models) diff --git a/addons/web/tests/test_ir_model.py b/addons/web/tests/test_ir_model.py index 8ba412761cb..3b718bbd3e9 100644 --- a/addons/web/tests/test_ir_model.py +++ b/addons/web/tests/test_ir_model.py @@ -63,3 +63,12 @@ class IrModelAccessTest(TransactionCase): # non existent model comes after existent model result = self.env['ir.model'].display_name_for(["res.company", "unexistent"]) self.assertEqual(result, [{"display_name": "Companies", "model": "res.company"}, {"display_name": "unexistent", "model": "unexistent"}]) + # transient models + result = self.env['ir.model'].display_name_for(["res.company", "base.language.export"]) + self.assertEqual(result, [{"display_name": "Companies", "model": "res.company"}, {"display_name": "base.language.export", "model": "base.language.export"}]) + + # do not return results for transient models + result = self.env['ir.model'].get_available_models() + result = {values["model"] for values in result} + self.assertIn("res.company", result) + self.assertNotIn("base.language.export", result) diff --git a/odoo/addons/test_new_api/models/test_new_api.py b/odoo/addons/test_new_api/models/test_new_api.py index 17c875fa9bf..bd3aa6d176a 100644 --- a/odoo/addons/test_new_api/models/test_new_api.py +++ b/odoo/addons/test_new_api/models/test_new_api.py @@ -962,6 +962,11 @@ class RequiredM2OTransient(models.TransientModel): bar = fields.Many2one('res.country', required=True) +class TestTransient(models.TransientModel): + _name = 'test_new_api.transient_model' + _description = 'Transient Model' + + class Attachment(models.Model): _name = 'test_new_api.attachment' _description = 'Attachment' diff --git a/odoo/addons/test_new_api/security/ir.model.access.csv b/odoo/addons/test_new_api/security/ir.model.access.csv index 1c25a03c3d1..8207897d07e 100644 --- a/odoo/addons/test_new_api/security/ir.model.access.csv +++ b/odoo/addons/test_new_api/security/ir.model.access.csv @@ -114,3 +114,4 @@ access_test_new_api_unsearchable_o2m_employee,access_test_new_api_unsearchable_o access_test_new_api_any_parent,access_test_new_api_any_parent,model_test_new_api_any_parent,base.group_user,1,0,0,0 access_test_new_api_any_child,access_test_new_api_any_child,model_test_new_api_any_child,base.group_user,1,0,0,0 access_test_new_api_any_tag,access_test_new_api_any_tag,model_test_new_api_any_tag,base.group_user,1,0,0,0 +access_test_new_api_transient_model,access_test_new_api_transient_model,model_test_new_api_transient_model,base.group_user,1,1,1,1 diff --git a/odoo/addons/test_new_api/tests/test_properties.py b/odoo/addons/test_new_api/tests/test_properties.py index 937ad908234..f3529c0c765 100644 --- a/odoo/addons/test_new_api/tests/test_properties.py +++ b/odoo/addons/test_new_api/tests/test_properties.py @@ -740,6 +740,21 @@ class PropertiesCase(TestPropertiesMixin): self.assertEqual(properties[1]['value'], (self.partner_2.id, self.partner_2.display_name)) self.assertEqual(properties[1]['comodel'], 'test_new_api.partner') + # should not be able to set a transient model + with self.assertRaises(ValueError): + self.message_2.attributes = [{ + "name": "moderator_partner_id", + "type": "many2one", + "comodel": "test_new_api.transient_model", + "definition_changed": True, + }] + with self.assertRaises(ValueError): + self.discussion_1.attributes_definition = [{ + "name": "moderator_partner_id", + "type": "many2one", + "comodel": "test_new_api.transient_model", + }] + @mute_logger('odoo.models.unlink', 'odoo.fields') def test_properties_field_many2one_unlink(self): """Test the case where we unlink the many2one record.""" @@ -1232,6 +1247,21 @@ class PropertiesCase(TestPropertiesMixin): 'value': [(partners[9].id, partners[9].display_name)], }]) + # should not be able to set a transient model + with self.assertRaises(ValueError): + self.message_2.attributes = [{ + "name": "partner_ids", + "type": "many2many", + "comodel": "test_new_api.transient_model", + "definition_changed": True, + }] + with self.assertRaises(ValueError): + self.discussion_1.attributes_definition = [{ + "name": "partner_ids", + "type": "many2many", + "comodel": "test_new_api.transient_model", + }] + @users('test') @mute_logger('odoo.addons.base.models.ir_rule', 'odoo.fields') def test_properties_field_many2many_filtering(self): diff --git a/odoo/fields.py b/odoo/fields.py index e6bb321c007..1d95386fe10 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -3964,7 +3964,7 @@ class PropertiesDefinition(Field): raise ValueError(f'Wrong property type {property_type!r}.') model = property_definition.get('comodel') - if model and model not in env: + if model and (model not in env or env[model].is_transient() or env[model]._abstract): raise ValueError(f'Invalid model name {model!r}') property_selection = property_definition.get('selection') From 15353f06d7b332197d3c5ceeab6bf48f47d2c60e Mon Sep 17 00:00:00 2001 From: std-odoo Date: Thu, 20 Apr 2023 11:44:26 +0200 Subject: [PATCH 4/6] [IMP] base, web: allow to group by properties Purpose ======= Allow to group records by their property values, like we can do with normal fields. Technical ========= Because there's no foreign key, when we group by a relational property we need to check the existence of the ids in the query (and same for selection and tags, because we might have value of a deleted option / tag in database). Task-3032464 Part-of: odoo/odoo#103510 --- addons/web/models/models.py | 21 +- addons/web/static/src/core/field_service.js | 13 +- .../src/model/relational_model/record.js | 18 +- .../relational_model/relational_model.js | 36 + .../src/model/relational_model/utils.js | 2 +- .../custom_group_by_item.xml | 1 + .../properties_group_by_item.js | 71 ++ .../properties_group_by_item.xml | 40 + .../search/search_bar_menu/search_bar_menu.js | 7 +- .../search_bar_menu/search_bar_menu.xml | 5 +- addons/web/static/src/search/search_model.js | 115 ++- addons/web/static/src/search/utils/misc.js | 1 + .../src/views/pivot/pivot_group_by_menu.js | 8 +- .../src/views/pivot/pivot_group_by_menu.xml | 5 +- .../static/tests/core/field_service_tests.js | 1 + .../tests/search/search_bar_menu_tests.js | 136 +++ .../static/tests/search/search_bar_tests.js | 3 +- .../test_new_api/tests/test_properties.py | 880 +++++++++++++++++- odoo/models.py | 346 ++++++- odoo/tools/query.py | 5 + 20 files changed, 1628 insertions(+), 86 deletions(-) create mode 100644 addons/web/static/src/search/properties_group_by_item/properties_group_by_item.js create mode 100644 addons/web/static/src/search/properties_group_by_item/properties_group_by_item.xml diff --git a/addons/web/models/models.py b/addons/web/models/models.py index 993da2a539d..61b7b7265ac 100644 --- a/addons/web/models/models.py +++ b/addons/web/models/models.py @@ -275,8 +275,9 @@ class Base(models.AbstractModel): :return a dictionnary mapping group_by values to dictionnaries mapping progress bar field values to the related number of records """ - group_by_fname = group_by.partition(':')[0] - field_type = self._fields[group_by_fname].type + group_by_fullname = group_by.partition(':')[0] + group_by_fieldname = group_by_fullname.split(".")[0] # split on "." in case we group on a property + field_type = self._fields[group_by_fieldname].type if field_type == 'selection': selection_labels = dict(self.fields_get()[group_by]['selection']) @@ -311,14 +312,22 @@ class Base(models.AbstractModel): # Workaround to match read_group's infrastructure # TO DO in master: harmonize this function and readgroup to allow factorization - group_by_name = group_by.partition(':')[0] + group_by_fullname = group_by.partition(':')[0] + group_by_fieldname = group_by_fullname.split(".")[0] # split on "." in case we group on a property group_by_modifier = group_by.partition(':')[2] or 'month' - records_values = self.search_read(domain or [], [progress_bar['field'], group_by_name]) - field_type = self._fields[group_by_name].type + records_values = self.search_read(domain or [], [progress_bar['field'], group_by_fieldname]) + field_type = self._fields[group_by_fieldname].type for record_values in records_values: - group_by_value = record_values.pop(group_by_name) + group_by_value = record_values.pop(group_by_fieldname) + property_name = group_by_fullname.partition('.')[2] + if field_type == "properties" and group_by_value: + group_by_value = next( + (definition['value'] for definition in group_by_value + if definition['name'] == property_name), + False, + ) # Again, imitating what _read_group_format_result and _read_group_prepare_data do if group_by_value and field_type in ['date', 'datetime']: diff --git a/addons/web/static/src/core/field_service.js b/addons/web/static/src/core/field_service.js index 4c405efa855..99555dd17a2 100644 --- a/addons/web/static/src/core/field_service.js +++ b/addons/web/static/src/core/field_service.js @@ -55,19 +55,22 @@ export const fieldService = { domain = Domain.and([[[definitionRecordField, "!=", false]], domain]).toList(); - const records = await orm.searchRead(definitionRecordModel, domain, [ - "display_name", - definitionRecordField, - ]); + const result = await orm.webSearchRead(definitionRecordModel, domain, { + specification: { + display_name: {}, + [definitionRecordField]: {}, + }, + }); const definitions = {}; - for (const record of records) { + for (const record of result.records) { for (const definition of record[definitionRecordField]) { definitions[definition.name] = { is_property: true, // for now, all properties are searchable but their definitions don't contain that info searchable: true, // differentiate definitions with same name but on different parent + record_id: record.id, record_name: record.display_name, ...definition, }; diff --git a/addons/web/static/src/model/relational_model/record.js b/addons/web/static/src/model/relational_model/record.js index 52afbb48d70..c9b76a69c8a 100644 --- a/addons/web/static/src/model/relational_model/record.js +++ b/addons/web/static/src/model/relational_model/record.js @@ -981,10 +981,22 @@ export class Record extends DataPoint { for (const [fieldName, value] of Object.entries(changes)) { const field = this.fields[fieldName]; if (field && field.relatedPropertyField) { - const propertyFieldName = field.relatedPropertyField.fieldName; - changes[propertyFieldName] = this.data[propertyFieldName].map((property) => - property.name === field.propertyName ? { ...property, value } : property + const [propertyFieldName, propertyName] = field.name.split("."); + const propertiesData = this.data[propertyFieldName] || []; + if (!propertiesData.find((property) => property.name === propertyName)) { + // try to change the value of a properties that has a different parent + this.model.notification.add( + _t( + "This record belongs to a different parent so you can not change this property." + ), + { type: "warning" } + ); + return; + } + changes[propertyFieldName] = propertiesData.map((property) => + property.name === propertyName ? { ...property, value } : property ); + delete changes[field.name]; } } const onChangeFields = Object.keys(changes).filter( diff --git a/addons/web/static/src/model/relational_model/relational_model.js b/addons/web/static/src/model/relational_model/relational_model.js index ed9468725ae..1b1af02b6dc 100644 --- a/addons/web/static/src/model/relational_model/relational_model.js +++ b/addons/web/static/src/model/relational_model/relational_model.js @@ -22,6 +22,7 @@ import { getBasicEvalContext, getFieldsSpec, isRelational, + makeActiveField, } from "./utils"; /** @@ -186,6 +187,30 @@ export class RelationalModel extends Model { // Protected // ------------------------------------------------------------------------- + /** + * If we group by default based on a property, the property might not be loaded in `fields`. + */ + async _getPropertyDefinition(config, propertyFullName) { + // dynamically load the property and add the definition in the fields attribute + const result = await this.orm.call( + config.resModel, + "get_property_definition", + [propertyFullName], + { context: config.context } + ); + if (!result) { + // the property might have been removed + config.groupBy = null; + } else { + result.propertyName = result.name; + result.name = propertyFullName; // "xxxxx" -> "property.xxxxx" + // needed for _applyChanges + result.relatedPropertyField = { fieldName: propertyFullName.split(".")[0] }; + result.relation = result.comodel; // match name on field + config.fields[propertyFullName] = result; + } + } + _askChanges() { const proms = []; this.bus.trigger("NEED_LOCAL_CHANGES", { proms }); @@ -330,6 +355,17 @@ export class RelationalModel extends Model { } config.groups = config.groups || {}; const firstGroupByName = config.groupBy[0].split(":")[0]; + if (firstGroupByName.includes(".")) { + if (!config.fields[firstGroupByName]) { + await this._getPropertyDefinition(config, firstGroupByName); + } + const propertiesFieldName = firstGroupByName.split(".")[0]; + if (!config.activeFields[propertiesFieldName]) { + // add the properties field so we load its data when reading the records + // so when we drag and drop we don't need to fetch the value of the record + config.activeFields[propertiesFieldName] = makeActiveField(); + } + } const orderBy = config.orderBy.filter( (o) => o.name === firstGroupByName || diff --git a/addons/web/static/src/model/relational_model/utils.js b/addons/web/static/src/model/relational_model/utils.js index 6e847445d8a..91f00ed6737 100644 --- a/addons/web/static/src/model/relational_model/utils.js +++ b/addons/web/static/src/model/relational_model/utils.js @@ -521,7 +521,7 @@ function getDisplayNameFromGroupData(field, rawValue) { if (field.type === "selection") { return Object.fromEntries(field.selection)[rawValue]; } - if (["many2one", "many2many"].includes(field.type)) { + if (["many2one", "many2many", "tags"].includes(field.type)) { return rawValue ? rawValue[1] : false; } return rawValue; diff --git a/addons/web/static/src/search/custom_group_by_item/custom_group_by_item.xml b/addons/web/static/src/search/custom_group_by_item/custom_group_by_item.xml index 51a41755b56..313b7d74374 100644 --- a/addons/web/static/src/search/custom_group_by_item/custom_group_by_item.xml +++ b/addons/web/static/src/search/custom_group_by_item/custom_group_by_item.xml @@ -6,6 +6,7 @@ diff --git a/addons/web/static/src/search/properties_group_by_item/properties_group_by_item.js b/addons/web/static/src/search/properties_group_by_item/properties_group_by_item.js new file mode 100644 index 00000000000..6b0fcc20f96 --- /dev/null +++ b/addons/web/static/src/search/properties_group_by_item/properties_group_by_item.js @@ -0,0 +1,71 @@ +/** @odoo-module **/ + +import { AccordionItem, ACCORDION } from "@web/core/dropdown/accordion_item"; +import { SearchDropdownItem } from "@web/search/search_dropdown_item/search_dropdown_item"; +import { Component, useState, useChildSubEnv } from "@odoo/owl"; + +export class PropertiesGroupByItem extends Component { + static template = "web.PropertiesGroupByItem"; + static components = { AccordionItem, SearchDropdownItem }; + static props = { + item: Object, + onGroup: Function, + }; + + setup() { + this.state = useState({ groupByItems: [] }); + useChildSubEnv({ + [ACCORDION]: { + accordionStateChanged: this.beforeOpen.bind(this), + }, + }); + } + + /** + * The properties field is considered as active if one of its property is active. + */ + get isActive() { + return this.state.groupByItems.some((item) => item.isActive); + } + + /** + * True if all group items come from the same definition record. + */ + get isSingleParent() { + const uniqueNames = new Set(this.state.groupByItems.map((item) => item.definitionRecordId)); + return uniqueNames.size < 2; + } + + /** + * Dynamically load the definition, only when needed (if we open the dropdown). + */ + async beforeOpen() { + if (this.definitionLoaded) { + return; + } + this.definitionLoaded = true; + + await this.env.searchModel.fillSearchViewItemsProperty(); + this._updateGroupByItems(); + } + + /** + * Callback to group records per one property. + */ + onGroup(ids) { + this.props.onGroup(ids); + this._updateGroupByItems(); // isActive state might have changed + } + + /** + * Update the component state to sync it with the search model group item. + */ + _updateGroupByItems() { + this.state.groupByItems = this.env.searchModel.getSearchItems( + (searchItem) => + ["groupBy", "dateGroupBy"].includes(searchItem.type) && + searchItem.isProperty && + searchItem.propertyFieldName === this.props.item.fieldName + ); + } +} diff --git a/addons/web/static/src/search/properties_group_by_item/properties_group_by_item.xml b/addons/web/static/src/search/properties_group_by_item/properties_group_by_item.xml new file mode 100644 index 00000000000..abb41b33f2a --- /dev/null +++ b/addons/web/static/src/search/properties_group_by_item/properties_group_by_item.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + No Properties + + + + diff --git a/addons/web/static/src/search/search_bar_menu/search_bar_menu.js b/addons/web/static/src/search/search_bar_menu/search_bar_menu.js index 75587be4637..43314badd6c 100644 --- a/addons/web/static/src/search/search_bar_menu/search_bar_menu.js +++ b/addons/web/static/src/search/search_bar_menu/search_bar_menu.js @@ -4,6 +4,7 @@ import { Component } from "@odoo/owl"; import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; import { DomainSelectorDialog } from "../../core/domain_selector_dialog/domain_selector_dialog"; import { Dropdown } from "@web/core/dropdown/dropdown"; +import { PropertiesGroupByItem } from "@web/search/properties_group_by_item/properties_group_by_item"; import { DropdownItem } from "@web/core/dropdown/dropdown_item"; import { registry } from "@web/core/registry"; import { sortBy } from "@web/core/utils/arrays"; @@ -25,6 +26,7 @@ export class SearchBarMenu extends Component { SearchDropdownItem, CustomGroupByItem, AccordionItem, + PropertiesGroupByItem, }; static props = { slots: { @@ -104,8 +106,9 @@ export class SearchBarMenu extends Component { * @returns {Object[]} */ get groupByItems() { - return this.env.searchModel.getSearchItems((searchItem) => - ["groupBy", "dateGroupBy"].includes(searchItem.type) + return this.env.searchModel.getSearchItems( + (searchItem) => + ["groupBy", "dateGroupBy"].includes(searchItem.type) && !searchItem.isProperty ); } diff --git a/addons/web/static/src/search/search_bar_menu/search_bar_menu.xml b/addons/web/static/src/search/search_bar_menu/search_bar_menu.xml index c44d2a66859..559f6b98c13 100644 --- a/addons/web/static/src/search/search_bar_menu/search_bar_menu.xml +++ b/addons/web/static/src/search/search_bar_menu/search_bar_menu.xml @@ -66,7 +66,10 @@