From b27f68b8d557211535ed7845f909a068d6342118 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Mon, 18 May 2026 00:06:18 -0400 Subject: [PATCH] feat(portal): real-time search + filter pills on 4 FP list pages Replaces the tab nav / portal.portal_searchbar on the 4 FP list pages with the new fp_portal_list_controls macro (filter pills + search input + sort dropdown) and drops portal_pager in favour of client-side filtering of up to 500 records: - Quote Requests (/my/quote_requests): filters: All / Active / Converted / Declined sorts: Newest / Reference / Status extra search fields: contact_name, contact_email, line.part_number, line.description, line.product_id.default_code - Work Orders (/my/jobs, cards layout): filters: All / Active / Ready to Ship / Complete sorts: Newest / Reference / Status extra search fields per card: part_catalog.part_number, part_catalog.name, sale_order.name, sale_order.client_order_ref, job.notes - Certifications (/my/certifications): no filters (all rows are terminal CoC jobs) sorts: Newest / Reference extra search fields: part name, processes (already in card text) - Packing Slips / Deliveries (/my/deliveries): no filters (all rows are state=done) sorts: Newest / Reference adds a visible Origin column (sale order ref) so customers can locate a slip by the SO it came from Each route accepts ?filter_state=... and ?sortby=... query params, returns up to 500 records, and passes result_total + clipped to the template so the macro can render a "showing latest 500 of N" notice when the cap is hit. Hidden cells inside each row carry extra terms that aren't displayed but are matched by the JS textContent scan. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../controllers/portal.py | 245 ++++++++++-------- .../views/fp_portal_templates.xml | 141 ++++++++-- 2 files changed, 251 insertions(+), 135 deletions(-) diff --git a/fusion_plating/fusion_plating_portal/controllers/portal.py b/fusion_plating/fusion_plating_portal/controllers/portal.py index 884a1c74..2dd5d95b 100644 --- a/fusion_plating/fusion_plating_portal/controllers/portal.py +++ b/fusion_plating/fusion_plating_portal/controllers/portal.py @@ -697,72 +697,70 @@ class FpCustomerPortal(CustomerPortal): ) # ========================================================================== - # QUOTE REQUESTS -- list with tabs + # QUOTE REQUESTS -- list with filter pills + real-time search # ========================================================================== @http.route( - ['/my/quote_requests', '/my/quote_requests/page/'], + ['/my/quote_requests'], type='http', auth='user', website=True, ) - def portal_my_quote_requests(self, page=1, sortby=None, filterby=None, **kw): + def portal_my_quote_requests(self, sortby=None, filter_state=None, **kw): partner = request.env.user.partner_id commercial = partner.commercial_partner_id Quote = request.env['fusion.plating.quote.request'] domain = [('partner_id', 'child_of', commercial.id)] - searchbar_sortings = { - 'date': {'label': _('Newest'), 'order': 'create_date desc'}, - 'name': {'label': _('Reference'), 'order': 'name desc'}, - 'state': {'label': _('Status'), 'order': 'state'}, + _sort_map = { + 'date': 'create_date desc', + 'name': 'name asc', + 'state': 'state asc', } - if not sortby: + if sortby not in _sort_map: sortby = 'date' - order = searchbar_sortings[sortby]['order'] + order = _sort_map[sortby] - # Tab filters - searchbar_filters = { - 'all': {'label': _('All'), 'domain': []}, - 'active': {'label': _('Active'), 'domain': [ - ('state', 'in', ['new', 'under_review', 'quoted']), - ]}, - 'converted': {'label': _('Converted'), 'domain': [ - ('state', '=', 'accepted'), - ]}, - 'declined': {'label': _('Declined'), 'domain': [ - ('state', 'in', ['declined', 'expired']), - ]}, + # Filter pills + _filter_map = { + 'all': [], + 'active': [('state', 'in', ['new', 'under_review', 'quoted'])], + 'converted': [('state', '=', 'accepted')], + 'declined': [('state', 'in', ['declined', 'expired'])], } - if not filterby or filterby not in searchbar_filters: - filterby = 'all' - domain += searchbar_filters[filterby]['domain'] + if filter_state not in _filter_map: + filter_state = 'all' + domain += _filter_map[filter_state] - total = Quote.search_count(domain) - pager = portal_pager( - url='/my/quote_requests', - url_args={'sortby': sortby, 'filterby': filterby}, - total=total, - page=page, - step=self._items_per_page, - ) - quote_requests = Quote.search( - domain, - order=order, - limit=self._items_per_page, - offset=pager['offset'], - ) + total_count = Quote.search_count(domain) + cap = 500 + clipped = total_count > cap + quote_requests = Quote.search(domain, order=order, limit=cap) request.session['my_fp_quote_requests_history'] = quote_requests.ids[:100] values = self._prepare_portal_layout_values() values.update({ 'quote_requests': quote_requests, 'page_name': 'fp_quote_requests', - 'pager': pager, 'default_url': '/my/quote_requests', - 'searchbar_sortings': searchbar_sortings, 'sortby': sortby, - 'searchbar_filters': searchbar_filters, - 'filterby': filterby, + 'filter_state': filter_state, + 'filters': [ + ('all', 'All'), + ('active', 'Active'), + ('converted', 'Converted'), + ('declined', 'Declined'), + ], + 'sorts': [ + ('date', 'Newest'), + ('name', 'Reference'), + ('state', 'Status'), + ], + 'result_total': total_count, + 'clipped': clipped, + 'search': '', + 'url': '/my/quote_requests', + 'extra_qs': '', + 'target': 'tbody.o_fp_qr_filterable', }) return request.render( 'fusion_plating_portal.portal_my_quote_requests', @@ -956,15 +954,15 @@ class FpCustomerPortal(CustomerPortal): return request.redirect('/my/quote_requests/%s' % quote.id) # ========================================================================== - # JOBS -- list + # JOBS -- list with filter pills + real-time search (cards layout) # ========================================================================== @http.route( - ['/my/jobs', '/my/jobs/page/'], + ['/my/jobs'], type='http', auth='user', website=True, ) - def portal_my_jobs(self, page=1, sortby=None, **kw): + def portal_my_jobs(self, sortby=None, filter_state=None, **kw): partner = request.env.user.partner_id commercial = partner.commercial_partner_id # sudo() so the rendered cards can traverse job.x_fc_job_id -> fp.job @@ -974,39 +972,56 @@ class FpCustomerPortal(CustomerPortal): Job = request.env['fusion.plating.portal.job'].sudo() domain = [('partner_id', 'child_of', commercial.id)] - searchbar_sortings = { - 'date': {'label': _('Newest'), 'order': 'received_date desc, id desc'}, - 'name': {'label': _('Reference'), 'order': 'name desc'}, - 'state': {'label': _('Status'), 'order': 'state'}, + _sort_map = { + 'date': 'received_date desc, id desc', + 'name': 'name asc', + 'state': 'state asc', } - if not sortby: + if sortby not in _sort_map: sortby = 'date' - order = searchbar_sortings[sortby]['order'] + order = _sort_map[sortby] - total = Job.search_count(domain) - pager = portal_pager( - url='/my/jobs', - url_args={'sortby': sortby}, - total=total, - page=page, - step=self._items_per_page, - ) - jobs = Job.search( - domain, - order=order, - limit=self._items_per_page, - offset=pager['offset'], - ) + # Filter pills + _filter_map = { + 'all': [], + 'active': [('state', 'in', ['received', 'in_progress', 'quality_check'])], + 'ready': [('state', '=', 'ready_to_ship')], + 'complete': [('state', 'in', ['shipped', 'complete'])], + } + if filter_state not in _filter_map: + filter_state = 'all' + domain += _filter_map[filter_state] + + total_count = Job.search_count(domain) + cap = 500 + clipped = total_count > cap + jobs = Job.search(domain, order=order, limit=cap) request.session['my_fp_jobs_history'] = jobs.ids[:100] values = self._prepare_portal_layout_values() values.update({ 'jobs': jobs, 'page_name': 'fp_jobs', - 'pager': pager, 'default_url': '/my/jobs', - 'searchbar_sortings': searchbar_sortings, 'sortby': sortby, + 'filter_state': filter_state, + 'filters': [ + ('all', 'All'), + ('active', 'Active'), + ('ready', 'Ready to Ship'), + ('complete', 'Complete'), + ], + 'sorts': [ + ('date', 'Newest'), + ('name', 'Reference'), + ('state', 'Status'), + ], + 'result_total': total_count, + 'clipped': clipped, + 'search': '', + 'url': '/my/jobs', + 'extra_qs': '', + 'target': '#fp_jobs_list', }) return request.render( 'fusion_plating_portal.portal_my_jobs', @@ -1239,15 +1254,15 @@ class FpCustomerPortal(CustomerPortal): return request.redirect('/my/account_summary') # ========================================================================== - # SHIPPING / DELIVERIES -- list + # SHIPPING / DELIVERIES -- list with search + sort # ========================================================================== @http.route( - ['/my/deliveries', '/my/deliveries/page/'], + ['/my/deliveries'], type='http', auth='user', website=True, ) - def portal_my_deliveries(self, page=1, sortby=None, **kw): + def portal_my_deliveries(self, sortby=None, **kw): partner = request.env.user.partner_id commercial = partner.commercial_partner_id Picking = request.env['stock.picking'].sudo() @@ -1257,37 +1272,37 @@ class FpCustomerPortal(CustomerPortal): ('state', '=', 'done'), ] - searchbar_sortings = { - 'date': {'label': _('Newest'), 'order': 'date_done desc'}, - 'name': {'label': _('Reference'), 'order': 'name desc'}, + _sort_map = { + 'date': 'date_done desc', + 'name': 'name asc', } - if not sortby: + if sortby not in _sort_map: sortby = 'date' - order = searchbar_sortings[sortby]['order'] + order = _sort_map[sortby] - total = Picking.search_count(domain) - pager = portal_pager( - url='/my/deliveries', - url_args={'sortby': sortby}, - total=total, - page=page, - step=self._items_per_page, - ) - deliveries = Picking.search( - domain, - order=order, - limit=self._items_per_page, - offset=pager['offset'], - ) + total_count = Picking.search_count(domain) + cap = 500 + clipped = total_count > cap + deliveries = Picking.search(domain, order=order, limit=cap) values = self._prepare_portal_layout_values() values.update({ 'deliveries': deliveries, 'page_name': 'fp_deliveries', - 'pager': pager, 'default_url': '/my/deliveries', - 'searchbar_sortings': searchbar_sortings, 'sortby': sortby, + 'filter_state': 'all', + 'filters': False, + 'sorts': [ + ('date', 'Newest'), + ('name', 'Reference'), + ], + 'result_total': total_count, + 'clipped': clipped, + 'search': '', + 'url': '/my/deliveries', + 'extra_qs': '', + 'target': 'tbody.o_fp_deliveries_filterable', }) return request.render( 'fusion_plating_portal.portal_my_deliveries', @@ -1295,15 +1310,15 @@ class FpCustomerPortal(CustomerPortal): ) # ========================================================================== - # CERTIFICATIONS -- list + # CERTIFICATIONS -- list with search + sort # ========================================================================== @http.route( - ['/my/certifications', '/my/certifications/page/'], + ['/my/certifications'], type='http', auth='user', website=True, ) - def portal_my_certifications(self, page=1, sortby=None, **kw): + def portal_my_certifications(self, sortby=None, **kw): partner = request.env.user.partner_id commercial = partner.commercial_partner_id # sudo() so the rendered cards can traverse job.x_fc_job_id -> fp.job @@ -1316,37 +1331,37 @@ class FpCustomerPortal(CustomerPortal): ('coc_attachment_id', '!=', False), ] - searchbar_sortings = { - 'date': {'label': _('Newest'), 'order': 'actual_ship_date desc, id desc'}, - 'name': {'label': _('Job Reference'), 'order': 'name desc'}, + _sort_map = { + 'date': 'actual_ship_date desc, id desc', + 'name': 'name asc', } - if not sortby: + if sortby not in _sort_map: sortby = 'date' - order = searchbar_sortings[sortby]['order'] + order = _sort_map[sortby] - total = Job.search_count(domain) - pager = portal_pager( - url='/my/certifications', - url_args={'sortby': sortby}, - total=total, - page=page, - step=self._items_per_page, - ) - cert_jobs = Job.search( - domain, - order=order, - limit=self._items_per_page, - offset=pager['offset'], - ) + total_count = Job.search_count(domain) + cap = 500 + clipped = total_count > cap + cert_jobs = Job.search(domain, order=order, limit=cap) values = self._prepare_portal_layout_values() values.update({ 'cert_jobs': cert_jobs, 'page_name': 'fp_certifications', - 'pager': pager, 'default_url': '/my/certifications', - 'searchbar_sortings': searchbar_sortings, 'sortby': sortby, + 'filter_state': 'all', + 'filters': False, + 'sorts': [ + ('date', 'Newest'), + ('name', 'Reference'), + ], + 'result_total': total_count, + 'clipped': clipped, + 'search': '', + 'url': '/my/certifications', + 'extra_qs': '', + 'target': 'tbody.o_fp_certs_filterable', }) return request.render( 'fusion_plating_portal.portal_my_certifications', diff --git a/fusion_plating/fusion_plating_portal/views/fp_portal_templates.xml b/fusion_plating/fusion_plating_portal/views/fp_portal_templates.xml index dbbe62cf..71714c93 100644 --- a/fusion_plating/fusion_plating_portal/views/fp_portal_templates.xml +++ b/fusion_plating/fusion_plating_portal/views/fp_portal_templates.xml @@ -7,7 +7,7 @@ - + - + - + - +