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 <td class="d-none"> 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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/<int: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/<int: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/<int: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/<int: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',
|
||||
|
||||
Reference in New Issue
Block a user