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 6d620a886..589cb5795 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,6 +5,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { var core = require('web.core'); var _t = core._t; var concurrency = require('web.concurrency'); + var rpc = require('web.rpc'); ListRenderer.include({ events: _.extend({}, ListRenderer.prototype.events, { @@ -25,16 +26,17 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { }, /** - * @override + * @override - FIXED: Single _renderView method with complete logic */ _renderView: function () { var self = this; - // Store current search value to restore after render + // Store current search state to restore after render var currentSearchValue = this._search ? this._search.value : ''; var wasFiltered = this._search && this._search.isFiltered; + var originalFilteredCount = this._search ? this._search.filteredCount : 0; - return this._super.apply(this, arguments).then(function () { + return this._super.apply(this, arguments).then(function (result) { // Add search box if this is a tree view if (self.arch && self.arch.tag === 'tree' && self.$el && self.$el.hasClass('o_list_view')) { @@ -44,22 +46,19 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { self._addSearchBox(); } - // Restore search UI state - self._restoreSearchUI(); - - // If there was a search active, restore the search state - // but don't re-trigger the search to avoid infinite loops + // Restore search UI state without re-triggering search if (currentSearchValue && wasFiltered) { console.log('Restoring search state for:', currentSearchValue); - self._search.value = currentSearchValue; - self._search.isFiltered = true; - // Show UI elements - self.$('.oe_clear_search').show(); - if (self._search.filteredCount) { - self.$('.oe_search_count').text(_t('Found: ') + self._search.filteredCount + _t(' records')).show(); - } + self._restoreSearchUIState(currentSearchValue, originalFilteredCount); + } + + // Update search count after Odoo's native rendering if we're in search mode + if (self._search && self._search.isFiltered && self._search.value) { + self._updateSearchCount(); } } + + return result; }); }, @@ -72,9 +71,9 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { } var html = '
' + - '' + + '' + '' + - '' + + '' + '' + '
'; this.$el.prepend($(html)); @@ -98,24 +97,29 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { }, /** - * Restore search UI after render + * Restore search UI state after render - SIMPLIFIED */ - _restoreSearchUI: function() { - if (!this._search) return; - + _restoreSearchUIState: function(searchValue, filteredCount) { var $input = this.$('.oe_search_input'); var $clearBtn = this.$('.oe_clear_search'); var $count = this.$('.oe_search_count'); // Restore input value - if ($input.length && this._search.value) { - $input.val(this._search.value); + if ($input.length && searchValue) { + $input.val(searchValue); $clearBtn.show(); - } - - // Restore count if filtered - if (this._search.isFiltered && this._search.filteredCount) { - $count.text(_t('Found: ') + this._search.filteredCount + _t(' records')).show(); + + // Restore search state + this._search.value = searchValue; + this._search.isFiltered = true; + + // Show count if available + if (filteredCount) { + this._search.filteredCount = filteredCount; + $count.text(_t('Found: ') + filteredCount + _t(' records')).show(); + } else { + $count.text(_t('Searching...')).show(); + } } }, @@ -127,7 +131,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { }, /** - * Handle search input keyup - FIXED LOGIC + * Handle search input keyup - ENHANCED LOGIC */ _onSearchKeyUp: function(e) { var self = this; @@ -138,15 +142,14 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { clearTimeout(this._search.timer); } - // Update UI + // Update UI immediately this.$('.oe_clear_search').toggle(!!value); // Store value this._search.value = value; - // CRITICAL FIX: Always trigger search, even for empty values + // ENHANCED: Always trigger search with appropriate delay this._search.timer = setTimeout(function() { - // This will handle both search and clear cases properly self._performSearch(value); }, 500); }, @@ -159,7 +162,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { }, /** - * REDESIGNED: Perform search using proper Odoo mechanisms - THE CORRECT WAY + * ENHANCED: Perform search using proper Odoo mechanisms */ _performSearch: function(value) { var self = this; @@ -170,17 +173,17 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { return Promise.resolve(); } - console.log('=== PERFORM SEARCH ==='); + console.log('=== ENHANCED PERFORM SEARCH ==='); console.log('Search value:', value); console.log('Value length:', value ? value.length : 0); return this._searchMutex.exec(function() { self._searchInProgress = true; - // FIXED: Handle empty search by calling clear search + // Handle empty search by clearing filters if (!value || value.length === 0) { - console.log('Empty search - clearing filters using Odoo method'); - return self._clearSearchOdooWay().finally(function() { + console.log('Empty search - clearing all filters'); + return self._clearSearchInternal().finally(function() { self._searchInProgress = false; }); } @@ -192,7 +195,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { return Promise.resolve(); } - console.log('Starting search using Odoo trigger_up method'); + console.log('Starting enhanced search for model:', self.state.model); // Store original domain only once if (!self._search.originalDomain && !self._search.isFiltered) { @@ -202,22 +205,22 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { // Show loading self._showLoading(true); - // THE CORRECT ODOO WAY: Use trigger_up with do_search - return self._searchUsingOdooMethod(value).finally(function() { + // ENHANCED: Use proper Odoo search with count + return self._searchWithCount(value).finally(function() { self._showLoading(false); self._searchInProgress = false; - console.log('=== Search completed ==='); + console.log('=== Enhanced search completed ==='); }); }); }, /** - * THE CORRECT ODOO METHOD: Use trigger_up to search + * ENHANCED: Search with accurate count using RPC */ - _searchUsingOdooMethod: function(value) { + _searchWithCount: function(value) { var self = this; - console.log('=== USING ODOO SEARCH METHOD ==='); + console.log('=== ENHANCED SEARCH WITH COUNT ==='); // Build search domain var searchDomain = this._buildSearchDomain(value); @@ -227,44 +230,60 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { } // Combine with original domain - var finalDomain = []; - if (this._search.originalDomain && this._search.originalDomain.length > 0) { - if (searchDomain.length > 0) { - finalDomain = ['&'].concat(this._search.originalDomain).concat(searchDomain); - } else { - finalDomain = this._search.originalDomain; - } - } else { - finalDomain = searchDomain; - } + var finalDomain = this._combineDomains(searchDomain); console.log('Final search domain:', finalDomain); - // THE CORRECT ODOO WAY: trigger_up with do_search - this.trigger_up('do_search', { - domain: finalDomain, - context: this.state.context || {}, - groupBy: this.state.groupedBy || [] + // STEP 1: Get accurate count using RPC + return rpc.query({ + model: this.state.model, + method: 'search_count', + args: [finalDomain], + context: this.state.context || {} + }).then(function(count) { + console.log('Accurate search count from server:', count); + + // Store count + self._search.filteredCount = count; + + // Update UI with accurate count + self.$('.oe_search_count').text(_t('Found: ') + count + _t(' records')).show(); + + // STEP 2: Apply search using trigger_up + self.trigger_up('do_search', { + domain: finalDomain, + context: self.state.context || {}, + groupBy: self.state.groupedBy || [] + }); + + // Update search state + self._search.isFiltered = true; + + return Promise.resolve(); + }).catch(function(error) { + console.error('Error getting search count:', error); + + // Fallback: still apply search without count + self.trigger_up('do_search', { + domain: finalDomain, + context: self.state.context || {}, + groupBy: self.state.groupedBy || [] + }); + + self._search.isFiltered = true; + self.$('.oe_search_count').text(_t('Search applied')).show(); + + return Promise.resolve(); }); - - // Update search state - this._search.isFiltered = true; - - // We can't get the count immediately since trigger_up is async - // The count will be updated when the view is re-rendered - // For now, show a generic "searching..." message - this.$('.oe_search_count').text(_t('Searching...')).show(); - - return Promise.resolve(); }, /** - * THE CORRECT ODOO METHOD: Clear search using trigger_up + * ENHANCED: Clear search using proper Odoo method */ - _clearSearchOdooWay: function() { + _clearSearchInternal: function() { var self = this; - console.log('=== CLEARING SEARCH USING ODOO METHOD ==='); + console.log('=== ENHANCED CLEAR SEARCH ==='); // Clear UI immediately this.$('.oe_search_input').val(''); @@ -276,7 +295,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { this._search.isFiltered = false; this._search.filteredCount = 0; - // THE CORRECT ODOO WAY: trigger_up with original domain + // Use original domain to clear search var originalDomain = this._search.originalDomain || []; this.trigger_up('do_search', { @@ -288,15 +307,15 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { // Clear stored original domain this._search.originalDomain = null; - console.log('Search cleared using Odoo method'); + console.log('Search cleared using enhanced method'); return Promise.resolve(); }, /** - * Clear search public method + * Public clear search method */ _clearSearch: function() { - return this._clearSearchOdooWay(); + return this._clearSearchInternal(); }, /** @@ -304,62 +323,100 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { */ _storeOriginalDomain: function() { if (this.state && this.state.domain && !this._search.originalDomain) { - this._search.originalDomain = this.state.domain.slice(); + // Deep copy the domain to avoid reference issues + this._search.originalDomain = JSON.parse(JSON.stringify(this.state.domain)); console.log('Stored original domain:', this._search.originalDomain); } }, /** - * Override _renderView to update search count after Odoo's native rendering + * ENHANCED: Combine search domain with original domain */ - _renderView: function () { - var self = this; - var promise = this._super.apply(this, arguments); + _combineDomains: function(searchDomain) { + var finalDomain = []; - // After rendering is complete, update the search count if we're in search mode - return promise.then(function() { - if (self._search && self._search.isFiltered && self._search.value) { - // Update count based on actual rendered rows - var visibleRows = self.$('.o_data_row:visible').length; - self._search.filteredCount = visibleRows; - self.$('.oe_search_count').text(_t('Found: ') + visibleRows + _t(' records')).show(); - - console.log('Updated search count after Odoo rendering:', visibleRows); + if (this._search.originalDomain && this._search.originalDomain.length > 0) { + if (searchDomain.length > 0) { + // Combine: originalDomain AND searchDomain + finalDomain = ['&']; + finalDomain = finalDomain.concat(this._search.originalDomain); + finalDomain = finalDomain.concat(searchDomain); + } else { + finalDomain = this._search.originalDomain.slice(); } - return promise; - }); + } else { + finalDomain = searchDomain.slice(); + } + + return finalDomain; }, /** - * Build search domain + * ENHANCED: Update search count after render + */ + _updateSearchCount: function() { + // If we have the accurate count from RPC, use it + if (this._search.filteredCount) { + this.$('.oe_search_count').text(_t('Found: ') + this._search.filteredCount + _t(' records')).show(); + } else { + // Fallback: count visible records (just for immediate feedback) + var visibleCount = this.state && this.state.data ? this.state.data.length : 0; + if (visibleCount > 0) { + this.$('.oe_search_count').text(_t('Showing: ') + visibleCount + _t(' records')).show(); + } + } + }, + + /** + * ENHANCED: Build search domain with better logic */ _buildSearchDomain: function(value) { var fields = this._getSearchableFields(); if (fields.length === 0) { + console.warn('No searchable fields found'); return []; } + console.log('Searching in fields:', fields.map(f => f.name + ' (' + f.type + ')')); + var conditions = []; var normalized = this._normalizeArabic(value); + // Build conditions for each field type fields.forEach(function(field) { - if (['char', 'text', 'html'].includes(field.type)) { - conditions.push([field.name, 'ilike', value]); - if (normalized !== value) { - conditions.push([field.name, 'ilike', normalized]); - } - } else if (['integer', 'float', 'monetary'].includes(field.type)) { - var num = parseFloat(value); - if (!isNaN(num)) { - conditions.push([field.name, '=', num]); - } - } else if (field.type === 'selection') { - conditions.push([field.name, 'ilike', value]); - } else if (field.type === 'many2one') { - conditions.push([field.name + '.name', 'ilike', value]); - if (normalized !== value) { - conditions.push([field.name + '.name', 'ilike', normalized]); + try { + if (['char', 'text', 'html'].includes(field.type)) { + // Text fields: use ilike for partial matching + conditions.push([field.name, 'ilike', value]); + if (normalized !== value) { + conditions.push([field.name, 'ilike', normalized]); + } + } else if (['integer', 'float', 'monetary'].includes(field.type)) { + // Numeric fields: exact match if value is numeric + var num = parseFloat(value); + if (!isNaN(num)) { + conditions.push([field.name, '=', num]); + } + } else if (field.type === 'selection') { + // Selection fields: search in selection values + conditions.push([field.name, 'ilike', value]); + } else if (field.type === 'many2one') { + // Many2one fields: search in related record's name + conditions.push([field.name + '.name', 'ilike', value]); + if (normalized !== value) { + conditions.push([field.name + '.name', 'ilike', normalized]); + } + } else if (field.type === 'boolean') { + // Boolean fields: match true/false/yes/no + var lowerValue = value.toLowerCase(); + if (['true', 'yes', 'نعم', '1'].includes(lowerValue)) { + conditions.push([field.name, '=', true]); + } else if (['false', 'no', 'لا', '0'].includes(lowerValue)) { + conditions.push([field.name, '=', false]); + } } + } catch (error) { + console.warn('Error processing field', field.name, ':', error); } }); @@ -372,7 +429,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { return conditions; } - // Add OR operators + // Add OR operators: |, |, |, condition1, condition2, condition3, condition4 var domain = []; for (var i = 1; i < conditions.length; i++) { domain.push('|'); @@ -381,39 +438,47 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { }, /** - * Get searchable fields + * ENHANCED: Get searchable fields with better logic */ _getSearchableFields: function() { var fields = []; var fieldDefs = this.state.fields || {}; - // Get from visible columns - if (this.columns) { + console.log('Available fields:', Object.keys(fieldDefs)); + console.log('Available columns:', this.columns ? this.columns.length : 0); + + // PRIORITY 1: Get from visible columns + if (this.columns && this.columns.length > 0) { this.columns.forEach(function(col) { if (!col.invisible && col.attrs && col.attrs.name) { - var field = fieldDefs[col.attrs.name]; - if (field && field.store !== false) { + var fieldName = col.attrs.name; + var field = fieldDefs[fieldName]; + if (field && field.store !== false && field.searchable !== false) { fields.push({ - name: col.attrs.name, - type: field.type + name: fieldName, + type: field.type, + string: field.string || fieldName }); } } }); } - // Add common searchable fields if none found + // PRIORITY 2: Add common searchable fields if none found from columns if (fields.length === 0) { - ['name', 'display_name', 'code', 'reference', 'description'].forEach(function(fname) { - if (fieldDefs[fname] && fieldDefs[fname].store !== false) { + var commonFields = ['name', 'display_name', 'code', 'reference', 'description', 'note']; + commonFields.forEach(function(fname) { + if (fieldDefs[fname] && fieldDefs[fname].store !== false && fieldDefs[fname].searchable !== false) { fields.push({ name: fname, - type: fieldDefs[fname].type + type: fieldDefs[fname].type, + string: fieldDefs[fname].string || fname }); } }); } + console.log('Final searchable fields:', fields); return fields; }, @@ -422,25 +487,35 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { */ _showLoading: function(show) { this.$('.oe_search_loading').toggle(show); + if (show) { + this.$('.oe_search_count').hide(); + } }, /** - * Normalize Arabic text for better searching + * ENHANCED: Normalize Arabic text for better searching */ _normalizeArabic: function(text) { if (!text) return ''; return text - .replace(/[\u064B-\u065F]/g, '') // Remove diacritics + // Remove diacritics (تشكيل) + .replace(/[\u064B-\u065F]/g, '') + // Convert Arabic-Indic digits to European digits .replace(/[\u0660-\u0669]/g, function(d) { return String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030); }) + // Convert Persian digits to European digits .replace(/[\u06F0-\u06F9]/g, function(d) { return String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030); }) + // Normalize Alef variations .replace(/[\u0622\u0623\u0625\u0627]/g, 'ا') + // Normalize Teh Marbuta .replace(/[\u0629]/g, 'ه') + // Normalize Yeh variations .replace(/[\u064A\u0626\u0649]/g, 'ي') + // Normalize Waw variations .replace(/[\u0624\u0648]/g, 'و'); }, @@ -451,6 +526,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { if (this._search && this._search.timer) { clearTimeout(this._search.timer); } + this._searchInProgress = false; return this._super.apply(this, arguments); } });