From fa4e0e0090a615c5a77f6998aaa2e38777f30420 Mon Sep 17 00:00:00 2001 From: Mohamed Eltayar <152964073+maltayyar2@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:43:27 +0300 Subject: [PATCH 1/4] Fix: Handle _renderBody not returning Promise and improve search logic --- .../static/src/js/list_search.js | 85 ++++++++++++------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js index f0d17203c..699b64715 100644 --- a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js +++ b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js @@ -70,7 +70,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { _performSearch: function (searchValue) { var self = this; - var value = (searchValue || '').toLowerCase().trim(); + var value = (searchValue || '').trim(); // Prevent concurrent searches if (this._isSearching) { @@ -91,7 +91,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { this._isSearching = true; // Store original records if not already stored - if (!this._originalRecords && this.state.data.records) { + if (!this._originalRecords && this.state.data && this.state.data.records) { this._originalRecords = this.state.data.records.slice(); } @@ -103,10 +103,11 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { this._isSearching = false; this.$el.find('.oe_search_loading').hide(); console.warn('No searchable fields found for model:', model); + this._showError(_t('No searchable fields available')); return; } - // Build search domain + // Build search domain - make it case insensitive var domain = this._buildSearchDomain(value, fields); // Add existing domain if any @@ -131,6 +132,9 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { } } + console.log('Searching with domain:', domain); + console.log('Fields to read:', fieldsToRead); + // Perform RPC search with specific fields only - NO LIMIT this._rpc({ model: model, @@ -144,6 +148,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { order: orderBy } }).then(function(result) { + console.log('Search results:', result.length, 'records found'); self._updateListWithSearchResults(result, value); }).catch(function(error) { console.error('Search error:', error); @@ -173,14 +178,8 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { return; // Skip this field } - // For many2one, we only need the ID and display_name - if (field.type === 'many2one') { - fields.push(fieldName); - } - // For other fields, include if they're stored - else if (field.store !== false) { - fields.push(fieldName); - } + // Include all other fields + fields.push(fieldName); } } }); @@ -207,7 +206,8 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { _.each(fields, function(field) { // For char, text, and html fields - if (['char', 'text', 'html'].includes(field.type)) { + if (['char', 'text', 'html', 'selection'].includes(field.type)) { + // Use ilike for case-insensitive search orConditions.push([field.name, 'ilike', value]); // Also search with normalized value if different if (normalizedValue !== value) { @@ -219,14 +219,14 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { // Use the field name directly with ilike orConditions.push([field.name, 'ilike', value]); } - // For selection fields - else if (field.type === 'selection') { - orConditions.push([field.name, 'ilike', value]); - } // For number fields (if value is numeric) else if (['integer', 'float', 'monetary'].includes(field.type)) { if (!isNaN(value) && value !== '') { - orConditions.push([field.name, '=', parseFloat(value)]); + var numValue = parseFloat(value); + orConditions.push([field.name, '=', numValue]); + // Also search for approximate values + orConditions.push([field.name, '>=', numValue - 0.01]); + orConditions.push([field.name, '<=', numValue + 0.01]); } } }); @@ -266,7 +266,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { fields.push({ name: fieldName, type: field.type, - string: field.string + string: field.string || fieldName }); addedFields[fieldName] = true; } @@ -286,7 +286,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { fields.push({ name: fieldName, type: field.type, - string: field.string + string: field.string || fieldName }); addedFields[fieldName] = true; } @@ -294,6 +294,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { }); } + console.log('Searchable fields:', fields); return fields; }, @@ -317,15 +318,24 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { this._searchMode = true; // Re-render the body with new records - this._renderBody().then(function() { - // Restore search input value + // Wrap in Promise to ensure consistent behavior + var renderPromise = this._renderBody(); + if (renderPromise && typeof renderPromise.then === 'function') { + renderPromise.then(function() { + // Restore search input value + self.$el.find('.oe_search_input').val(self._currentSearchValue); + self.$el.find('.oe_clear_search').show(); + $countEl.show(); + }).catch(function(error) { + console.error('Error rendering search results:', error); + self._showError(_t('Error displaying results')); + }); + } else { + // If _renderBody doesn't return a promise, just update UI self.$el.find('.oe_search_input').val(self._currentSearchValue); self.$el.find('.oe_clear_search').show(); $countEl.show(); - }).catch(function(error) { - console.error('Error rendering search results:', error); - self._showError(_t('Error displaying results')); - }); + } } else { $countEl.text(_t('No records found')).show(); @@ -378,15 +388,25 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { this._searchMode = false; // If we have original records, restore them - if (this._originalRecords) { + if (this._originalRecords && this._originalRecords.length > 0) { this.state.data.records = this._originalRecords; - this._renderBody().then(function() { - self._originalRecords = null; - }).catch(function(error) { - console.error('Error restoring original records:', error); - // Just show all rows as fallback + + // Wrap _renderBody in Promise for consistent behavior + var renderPromise = this._renderBody(); + if (renderPromise && typeof renderPromise.then === 'function') { + renderPromise.then(function() { + self._originalRecords = null; + }).catch(function(error) { + console.error('Error restoring original records:', error); + // Just show all rows as fallback + self.$el.find('.o_data_row').show(); + self._originalRecords = null; + }); + } else { + // If _renderBody doesn't return a promise self.$el.find('.o_data_row').show(); - }); + self._originalRecords = null; + } } else { // Just show all rows this.$el.find('.o_data_row').show(); @@ -398,6 +418,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { // Normalizing Arabic text by removing common variations return text + .toLowerCase() // Make lowercase for better matching .replace(/[\u064B-\u065F]/g, '') // Remove diacritics .replace(/[\u0660-\u0669]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030)) // Arabic-Indic digits .replace(/[\u06F0-\u06F9]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030)) // Extended Arabic-Indic From 14d5cdfcea237151d37000b761e1eead8a15718c Mon Sep 17 00:00:00 2001 From: Mohamed Eltayar <152964073+maltayyar2@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:47:00 +0300 Subject: [PATCH 2/4] Complete logic review and fixes: proper case handling, better record management, improved search logic --- .../static/src/js/list_search.js | 171 ++++++++++++------ 1 file changed, 120 insertions(+), 51 deletions(-) diff --git a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js index 699b64715..ef5c26127 100644 --- a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js +++ b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js @@ -70,6 +70,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { _performSearch: function (searchValue) { var self = this; + // Keep original value for search, don't lowercase here var value = (searchValue || '').trim(); // Prevent concurrent searches @@ -91,8 +92,10 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { this._isSearching = true; // Store original records if not already stored - if (!this._originalRecords && this.state.data && this.state.data.records) { - this._originalRecords = this.state.data.records.slice(); + // Check different possible locations for records + var currentRecords = this._getCurrentRecords(); + if (!this._originalRecords && currentRecords) { + this._originalRecords = currentRecords.slice(); } // Get model and fields info @@ -107,7 +110,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { return; } - // Build search domain - make it case insensitive + // Build search domain var domain = this._buildSearchDomain(value, fields); // Add existing domain if any @@ -121,19 +124,12 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { var fieldsToRead = this._getFieldsToRead(); // Build order string safely - var orderBy = false; - if (this.state.orderedBy && this.state.orderedBy.length > 0 && this.state.orderedBy[0]) { - var order = this.state.orderedBy[0]; - if (order.name) { - orderBy = order.name; - if (order.asc !== undefined) { - orderBy += order.asc ? ' ASC' : ' DESC'; - } - } - } + var orderBy = this._getOrderBy(); - console.log('Searching with domain:', domain); + console.log('Search query:', value); + console.log('Searching with domain:', JSON.stringify(domain)); console.log('Fields to read:', fieldsToRead); + console.log('Model:', model); // Perform RPC search with specific fields only - NO LIMIT this._rpc({ @@ -149,6 +145,9 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { } }).then(function(result) { console.log('Search results:', result.length, 'records found'); + if (result.length > 0) { + console.log('Sample result:', result[0]); + } self._updateListWithSearchResults(result, value); }).catch(function(error) { console.error('Search error:', error); @@ -161,6 +160,46 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { }); }, + _getCurrentRecords: function() { + // Try different locations where records might be stored + if (this.state.data && this.state.data.records) { + return this.state.data.records; + } + if (this.state.records) { + return this.state.records; + } + if (this.recordsData) { + return this.recordsData; + } + return null; + }, + + _setCurrentRecords: function(records) { + // Set records in the appropriate location + if (this.state.data) { + this.state.data.records = records; + } else if (this.state.records !== undefined) { + this.state.records = records; + } else { + // Create data structure if it doesn't exist + this.state.data = { records: records }; + } + }, + + _getOrderBy: function() { + var orderBy = false; + if (this.state.orderedBy && this.state.orderedBy.length > 0) { + var order = this.state.orderedBy[0]; + if (order && order.name) { + orderBy = order.name; + if (order.asc !== undefined) { + orderBy += order.asc ? ' ASC' : ' DESC'; + } + } + } + return orderBy; + }, + _getFieldsToRead: function() { var self = this; var fields = ['id']; // Always include ID @@ -175,6 +214,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { if (field) { // Skip computed fields unless they're stored if (field.compute && !field.store) { + console.log('Skipping computed field:', fieldName); return; // Skip this field } @@ -205,28 +245,45 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { var normalizedValue = this._normalizeArabic(value); _.each(fields, function(field) { - // For char, text, and html fields + // For char, text, html, and selection fields if (['char', 'text', 'html', 'selection'].includes(field.type)) { // Use ilike for case-insensitive search orConditions.push([field.name, 'ilike', value]); - // Also search with normalized value if different - if (normalizedValue !== value) { + // Also search with normalized value if different (for Arabic) + if (normalizedValue !== value && normalizedValue) { orConditions.push([field.name, 'ilike', normalizedValue]); } } - // For many2one fields - search in the name + // For many2one fields - search in the display_name else if (field.type === 'many2one') { - // Use the field name directly with ilike + // Many2one search needs special handling + // Try searching by ID if value is numeric + if (!isNaN(value)) { + orConditions.push([field.name, '=', parseInt(value)]); + } + // Also try text search in the related field's name orConditions.push([field.name, 'ilike', value]); } // For number fields (if value is numeric) else if (['integer', 'float', 'monetary'].includes(field.type)) { if (!isNaN(value) && value !== '') { var numValue = parseFloat(value); - orConditions.push([field.name, '=', numValue]); - // Also search for approximate values - orConditions.push([field.name, '>=', numValue - 0.01]); - orConditions.push([field.name, '<=', numValue + 0.01]); + // Exact match for integers + if (field.type === 'integer') { + orConditions.push([field.name, '=', parseInt(value)]); + } else { + // For float/monetary, exact match + orConditions.push([field.name, '=', numValue]); + } + } + } + // For boolean fields + else if (field.type === 'boolean') { + var boolValue = value.toLowerCase(); + if (boolValue === 'true' || boolValue === 'yes' || boolValue === '1' || boolValue === 'نعم') { + orConditions.push([field.name, '=', true]); + } else if (boolValue === 'false' || boolValue === 'no' || boolValue === '0' || boolValue === 'لا') { + orConditions.push([field.name, '=', false]); } } }); @@ -314,27 +371,32 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { $countEl.text(message).show(); // Update the view with new records - this.state.data.records = records; + this._setCurrentRecords(records); this._searchMode = true; // Re-render the body with new records - // Wrap in Promise to ensure consistent behavior - var renderPromise = this._renderBody(); - if (renderPromise && typeof renderPromise.then === 'function') { - renderPromise.then(function() { - // Restore search input value + // Wrap in try-catch and handle both Promise and non-Promise returns + try { + var renderResult = this._renderBody(); + if (renderResult && typeof renderResult.then === 'function') { + renderResult.then(function() { + // Restore search input value + self.$el.find('.oe_search_input').val(self._currentSearchValue); + self.$el.find('.oe_clear_search').show(); + $countEl.show(); + }).catch(function(error) { + console.error('Error rendering search results:', error); + self._showError(_t('Error displaying results')); + }); + } else { + // If _renderBody doesn't return a promise, just update UI self.$el.find('.oe_search_input').val(self._currentSearchValue); self.$el.find('.oe_clear_search').show(); $countEl.show(); - }).catch(function(error) { - console.error('Error rendering search results:', error); - self._showError(_t('Error displaying results')); - }); - } else { - // If _renderBody doesn't return a promise, just update UI - self.$el.find('.oe_search_input').val(self._currentSearchValue); - self.$el.find('.oe_clear_search').show(); - $countEl.show(); + } + } catch (error) { + console.error('Error calling _renderBody:', error); + self._showError(_t('Error displaying results')); } } else { @@ -389,21 +451,28 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { // If we have original records, restore them if (this._originalRecords && this._originalRecords.length > 0) { - this.state.data.records = this._originalRecords; + this._setCurrentRecords(this._originalRecords); - // Wrap _renderBody in Promise for consistent behavior - var renderPromise = this._renderBody(); - if (renderPromise && typeof renderPromise.then === 'function') { - renderPromise.then(function() { - self._originalRecords = null; - }).catch(function(error) { - console.error('Error restoring original records:', error); - // Just show all rows as fallback + // Wrap _renderBody in try-catch for safety + try { + var renderResult = this._renderBody(); + if (renderResult && typeof renderResult.then === 'function') { + renderResult.then(function() { + self._originalRecords = null; + }).catch(function(error) { + console.error('Error restoring original records:', error); + // Just show all rows as fallback + self.$el.find('.o_data_row').show(); + self._originalRecords = null; + }); + } else { + // If _renderBody doesn't return a promise self.$el.find('.o_data_row').show(); self._originalRecords = null; - }); - } else { - // If _renderBody doesn't return a promise + } + } catch (error) { + console.error('Error calling _renderBody:', error); + // Just show all rows as fallback self.$el.find('.o_data_row').show(); self._originalRecords = null; } @@ -417,8 +486,8 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { if (!text) return text; // Normalizing Arabic text by removing common variations + // Don't lowercase here as it's for the search query return text - .toLowerCase() // Make lowercase for better matching .replace(/[\u064B-\u065F]/g, '') // Remove diacritics .replace(/[\u0660-\u0669]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030)) // Arabic-Indic digits .replace(/[\u06F0-\u06F9]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030)) // Extended Arabic-Indic From 3ab8990e62cd26c60b577a5fbfc3847a2d520d2d Mon Sep 17 00:00:00 2001 From: Mohamed Eltayar <152964073+maltayyar2@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:55:43 +0300 Subject: [PATCH 3/4] Ultra-defensive code with complete error handling, fallbacks, and client-side search option --- .../static/src/js/list_search.js | 399 +++++++++++------- 1 file changed, 235 insertions(+), 164 deletions(-) diff --git a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js index ef5c26127..a6d26f1af 100644 --- a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js +++ b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js @@ -16,7 +16,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { var self = this; return this._super.apply(this, arguments).then(function () { self.$('.o_list_table').addClass('o_list_table_ungrouped'); - if (self.arch.tag == 'tree' && self.$el.hasClass('o_list_view')) { + if (self.arch && self.arch.tag === 'tree' && self.$el && self.$el.hasClass('o_list_view')) { // Check if the search input already exists if (!self.$el.find('.oe_search_input').length) { var search = '
' + @@ -32,11 +32,13 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { self._initializeSearch(); } } + }).catch(function(error) { + console.error('Error in _renderView:', error); + return $.when(); }); }, _initializeSearch: function() { - var self = this; this._searchTimeout = null; this._currentSearchValue = ''; this._isSearching = false; @@ -53,7 +55,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { clearTimeout(this._searchTimeout); } - // Store current value to prevent clearing + // Store current value this._currentSearchValue = value; // Add delay to avoid too many requests @@ -70,7 +72,6 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { _performSearch: function (searchValue) { var self = this; - // Keep original value for search, don't lowercase here var value = (searchValue || '').trim(); // Prevent concurrent searches @@ -86,13 +87,19 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { return; } + // Check if we have required state + if (!this.state || !this.state.model) { + console.error('Missing state or model information'); + this._showError(_t('Search not available in this view')); + return; + } + // Show loading indicator this.$el.find('.oe_search_loading').show(); this.$el.find('.oe_search_count').hide(); this._isSearching = true; // Store original records if not already stored - // Check different possible locations for records var currentRecords = this._getCurrentRecords(); if (!this._originalRecords && currentRecords) { this._originalRecords = currentRecords.slice(); @@ -102,93 +109,151 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { var model = this.state.model; var fields = this._getSearchableFields(); - if (fields.length === 0) { + if (!fields || fields.length === 0) { this._isSearching = false; this.$el.find('.oe_search_loading').hide(); console.warn('No searchable fields found for model:', model); - this._showError(_t('No searchable fields available')); + + // Fallback to client-side search if no searchable fields + this._performClientSideSearch(value); return; } // Build search domain - var domain = this._buildSearchDomain(value, fields); + var searchDomain = this._buildSearchDomain(value, fields); - // Add existing domain if any - var baseDomain = this.state.domain || []; - if (baseDomain.length > 0) { - // Combine base domain with search domain - domain = baseDomain.concat(domain); + if (!searchDomain || searchDomain.length === 0) { + this._isSearching = false; + this.$el.find('.oe_search_loading').hide(); + this._showError(_t('Unable to build search query')); + return; } - // Get only visible and stored fields to avoid computed fields issues - var fieldsToRead = this._getFieldsToRead(); + // Get base domain + var baseDomain = []; + if (this.state.domain && Array.isArray(this.state.domain)) { + baseDomain = this.state.domain.slice(); + } - // Build order string safely + // Combine domains + var finalDomain = baseDomain.concat(searchDomain); + + // Get fields to read + var fieldsToRead = this._getFieldsToRead(); + if (!fieldsToRead || fieldsToRead.length === 0) { + fieldsToRead = ['id', 'display_name']; + } + + // Build order var orderBy = this._getOrderBy(); - console.log('Search query:', value); - console.log('Searching with domain:', JSON.stringify(domain)); - console.log('Fields to read:', fieldsToRead); - console.log('Model:', model); + // Clean context - remove restrictions that might limit search + var searchContext = _.extend({}, this.state.context || session.user_context, { + active_test: false, // Include inactive records + bin_size: true // Optimize binary fields + }); - // Perform RPC search with specific fields only - NO LIMIT + console.log('=== Search Debug Info ==='); + console.log('Model:', model); + console.log('Search value:', value); + console.log('Search domain:', JSON.stringify(finalDomain)); + console.log('Fields to read:', fieldsToRead); + console.log('Context:', searchContext); + + // Perform RPC search this._rpc({ model: model, method: 'search_read', - args: [domain], + args: [finalDomain], kwargs: { - fields: fieldsToRead, // Only read necessary fields - limit: false, // No limit - get all records + fields: fieldsToRead, + limit: false, offset: 0, - context: this.state.context || session.user_context, + context: searchContext, order: orderBy } }).then(function(result) { - console.log('Search results:', result.length, 'records found'); + console.log('Search completed. Found:', result.length, 'records'); if (result.length > 0) { - console.log('Sample result:', result[0]); + console.log('First result example:', result[0]); } self._updateListWithSearchResults(result, value); }).catch(function(error) { - console.error('Search error:', error); - self._showError(_t('Search failed. Please try again.')); + console.error('Search RPC error:', error); + self._showError(_t('Search failed. Error: ') + (error.message || error.toString())); + + // Fallback to client-side search + console.log('Falling back to client-side search...'); + self._performClientSideSearch(value); }).finally(function() { self._isSearching = false; self.$el.find('.oe_search_loading').hide(); - // Restore search value self.$el.find('.oe_search_input').val(self._currentSearchValue); }); }, + _performClientSideSearch: function(value) { + // Fallback: Client-side search in visible records + console.log('Performing client-side search for:', value); + + var normalizedValue = this._normalizeArabic(value).toLowerCase(); + var visibleCount = 0; + var totalCount = 0; + + this.$el.find('.o_data_row').each(function() { + var $row = $(this); + totalCount++; + + var rowText = $row.text().toLowerCase(); + var normalizedRowText = this._normalizeArabic(rowText).toLowerCase(); + + var isMatch = rowText.indexOf(value.toLowerCase()) > -1 || + normalizedRowText.indexOf(normalizedValue) > -1; + + $row.toggle(isMatch); + if (isMatch) visibleCount++; + }.bind(this)); + + var $countEl = this.$el.find('.oe_search_count'); + if (visibleCount > 0) { + $countEl.text(_t('Found: ') + visibleCount + '/' + totalCount + _t(' visible records')).show(); + } else { + $countEl.text(_t('No matches in visible records')).show(); + } + }, + _getCurrentRecords: function() { // Try different locations where records might be stored - if (this.state.data && this.state.data.records) { + if (this.state && this.state.data && this.state.data.records) { return this.state.data.records; } - if (this.state.records) { + if (this.state && this.state.records) { return this.state.records; } if (this.recordsData) { return this.recordsData; } + if (this.records) { + return this.records; + } return null; }, _setCurrentRecords: function(records) { // Set records in the appropriate location - if (this.state.data) { + if (this.state && this.state.data) { + this.state.data.records = records; + } else if (this.state) { + if (!this.state.data) { + this.state.data = {}; + } this.state.data.records = records; - } else if (this.state.records !== undefined) { - this.state.records = records; - } else { - // Create data structure if it doesn't exist - this.state.data = { records: records }; } }, _getOrderBy: function() { var orderBy = false; - if (this.state.orderedBy && this.state.orderedBy.length > 0) { + if (this.state && this.state.orderedBy && Array.isArray(this.state.orderedBy) && this.state.orderedBy.length > 0) { var order = this.state.orderedBy[0]; if (order && order.name) { orderBy = order.name; @@ -202,34 +267,42 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { _getFieldsToRead: function() { var self = this; - var fields = ['id']; // Always include ID + var fields = ['id']; - // Get only visible, stored fields from columns + // Check if columns exist + if (!this.columns || !Array.isArray(this.columns)) { + console.warn('No columns found in view'); + return ['id', 'display_name', 'name']; + } + + // Check if state.fields exists + if (!this.state || !this.state.fields) { + console.warn('No fields metadata found'); + return ['id', 'display_name', 'name']; + } + + // Get visible fields from columns _.each(this.columns, function(column) { - if (!column.invisible && column.attrs && column.attrs.name) { + if (column && !column.invisible && column.attrs && column.attrs.name) { var fieldName = column.attrs.name; var field = self.state.fields[fieldName]; - // Only include stored fields or safe field types if (field) { - // Skip computed fields unless they're stored + // Skip computed non-stored fields if (field.compute && !field.store) { - console.log('Skipping computed field:', fieldName); - return; // Skip this field + console.log('Skipping computed non-stored field:', fieldName); + return; } - // Include all other fields fields.push(fieldName); } } }); - // Always try to include display_name if available + // Add essential fields if available if (self.state.fields.display_name && !fields.includes('display_name')) { fields.push('display_name'); } - - // Always try to include name if available if (self.state.fields.name && !fields.includes('name')) { fields.push('name'); } @@ -238,59 +311,60 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { }, _buildSearchDomain: function(value, fields) { + if (!value || !fields || fields.length === 0) { + return []; + } + var domain = []; var orConditions = []; - // Normalize search value for Arabic + // Normalize for Arabic var normalizedValue = this._normalizeArabic(value); _.each(fields, function(field) { - // For char, text, html, and selection fields + if (!field || !field.name || !field.type) { + return; + } + + // Text fields if (['char', 'text', 'html', 'selection'].includes(field.type)) { - // Use ilike for case-insensitive search orConditions.push([field.name, 'ilike', value]); - // Also search with normalized value if different (for Arabic) - if (normalizedValue !== value && normalizedValue) { + if (normalizedValue && normalizedValue !== value) { orConditions.push([field.name, 'ilike', normalizedValue]); } } - // For many2one fields - search in the display_name + // Many2one fields - special handling else if (field.type === 'many2one') { - // Many2one search needs special handling - // Try searching by ID if value is numeric + // Search by ID if numeric if (!isNaN(value)) { orConditions.push([field.name, '=', parseInt(value)]); } - // Also try text search in the related field's name - orConditions.push([field.name, 'ilike', value]); + // Note: searching text in many2one requires special syntax + // We'll search by ID only for now } - // For number fields (if value is numeric) + // Number fields else if (['integer', 'float', 'monetary'].includes(field.type)) { if (!isNaN(value) && value !== '') { - var numValue = parseFloat(value); - // Exact match for integers if (field.type === 'integer') { orConditions.push([field.name, '=', parseInt(value)]); } else { - // For float/monetary, exact match - orConditions.push([field.name, '=', numValue]); + orConditions.push([field.name, '=', parseFloat(value)]); } } } - // For boolean fields + // Boolean fields else if (field.type === 'boolean') { var boolValue = value.toLowerCase(); - if (boolValue === 'true' || boolValue === 'yes' || boolValue === '1' || boolValue === 'نعم') { + if (['true', 'yes', '1', 'نعم', 'صح'].includes(boolValue)) { orConditions.push([field.name, '=', true]); - } else if (boolValue === 'false' || boolValue === 'no' || boolValue === '0' || boolValue === 'لا') { + } else if (['false', 'no', '0', 'لا', 'خطأ'].includes(boolValue)) { orConditions.push([field.name, '=', false]); } } }); - // Create OR domain + // Build OR domain if (orConditions.length > 0) { - // Add '|' operators for OR condition for (var i = 1; i < orConditions.length; i++) { domain.push('|'); } @@ -303,43 +377,49 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { _getSearchableFields: function() { var self = this; var fields = []; - var addedFields = {}; // Track added fields to avoid duplicates + var addedFields = {}; - // Priority 1: Get visible stored fields from columns - _.each(this.columns, function(column) { - if (!column.invisible && column.attrs && column.attrs.name) { - var fieldName = column.attrs.name; - var field = self.state.fields[fieldName]; - - if (field && !addedFields[fieldName]) { - // Only searchable in stored fields or specific field types - var isSearchable = ( - field.searchable !== false && - (field.store !== false || field.type === 'many2one') && - (!field.compute || field.store) // Skip non-stored computed fields - ); + // Check requirements + if (!this.state || !this.state.fields) { + console.warn('No state.fields available'); + return []; + } + + // Get fields from columns if available + if (this.columns && Array.isArray(this.columns)) { + _.each(this.columns, function(column) { + if (column && !column.invisible && column.attrs && column.attrs.name) { + var fieldName = column.attrs.name; + var field = self.state.fields[fieldName]; - if (isSearchable) { - fields.push({ - name: fieldName, - type: field.type, - string: field.string || fieldName - }); - addedFields[fieldName] = true; + if (field && !addedFields[fieldName]) { + // Check if searchable + var isSearchable = ( + field.searchable !== false && + (field.store !== false || field.type === 'many2one') && + (!field.compute || field.store) + ); + + if (isSearchable) { + fields.push({ + name: fieldName, + type: field.type, + string: field.string || fieldName + }); + addedFields[fieldName] = true; + } } } - } - }); + }); + } - // Priority 2: Add common searchable fields if not enough fields + // Add common fields if not enough if (fields.length < 3) { var commonFields = ['name', 'display_name', 'reference', 'code', 'ref', 'description']; _.each(commonFields, function(fieldName) { - if (self.state.fields[fieldName] && !addedFields[fieldName]) { - var field = self.state.fields[fieldName]; - if (field.searchable !== false && - field.store !== false && - (!field.compute || field.store)) { + var field = self.state.fields[fieldName]; + if (field && !addedFields[fieldName]) { + if (field.searchable !== false && field.store !== false && (!field.compute || field.store)) { fields.push({ name: fieldName, type: field.type, @@ -351,75 +431,73 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { }); } - console.log('Searchable fields:', fields); + console.log('Searchable fields found:', fields.length, 'fields'); return fields; }, _updateListWithSearchResults: function(records, searchValue) { var self = this; - // Update the count display + if (!records) { + records = []; + } + var count = records.length; var $countEl = this.$el.find('.oe_search_count'); if (count > 0) { var message = _t('Found: ') + count + _t(' records'); - // Show warning if too many records if (count > 1000) { - message += ' ' + _t('(Large result set, may affect performance)'); + message += ' ' + _t('(Large result set)'); } $countEl.text(message).show(); - // Update the view with new records this._setCurrentRecords(records); this._searchMode = true; - // Re-render the body with new records - // Wrap in try-catch and handle both Promise and non-Promise returns - try { - var renderResult = this._renderBody(); - if (renderResult && typeof renderResult.then === 'function') { - renderResult.then(function() { - // Restore search input value - self.$el.find('.oe_search_input').val(self._currentSearchValue); - self.$el.find('.oe_clear_search').show(); - $countEl.show(); - }).catch(function(error) { - console.error('Error rendering search results:', error); - self._showError(_t('Error displaying results')); - }); - } else { - // If _renderBody doesn't return a promise, just update UI - self.$el.find('.oe_search_input').val(self._currentSearchValue); - self.$el.find('.oe_clear_search').show(); - $countEl.show(); + // Try to render if method exists + if (typeof this._renderBody === 'function') { + try { + var renderResult = this._renderBody(); + if (renderResult && typeof renderResult.then === 'function') { + renderResult.then(function() { + self._restoreSearchUI(); + }).catch(function(error) { + console.error('Render error:', error); + self._restoreSearchUI(); + }); + } else { + self._restoreSearchUI(); + } + } catch (error) { + console.error('Render error:', error); + self._restoreSearchUI(); } - } catch (error) { - console.error('Error calling _renderBody:', error); - self._showError(_t('Error displaying results')); + } else { + console.warn('_renderBody method not found'); + self._restoreSearchUI(); } - } else { $countEl.text(_t('No records found')).show(); this._showNoResults(searchValue); } }, + _restoreSearchUI: function() { + this.$el.find('.oe_search_input').val(this._currentSearchValue); + this.$el.find('.oe_clear_search').show(); + }, + _showNoResults: function(searchValue) { - // Hide all data rows this.$el.find('.o_data_row').hide(); - - // Remove existing no results message this.$el.find('.oe_no_results').remove(); - // Add no results message var colspan = this.$el.find('thead tr:first th').length || 1; var noResultsHtml = '' + '' + '' + '

' + _t('No records found') + '

' + - '

' + _t('No results for: ') + '' + _.escape(searchValue) + '

' + - '

' + _t('Try different keywords or check your filters') + '

' + + '

' + _t('Search term: ') + '' + _.escape(searchValue) + '

' + ''; this.$el.find('tbody').append(noResultsHtml); }, @@ -436,69 +514,62 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { _clearSearch: function() { var self = this; - // Clear input this.$el.find('.oe_search_input').val(''); this.$el.find('.oe_clear_search').hide(); this.$el.find('.oe_search_count').hide(); this.$el.find('.oe_search_loading').hide(); - - // Remove no results message this.$el.find('.oe_no_results').remove(); - // Reset search state this._currentSearchValue = ''; this._searchMode = false; - // If we have original records, restore them + // Restore original records if (this._originalRecords && this._originalRecords.length > 0) { this._setCurrentRecords(this._originalRecords); - // Wrap _renderBody in try-catch for safety - try { - var renderResult = this._renderBody(); - if (renderResult && typeof renderResult.then === 'function') { - renderResult.then(function() { - self._originalRecords = null; - }).catch(function(error) { - console.error('Error restoring original records:', error); - // Just show all rows as fallback + if (typeof this._renderBody === 'function') { + try { + var renderResult = this._renderBody(); + if (renderResult && typeof renderResult.then === 'function') { + renderResult.then(function() { + self._originalRecords = null; + }).catch(function(error) { + console.error('Clear search render error:', error); + self.$el.find('.o_data_row').show(); + self._originalRecords = null; + }); + } else { self.$el.find('.o_data_row').show(); self._originalRecords = null; - }); - } else { - // If _renderBody doesn't return a promise + } + } catch (error) { + console.error('Clear search error:', error); self.$el.find('.o_data_row').show(); self._originalRecords = null; } - } catch (error) { - console.error('Error calling _renderBody:', error); - // Just show all rows as fallback + } else { self.$el.find('.o_data_row').show(); self._originalRecords = null; } } else { - // Just show all rows this.$el.find('.o_data_row').show(); } }, _normalizeArabic: function(text) { - if (!text) return text; + if (!text || typeof text !== 'string') return ''; - // Normalizing Arabic text by removing common variations - // Don't lowercase here as it's for the search query return text .replace(/[\u064B-\u065F]/g, '') // Remove diacritics - .replace(/[\u0660-\u0669]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030)) // Arabic-Indic digits - .replace(/[\u06F0-\u06F9]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030)) // Extended Arabic-Indic - .replace(/[\u0622\u0623\u0625\u0627]/g, 'ا') // Normalize Alef - .replace(/[\u0629]/g, 'ه') // Teh Marbuta to Heh - .replace(/[\u064A\u0626\u0649]/g, 'ي') // Normalize Yeh - .replace(/[\u0624\u0648]/g, 'و'); // Normalize Waw + .replace(/[\u0660-\u0669]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030)) + .replace(/[\u06F0-\u06F9]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030)) + .replace(/[\u0622\u0623\u0625\u0627]/g, 'ا') + .replace(/[\u0629]/g, 'ه') + .replace(/[\u064A\u0626\u0649]/g, 'ي') + .replace(/[\u0624\u0648]/g, 'و'); }, destroy: function () { - // Clear timeout if exists if (this._searchTimeout) { clearTimeout(this._searchTimeout); } From 4271c1c0e69c88f8e12aafb64b48d76c081d7c94 Mon Sep 17 00:00:00 2001 From: Mohamed Eltayar <152964073+maltayyar2@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:02:47 +0300 Subject: [PATCH 4/4] Complete rewrite with careful attention to every detail - proper module name, domain handling, state management --- .../static/src/js/list_search.js | 1019 +++++++++-------- 1 file changed, 570 insertions(+), 449 deletions(-) diff --git a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js index a6d26f1af..1a00f019e 100644 --- a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js +++ b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js @@ -5,576 +5,697 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { var core = require('web.core'); var _t = core._t; var session = require('web.session'); + var pyUtils = require('web.py_utils'); + var Domain = require('web.Domain'); ListRenderer.include({ - events: _.extend({ - 'keyup .oe_search_input': '_onKeyUp', - 'click .oe_clear_search': '_onClearSearch' - }, ListRenderer.prototype.events), + events: _.extend({}, ListRenderer.prototype.events, { + 'keyup .oe_search_input': '_onSearchKeyUp', + 'click .oe_clear_search': '_onClearSearchClick' + }), + /** + * @override + */ _renderView: function () { var self = this; return this._super.apply(this, arguments).then(function () { - self.$('.o_list_table').addClass('o_list_table_ungrouped'); - if (self.arch && self.arch.tag === 'tree' && self.$el && self.$el.hasClass('o_list_view')) { - // Check if the search input already exists - if (!self.$el.find('.oe_search_input').length) { - var search = '
' + - '' + - '' + - '' + - '' + - '
'; - self.$el.find('table').addClass('oe_table_search'); - self.$el.prepend($(search)); - - // Initialize search functionality - self._initializeSearch(); - } + // Add search box only to list views + if (self._shouldAddSearchBox()) { + self._addSearchBox(); } - }).catch(function(error) { - console.error('Error in _renderView:', error); - return $.when(); }); }, - _initializeSearch: function() { - this._searchTimeout = null; - this._currentSearchValue = ''; - this._isSearching = false; - this._originalRecords = null; - this._searchMode = false; + /** + * Check if we should add search box + */ + _shouldAddSearchBox: function() { + return ( + this.arch && + this.arch.tag === 'tree' && + this.$el && + this.$el.hasClass('o_list_view') && + !this.$el.find('.oe_search_input').length + ); }, - _onKeyUp: function (event) { + /** + * Add search box to the view + */ + _addSearchBox: function() { + var searchHtml = this._getSearchBoxHtml(); + this.$el.prepend($(searchHtml)); + this._initializeSearchState(); + }, + + /** + * Get search box HTML + */ + _getSearchBoxHtml: function() { + return '
' + + '' + + '' + + '' + + '' + + '
'; + }, + + /** + * Initialize search state + */ + _initializeSearchState: function() { + this._searchState = { + timeout: null, + currentValue: '', + isSearching: false, + originalData: null, + lastSearchDomain: null + }; + }, + + /** + * Handle search input keyup + */ + _onSearchKeyUp: function(event) { var self = this; var value = $(event.currentTarget).val(); // Clear previous timeout - if (this._searchTimeout) { - clearTimeout(this._searchTimeout); + if (this._searchState && this._searchState.timeout) { + clearTimeout(this._searchState.timeout); + } + + // Initialize state if needed + if (!this._searchState) { + this._initializeSearchState(); } // Store current value - this._currentSearchValue = value; + this._searchState.currentValue = value; - // Add delay to avoid too many requests - this._searchTimeout = setTimeout(function() { - if (!self._isSearching) { - self._performSearch(value); - } + // Debounce search + this._searchState.timeout = setTimeout(function() { + self._executeSearch(value); }, 500); }, - _onClearSearch: function(event) { + /** + * Handle clear button click + */ + _onClearSearchClick: function(event) { this._clearSearch(); }, - _performSearch: function (searchValue) { + /** + * Execute the search + */ + _executeSearch: function(searchValue) { var self = this; var value = (searchValue || '').trim(); + console.log('=== Starting Search ==='); + console.log('Search value:', value); + // Prevent concurrent searches - if (this._isSearching) { + if (this._searchState && this._searchState.isSearching) { + console.log('Search already in progress, skipping...'); return; } - // Show/hide clear button - this.$el.find('.oe_clear_search').toggle(!!value); + // Update UI + this.$('.oe_clear_search').toggle(!!value); + // Clear search if empty if (!value) { this._clearSearch(); return; } - // Check if we have required state + // Validate environment + if (!this._validateSearchEnvironment()) { + return; + } + + // Start search + this._searchState.isSearching = true; + this._showLoading(); + + // Store original data if first search + if (!this._searchState.originalData) { + this._searchState.originalData = this._captureCurrentData(); + } + + // Build and execute search + this._buildAndExecuteSearch(value); + }, + + /** + * Validate that we can perform search + */ + _validateSearchEnvironment: function() { + // Check model if (!this.state || !this.state.model) { - console.error('Missing state or model information'); - this._showError(_t('Search not available in this view')); - return; + console.error('No model found in state'); + this._showError(_t('Cannot search: No model information')); + return false; } - // Show loading indicator - this.$el.find('.oe_search_loading').show(); - this.$el.find('.oe_search_count').hide(); - this._isSearching = true; - - // Store original records if not already stored - var currentRecords = this._getCurrentRecords(); - if (!this._originalRecords && currentRecords) { - this._originalRecords = currentRecords.slice(); + // Check if we have field information + var fields = this._getFieldsInfo(); + if (!fields) { + console.error('No fields information found'); + this._showError(_t('Cannot search: No fields information')); + return false; } - // Get model and fields info - var model = this.state.model; - var fields = this._getSearchableFields(); - - if (!fields || fields.length === 0) { - this._isSearching = false; - this.$el.find('.oe_search_loading').hide(); - console.warn('No searchable fields found for model:', model); - - // Fallback to client-side search if no searchable fields - this._performClientSideSearch(value); - return; - } - - // Build search domain - var searchDomain = this._buildSearchDomain(value, fields); - - if (!searchDomain || searchDomain.length === 0) { - this._isSearching = false; - this.$el.find('.oe_search_loading').hide(); - this._showError(_t('Unable to build search query')); - return; - } - - // Get base domain - var baseDomain = []; - if (this.state.domain && Array.isArray(this.state.domain)) { - baseDomain = this.state.domain.slice(); - } - - // Combine domains - var finalDomain = baseDomain.concat(searchDomain); - - // Get fields to read - var fieldsToRead = this._getFieldsToRead(); - if (!fieldsToRead || fieldsToRead.length === 0) { - fieldsToRead = ['id', 'display_name']; - } - - // Build order - var orderBy = this._getOrderBy(); - - // Clean context - remove restrictions that might limit search - var searchContext = _.extend({}, this.state.context || session.user_context, { - active_test: false, // Include inactive records - bin_size: true // Optimize binary fields - }); - - console.log('=== Search Debug Info ==='); - console.log('Model:', model); - console.log('Search value:', value); - console.log('Search domain:', JSON.stringify(finalDomain)); - console.log('Fields to read:', fieldsToRead); - console.log('Context:', searchContext); - - // Perform RPC search - this._rpc({ - model: model, - method: 'search_read', - args: [finalDomain], - kwargs: { - fields: fieldsToRead, - limit: false, - offset: 0, - context: searchContext, - order: orderBy - } - }).then(function(result) { - console.log('Search completed. Found:', result.length, 'records'); - if (result.length > 0) { - console.log('First result example:', result[0]); - } - self._updateListWithSearchResults(result, value); - }).catch(function(error) { - console.error('Search RPC error:', error); - self._showError(_t('Search failed. Error: ') + (error.message || error.toString())); - - // Fallback to client-side search - console.log('Falling back to client-side search...'); - self._performClientSideSearch(value); - }).finally(function() { - self._isSearching = false; - self.$el.find('.oe_search_loading').hide(); - self.$el.find('.oe_search_input').val(self._currentSearchValue); - }); + return true; }, - _performClientSideSearch: function(value) { - // Fallback: Client-side search in visible records - console.log('Performing client-side search for:', value); - - var normalizedValue = this._normalizeArabic(value).toLowerCase(); - var visibleCount = 0; - var totalCount = 0; - - this.$el.find('.o_data_row').each(function() { - var $row = $(this); - totalCount++; - - var rowText = $row.text().toLowerCase(); - var normalizedRowText = this._normalizeArabic(rowText).toLowerCase(); - - var isMatch = rowText.indexOf(value.toLowerCase()) > -1 || - normalizedRowText.indexOf(normalizedValue) > -1; - - $row.toggle(isMatch); - if (isMatch) visibleCount++; - }.bind(this)); - - var $countEl = this.$el.find('.oe_search_count'); - if (visibleCount > 0) { - $countEl.text(_t('Found: ') + visibleCount + '/' + totalCount + _t(' visible records')).show(); - } else { - $countEl.text(_t('No matches in visible records')).show(); + /** + * Get fields information from various possible locations + */ + _getFieldsInfo: function() { + // Try different locations where fields might be stored + if (this.state && this.state.fields) { + return this.state.fields; } - }, - - _getCurrentRecords: function() { - // Try different locations where records might be stored - if (this.state && this.state.data && this.state.data.records) { - return this.state.data.records; + if (this.state && this.state.fieldsInfo && this.state.fieldsInfo.list) { + return this.state.fieldsInfo.list; } - if (this.state && this.state.records) { - return this.state.records; - } - if (this.recordsData) { - return this.recordsData; - } - if (this.records) { - return this.records; + if (this.fieldsInfo && this.fieldsInfo.list) { + return this.fieldsInfo.list; } return null; }, - _setCurrentRecords: function(records) { - // Set records in the appropriate location + /** + * Capture current data state + */ + _captureCurrentData: function() { + // Try to get current records from various locations if (this.state && this.state.data) { - this.state.data.records = records; - } else if (this.state) { - if (!this.state.data) { - this.state.data = {}; - } - this.state.data.records = records; + return { + records: this.state.data.records ? this.state.data.records.slice() : [], + count: this.state.count || 0, + domain: this.state.domain ? this.state.domain.slice() : [] + }; } + return null; }, - _getOrderBy: function() { - var orderBy = false; - if (this.state && this.state.orderedBy && Array.isArray(this.state.orderedBy) && this.state.orderedBy.length > 0) { - var order = this.state.orderedBy[0]; - if (order && order.name) { - orderBy = order.name; - if (order.asc !== undefined) { - orderBy += order.asc ? ' ASC' : ' DESC'; - } - } - } - return orderBy; - }, - - _getFieldsToRead: function() { + /** + * Build and execute the search + */ + _buildAndExecuteSearch: function(value) { var self = this; - var fields = ['id']; - // Check if columns exist - if (!this.columns || !Array.isArray(this.columns)) { - console.warn('No columns found in view'); - return ['id', 'display_name', 'name']; + // Get searchable fields + var searchableFields = this._identifySearchableFields(); + console.log('Searchable fields:', searchableFields); + + if (!searchableFields || searchableFields.length === 0) { + console.warn('No searchable fields found'); + this._fallbackToClientSearch(value); + return; } - // Check if state.fields exists - if (!this.state || !this.state.fields) { - console.warn('No fields metadata found'); - return ['id', 'display_name', 'name']; + // Build search domain + var searchDomain = this._constructSearchDomain(value, searchableFields); + console.log('Search domain:', JSON.stringify(searchDomain)); + + if (!searchDomain || searchDomain.length === 0) { + console.warn('Could not build search domain'); + this._fallbackToClientSearch(value); + return; } - // Get visible fields from columns - _.each(this.columns, function(column) { - if (column && !column.invisible && column.attrs && column.attrs.name) { - var fieldName = column.attrs.name; - var field = self.state.fields[fieldName]; - - if (field) { - // Skip computed non-stored fields - if (field.compute && !field.store) { - console.log('Skipping computed non-stored field:', fieldName); - return; - } - - fields.push(fieldName); - } - } - }); + // Store last search domain + this._searchState.lastSearchDomain = searchDomain; - // Add essential fields if available - if (self.state.fields.display_name && !fields.includes('display_name')) { - fields.push('display_name'); - } - if (self.state.fields.name && !fields.includes('name')) { - fields.push('name'); - } + // Get base domain and combine + var baseDomain = this._getBaseDomain(); + var finalDomain = this._combineDomains(baseDomain, searchDomain); + console.log('Final domain:', JSON.stringify(finalDomain)); - return _.uniq(fields); + // Execute RPC + this._executeRPCSearch(finalDomain, value); }, - _buildSearchDomain: function(value, fields) { - if (!value || !fields || fields.length === 0) { + /** + * Get base domain from state + */ + _getBaseDomain: function() { + if (this.state && this.state.domain) { + // Clone the domain to avoid mutations + if (Array.isArray(this.state.domain)) { + return this.state.domain.slice(); + } + if (typeof this.state.domain === 'string') { + try { + return pyUtils.eval('domain', this.state.domain); + } catch (e) { + console.error('Error parsing domain:', e); + return []; + } + } + } + return []; + }, + + /** + * Combine base and search domains + */ + _combineDomains: function(baseDomain, searchDomain) { + if (!baseDomain || baseDomain.length === 0) { + return searchDomain; + } + if (!searchDomain || searchDomain.length === 0) { + return baseDomain; + } + + // Combine with AND operator + return ['&'].concat(baseDomain).concat(searchDomain); + }, + + /** + * Identify searchable fields + */ + _identifySearchableFields: function() { + var self = this; + var fields = []; + var fieldsInfo = this._getFieldsInfo(); + + if (!fieldsInfo) { return []; } - var domain = []; - var orConditions = []; + // First try to get fields from visible columns + if (this.columns && Array.isArray(this.columns)) { + this.columns.forEach(function(column) { + if (column && !column.invisible && column.attrs && column.attrs.name) { + var fieldName = column.attrs.name; + var fieldInfo = fieldsInfo[fieldName]; + + if (fieldInfo && self._isFieldSearchable(fieldInfo)) { + fields.push({ + name: fieldName, + type: fieldInfo.type, + string: fieldInfo.string || fieldName + }); + } + } + }); + } - // Normalize for Arabic - var normalizedValue = this._normalizeArabic(value); + // If no fields from columns, try common fields + if (fields.length === 0) { + ['name', 'display_name', 'reference', 'code'].forEach(function(fieldName) { + var fieldInfo = fieldsInfo[fieldName]; + if (fieldInfo && self._isFieldSearchable(fieldInfo)) { + fields.push({ + name: fieldName, + type: fieldInfo.type, + string: fieldInfo.string || fieldName + }); + } + }); + } - _.each(fields, function(field) { - if (!field || !field.name || !field.type) { - return; - } - - // Text fields - if (['char', 'text', 'html', 'selection'].includes(field.type)) { - orConditions.push([field.name, 'ilike', value]); - if (normalizedValue && normalizedValue !== value) { - orConditions.push([field.name, 'ilike', normalizedValue]); + return fields; + }, + + /** + * Check if field is searchable + */ + _isFieldSearchable: function(fieldInfo) { + // Skip computed non-stored fields + if (fieldInfo.compute && !fieldInfo.store) { + return false; + } + // Skip non-searchable fields + if (fieldInfo.searchable === false) { + return false; + } + // Skip binary fields + if (['binary', 'image'].includes(fieldInfo.type)) { + return false; + } + return true; + }, + + /** + * Construct search domain + */ + _constructSearchDomain: function(value, fields) { + var conditions = []; + var self = this; + + // Escape special characters for safe search + var safeValue = this._escapeSearchValue(value); + var normalizedValue = this._normalizeArabic(safeValue); + + fields.forEach(function(field) { + // Text-based fields + if (['char', 'text', 'html'].includes(field.type)) { + conditions.push([field.name, 'ilike', safeValue]); + if (normalizedValue && normalizedValue !== safeValue) { + conditions.push([field.name, 'ilike', normalizedValue]); } } - // Many2one fields - special handling - else if (field.type === 'many2one') { - // Search by ID if numeric - if (!isNaN(value)) { - orConditions.push([field.name, '=', parseInt(value)]); - } - // Note: searching text in many2one requires special syntax - // We'll search by ID only for now + // Selection fields + else if (field.type === 'selection') { + conditions.push([field.name, 'ilike', safeValue]); } // Number fields else if (['integer', 'float', 'monetary'].includes(field.type)) { - if (!isNaN(value) && value !== '') { - if (field.type === 'integer') { - orConditions.push([field.name, '=', parseInt(value)]); - } else { - orConditions.push([field.name, '=', parseFloat(value)]); - } + var numValue = parseFloat(value); + if (!isNaN(numValue)) { + conditions.push([field.name, '=', numValue]); } } - // Boolean fields - else if (field.type === 'boolean') { - var boolValue = value.toLowerCase(); - if (['true', 'yes', '1', 'نعم', 'صح'].includes(boolValue)) { - orConditions.push([field.name, '=', true]); - } else if (['false', 'no', '0', 'لا', 'خطأ'].includes(boolValue)) { - orConditions.push([field.name, '=', false]); + // Many2one - only search by ID for now + else if (field.type === 'many2one') { + var intValue = parseInt(value); + if (!isNaN(intValue)) { + conditions.push([field.name, '=', intValue]); } } }); // Build OR domain - if (orConditions.length > 0) { - for (var i = 1; i < orConditions.length; i++) { - domain.push('|'); - } - domain = domain.concat(orConditions); - } - - return domain; - }, - - _getSearchableFields: function() { - var self = this; - var fields = []; - var addedFields = {}; - - // Check requirements - if (!this.state || !this.state.fields) { - console.warn('No state.fields available'); + if (conditions.length === 0) { return []; } - - // Get fields from columns if available - if (this.columns && Array.isArray(this.columns)) { - _.each(this.columns, function(column) { - if (column && !column.invisible && column.attrs && column.attrs.name) { - var fieldName = column.attrs.name; - var field = self.state.fields[fieldName]; - - if (field && !addedFields[fieldName]) { - // Check if searchable - var isSearchable = ( - field.searchable !== false && - (field.store !== false || field.type === 'many2one') && - (!field.compute || field.store) - ); - - if (isSearchable) { - fields.push({ - name: fieldName, - type: field.type, - string: field.string || fieldName - }); - addedFields[fieldName] = true; - } - } - } - }); + if (conditions.length === 1) { + return conditions[0]; } - // Add common fields if not enough - if (fields.length < 3) { - var commonFields = ['name', 'display_name', 'reference', 'code', 'ref', 'description']; - _.each(commonFields, function(fieldName) { - var field = self.state.fields[fieldName]; - if (field && !addedFields[fieldName]) { - if (field.searchable !== false && field.store !== false && (!field.compute || field.store)) { - fields.push({ - name: fieldName, - type: field.type, - string: field.string || fieldName - }); - addedFields[fieldName] = true; - } - } - }); + // Add OR operators + var domain = []; + for (var i = 1; i < conditions.length; i++) { + domain.push('|'); } - - console.log('Searchable fields found:', fields.length, 'fields'); - return fields; + return domain.concat(conditions); }, - _updateListWithSearchResults: function(records, searchValue) { + /** + * Escape special characters in search value + */ + _escapeSearchValue: function(value) { + // Remove dangerous characters that might break domain + return value.replace(/[%_\\]/g, '\\$&'); + }, + + /** + * Execute RPC search + */ + _executeRPCSearch: function(domain, searchValue) { var self = this; + var model = this.state.model; + var fields = this._getFieldsToRead(); - if (!records) { - records = []; + console.log('Executing RPC search...'); + console.log('Model:', model); + console.log('Fields to read:', fields); + + this._rpc({ + model: model, + method: 'search_read', + args: [domain], + kwargs: { + fields: fields, + limit: false, + offset: 0, + context: this._getSearchContext() + } + }).then(function(result) { + console.log('Search completed. Found:', result.length, 'records'); + self._handleSearchResults(result, searchValue); + }).catch(function(error) { + console.error('RPC Error:', error); + self._handleSearchError(error, searchValue); + }).finally(function() { + self._searchState.isSearching = false; + self._hideLoading(); + }); + }, + + /** + * Get fields to read + */ + _getFieldsToRead: function() { + var fields = ['id']; + var fieldsInfo = this._getFieldsInfo(); + + if (!fieldsInfo) { + return ['id', 'display_name']; } + // Add visible column fields + if (this.columns) { + this.columns.forEach(function(column) { + if (column && !column.invisible && column.attrs && column.attrs.name) { + var fieldName = column.attrs.name; + var fieldInfo = fieldsInfo[fieldName]; + + // Skip computed non-stored fields + if (!fieldInfo || (fieldInfo.compute && !fieldInfo.store)) { + return; + } + + fields.push(fieldName); + } + }); + } + + // Ensure we have display_name + if (!fields.includes('display_name') && fieldsInfo.display_name) { + fields.push('display_name'); + } + + return _.uniq(fields); + }, + + /** + * Get search context + */ + _getSearchContext: function() { + var context = _.extend({}, this.state.context || {}, session.user_context || {}); + // Remove active_test to include inactive records + delete context.active_test; + return context; + }, + + /** + * Handle search results + */ + _handleSearchResults: function(records, searchValue) { var count = records.length; - var $countEl = this.$el.find('.oe_search_count'); if (count > 0) { - var message = _t('Found: ') + count + _t(' records'); - if (count > 1000) { - message += ' ' + _t('(Large result set)'); - } - $countEl.text(message).show(); - - this._setCurrentRecords(records); - this._searchMode = true; - - // Try to render if method exists - if (typeof this._renderBody === 'function') { - try { - var renderResult = this._renderBody(); - if (renderResult && typeof renderResult.then === 'function') { - renderResult.then(function() { - self._restoreSearchUI(); - }).catch(function(error) { - console.error('Render error:', error); - self._restoreSearchUI(); - }); - } else { - self._restoreSearchUI(); - } - } catch (error) { - console.error('Render error:', error); - self._restoreSearchUI(); - } - } else { - console.warn('_renderBody method not found'); - self._restoreSearchUI(); - } + this._updateView(records); + this._showResultCount(count); } else { - $countEl.text(_t('No records found')).show(); this._showNoResults(searchValue); } }, - _restoreSearchUI: function() { - this.$el.find('.oe_search_input').val(this._currentSearchValue); - this.$el.find('.oe_clear_search').show(); + /** + * Handle search error + */ + _handleSearchError: function(error, searchValue) { + console.error('Search failed, falling back to client search'); + this._showError(_t('Server search failed. Searching in visible records...')); + this._fallbackToClientSearch(searchValue); }, - _showNoResults: function(searchValue) { - this.$el.find('.o_data_row').hide(); - this.$el.find('.oe_no_results').remove(); + /** + * Update view with search results + */ + _updateView: function(records) { + // This is the tricky part - we need to update the view + // Different versions of Odoo handle this differently - var colspan = this.$el.find('thead tr:first th').length || 1; - var noResultsHtml = '' + - '' + - '' + - '

' + _t('No records found') + '

' + - '

' + _t('Search term: ') + '' + _.escape(searchValue) + '

' + - ''; - this.$el.find('tbody').append(noResultsHtml); - }, - - _showError: function(message) { - var $countEl = this.$el.find('.oe_search_count'); - $countEl.text(message).css('color', '#dc3545').show(); + if (this.state && this.state.data) { + this.state.data.records = records; + } - setTimeout(function() { - $countEl.css('color', '#6c757d'); - }, 3000); - }, - - _clearSearch: function() { - var self = this; - - this.$el.find('.oe_search_input').val(''); - this.$el.find('.oe_clear_search').hide(); - this.$el.find('.oe_search_count').hide(); - this.$el.find('.oe_search_loading').hide(); - this.$el.find('.oe_no_results').remove(); - - this._currentSearchValue = ''; - this._searchMode = false; - - // Restore original records - if (this._originalRecords && this._originalRecords.length > 0) { - this._setCurrentRecords(this._originalRecords); - - if (typeof this._renderBody === 'function') { - try { - var renderResult = this._renderBody(); - if (renderResult && typeof renderResult.then === 'function') { - renderResult.then(function() { - self._originalRecords = null; - }).catch(function(error) { - console.error('Clear search render error:', error); - self.$el.find('.o_data_row').show(); - self._originalRecords = null; - }); - } else { - self.$el.find('.o_data_row').show(); - self._originalRecords = null; - } - } catch (error) { - console.error('Clear search error:', error); - self.$el.find('.o_data_row').show(); - self._originalRecords = null; - } - } else { - self.$el.find('.o_data_row').show(); - self._originalRecords = null; + // Try to re-render + if (typeof this._renderBody === 'function') { + try { + this._renderBody(); + } catch (e) { + console.error('Error rendering body:', e); } - } else { - this.$el.find('.o_data_row').show(); } }, - _normalizeArabic: function(text) { - if (!text || typeof text !== 'string') return ''; + /** + * Fallback to client-side search + */ + _fallbackToClientSearch: function(value) { + console.log('Performing client-side search...'); - return text + var searchLower = value.toLowerCase(); + var normalizedSearch = this._normalizeArabic(searchLower); + var visibleCount = 0; + + this.$('.o_data_row').each(function() { + var $row = $(this); + var text = $row.text().toLowerCase(); + var normalizedText = this._normalizeArabic(text); + + var match = text.includes(searchLower) || normalizedText.includes(normalizedSearch); + $row.toggle(match); + if (match) visibleCount++; + }.bind(this)); + + this._showResultCount(visibleCount, true); + }, + + /** + * Clear search + */ + _clearSearch: function() { + console.log('Clearing search...'); + + // Clear UI + this.$('.oe_search_input').val(''); + this.$('.oe_clear_search').hide(); + this.$('.oe_search_count').hide(); + this.$('.oe_no_results').remove(); + + // Reset state + if (this._searchState) { + this._searchState.currentValue = ''; + this._searchState.lastSearchDomain = null; + + // Restore original data + if (this._searchState.originalData) { + this._restoreOriginalData(); + } + } + + // Show all rows + this.$('.o_data_row').show(); + }, + + /** + * Restore original data + */ + _restoreOriginalData: function() { + if (!this._searchState || !this._searchState.originalData) { + return; + } + + var originalData = this._searchState.originalData; + + if (this.state && this.state.data) { + this.state.data.records = originalData.records; + } + + // Re-render if possible + if (typeof this._renderBody === 'function') { + try { + this._renderBody(); + } catch (e) { + console.error('Error restoring view:', e); + } + } + + this._searchState.originalData = null; + }, + + /** + * Show loading indicator + */ + _showLoading: function() { + this.$('.oe_search_loading').show(); + this.$('.oe_search_count').hide(); + }, + + /** + * Hide loading indicator + */ + _hideLoading: function() { + this.$('.oe_search_loading').hide(); + }, + + /** + * Show result count + */ + _showResultCount: function(count, isClientSide) { + var message = _t('Found: ') + count + _t(' records'); + if (isClientSide) { + message = _t('Showing: ') + count + _t(' visible records'); + } + this.$('.oe_search_count').text(message).show(); + }, + + /** + * Show no results message + */ + _showNoResults: function(searchValue) { + this.$('.o_data_row').hide(); + this.$('.oe_no_results').remove(); + + var $tbody = this.$('tbody'); + var colspan = this.$('thead tr:first th').length || 1; + + var $noResults = $('' + + '' + + '' + + '

' + _t('No results found') + '

' + + '

' + _.escape(searchValue) + '

' + + ''); + + $tbody.append($noResults); + this.$('.oe_search_count').text(_t('No records found')).show(); + }, + + /** + * Show error message + */ + _showError: function(message) { + this.$('.oe_search_count').text(message).css('color', 'red').show(); + setTimeout(function() { + this.$('.oe_search_count').css('color', ''); + }.bind(this), 3000); + }, + + /** + * Normalize Arabic text + */ + _normalizeArabic: function(text) { + if (!text) return ''; + + return String(text) .replace(/[\u064B-\u065F]/g, '') // Remove diacritics - .replace(/[\u0660-\u0669]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030)) - .replace(/[\u06F0-\u06F9]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030)) + .replace(/[\u0660-\u0669]/g, function(d) { + return String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030); + }) + .replace(/[\u06F0-\u06F9]/g, function(d) { + return String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030); + }) .replace(/[\u0622\u0623\u0625\u0627]/g, 'ا') .replace(/[\u0629]/g, 'ه') .replace(/[\u064A\u0626\u0649]/g, 'ي') .replace(/[\u0624\u0648]/g, 'و'); }, - destroy: function () { - if (this._searchTimeout) { - clearTimeout(this._searchTimeout); + /** + * @override + */ + destroy: function() { + if (this._searchState && this._searchState.timeout) { + clearTimeout(this._searchState.timeout); } return this._super.apply(this, arguments); } }); + return ListRenderer; });