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 = '
' +
- '' +
- '' +
- '' +
- ' ' + _t('Searching...') + '' +
- '
';
- 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 '
' +
+ '' +
+ '' +
+ '' +
+ '' +
+ ' ' + _t('Searching...') + '' +
+ '
';
+ },
+
+ /**
+ * 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;
});