@@ -59,3 +59,135 @@ def action_unarchive(self):
5959 if registrants and not self ._check_archive_permission ():
6060 raise AccessError (_ ("You do not have the necessary permissions to unarchive registrants." ))
6161 return super ().action_unarchive ()
62+
63+ @api .model
64+ def get_search_config (self ):
65+ """Return registry search configuration for the JS portal."""
66+ get_param = self .env ["ir.config_parameter" ].sudo ().get_param # nosemgrep: odoo-sudo-without-context
67+ result_limit = int (get_param ("spp_registry_search.result_limit" , "50" ))
68+ min_chars = int (get_param ("spp_registry_search.min_chars" , "3" ))
69+ return {
70+ "search_mode" : get_param ("spp_registry_search.search_mode" , "unified" ),
71+ "target_field" : get_param ("spp_registry_search.target_field" , "name" ),
72+ "result_limit" : max (10 , min (200 , result_limit )),
73+ "min_chars" : max (1 , min (10 , min_chars )),
74+ }
75+
76+ @api .model
77+ def search_registrants (self , search_term , search_type = "all" , search_field = None , advanced_filters = None , limit = 50 ):
78+ """Optimized registrant search that respects configured search mode.
79+
80+ Instead of a single ORM domain with OR across JOINs, runs separate
81+ targeted queries per field and merges results. Each individual query
82+ can use indexes efficiently.
83+
84+ Args:
85+ search_term: The search string
86+ search_type: 'all', 'individuals', or 'groups'
87+ search_field: If targeted mode, which field to search ('name', 'id_number', 'phone', 'email')
88+ advanced_filters: Dict with optional date range filters
89+ limit: Maximum results to return
90+
91+ Returns:
92+ list: List of dicts with partner data
93+ """
94+ if not search_term :
95+ return []
96+
97+ # Read config with fallback defaults
98+ get_param = self .env ["ir.config_parameter" ].sudo ().get_param # nosemgrep: odoo-sudo-without-context
99+ config_limit = int (get_param ("spp_registry_search.result_limit" , "50" ))
100+ limit = max (10 , min (200 , limit or config_limit ))
101+
102+ search_mode = get_param ("spp_registry_search.search_mode" , "unified" )
103+
104+ # If targeted mode and a field is specified, only search that field
105+ if search_mode == "targeted" and search_field :
106+ partner_ids = self ._search_by_field (search_term , search_field , limit )
107+ else :
108+ # Unified mode: run separate queries per field and merge
109+ partner_ids = self ._search_unified (search_term , limit )
110+
111+ if not partner_ids :
112+ return []
113+
114+ # Build final domain with type and advanced filters
115+ domain = [("id" , "in" , list (partner_ids )), ("is_registrant" , "=" , True )]
116+
117+ if search_type == "individuals" :
118+ domain .append (("is_group" , "=" , False ))
119+ elif search_type == "groups" :
120+ domain .append (("is_group" , "=" , True ))
121+
122+ if advanced_filters :
123+ if advanced_filters .get ("registrationDateFrom" ):
124+ domain .append (("registration_date" , ">=" , advanced_filters ["registrationDateFrom" ]))
125+ if advanced_filters .get ("registrationDateTo" ):
126+ domain .append (("registration_date" , "<=" , advanced_filters ["registrationDateTo" ]))
127+
128+ fields_to_read = ["name" , "is_group" , "phone" , "email" , "registration_date" , "disabled" ]
129+ return self .search_read (domain , fields_to_read , limit = limit , order = "id desc" )
130+
131+ def _search_unified (self , search_term , limit ):
132+ """Run separate indexed queries per field and merge results."""
133+ partner_ids = set ()
134+
135+ # Query 1: Name match (uses trigram index)
136+ name_matches = self .search (
137+ [("is_registrant" , "=" , True ), ("name" , "ilike" , search_term )],
138+ limit = limit ,
139+ )
140+ partner_ids .update (name_matches .ids )
141+
142+ # Query 2: ID number exact match (uses B-tree index)
143+ reg_ids = self .env ["spp.registry.id" ].search (
144+ [("value" , "=" , search_term )],
145+ limit = limit ,
146+ )
147+ partner_ids .update (reg_ids .mapped ("partner_id" ).ids )
148+
149+ # Query 3: Phone number match (uses trigram index)
150+ phones = self .env ["spp.phone.number" ].search (
151+ [("phone_no" , "ilike" , search_term )],
152+ limit = limit ,
153+ )
154+ partner_ids .update (phones .mapped ("partner_id" ).ids )
155+
156+ # Query 4: Email match (uses trigram index)
157+ email_matches = self .search (
158+ [("is_registrant" , "=" , True ), ("email" , "ilike" , search_term )],
159+ limit = limit ,
160+ )
161+ partner_ids .update (email_matches .ids )
162+
163+ return partner_ids
164+
165+ def _search_by_field (self , search_term , search_field , limit ):
166+ """Search a single field (targeted mode)."""
167+ if search_field == "name" :
168+ return set (
169+ self .search (
170+ [("is_registrant" , "=" , True ), ("name" , "ilike" , search_term )],
171+ limit = limit ,
172+ ).ids
173+ )
174+ elif search_field == "id_number" :
175+ reg_ids = self .env ["spp.registry.id" ].search (
176+ [("value" , "=" , search_term )],
177+ limit = limit ,
178+ )
179+ return set (reg_ids .mapped ("partner_id" ).ids )
180+ elif search_field == "phone" :
181+ phones = self .env ["spp.phone.number" ].search (
182+ [("phone_no" , "ilike" , search_term )],
183+ limit = limit ,
184+ )
185+ return set (phones .mapped ("partner_id" ).ids )
186+ elif search_field == "email" :
187+ return set (
188+ self .search (
189+ [("is_registrant" , "=" , True ), ("email" , "ilike" , search_term )],
190+ limit = limit ,
191+ ).ids
192+ )
193+ return set ()
0 commit comments