feat(portal): Account Summary template (3 tabs, filter, search, sort, pager)

Tabs: Invoices / Credit Memos / Statements (V1 placeholder).
Page header carries the Open Balance pill. Per-tab filter pills
(Open/Closed/All), search box (name OR ref), sort dropdown
(newest/oldest/largest/smallest), 10-per-page pager.

Empty states: 'No results for X' for failed searches, 'No records
in this tab' for empty result sets, and the dedicated Statements
'coming soon' card. Statements tab hides the filter/search/sort
strip — nothing to filter yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-17 14:19:33 -04:00
parent b92a396934
commit 77b84ac11b
3 changed files with 178 additions and 0 deletions

View File

@@ -60,6 +60,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
'views/fp_quote_request_views.xml',
'views/fp_portal_dashboard.xml',
'views/fp_portal_templates.xml',
'views/fp_portal_account_summary.xml', # NEW — Task 10
'views/fp_portal_configurator_templates.xml',
'views/fp_portal_breadcrumbs.xml',
'views/fp_sale_order_portal.xml',

View File

@@ -270,3 +270,21 @@
}
}
}
// Filter pills used by Account Summary (also reusable elsewhere)
.o_fp_filter_pill {
display: inline-block;
padding: .25rem .75rem;
border-radius: $fp-radius-pill;
background: $fp-section-bg;
color: $fp-muted;
font-size: .8rem;
text-decoration: none;
transition: background .12s ease, color .12s ease;
&:hover { background: $fp-mint; color: $fp-teal-dark; text-decoration: none; }
&.o_fp_filter_pill_active {
background: $fp-gradient-primary;
color: #fff;
font-weight: 600;
}
}

View File

@@ -0,0 +1,159 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
-->
<odoo>
<template id="portal_my_account_summary" name="Account Summary">
<t t-call="portal.portal_layout">
<div class="o_fp_account_summary">
<!-- Page header: title + Open Balance pill -->
<div class="d-flex justify-content-between align-items-baseline mb-3">
<h3 class="mb-0" style="color: var(--fp-text, #111827)">Account Summary</h3>
<div class="o_fp_badge o_fp_badge_paid" t-if="open_balance">
<span class="o_fp_badge_dot"/>
Open Balance:
<span t-field="open_balance"
t-options='{"widget": "monetary", "display_currency": currency}'/>
</div>
<span class="o_fp_badge" t-else=""
style="background:#f3f7f6;color:#374151">
Open Balance: $0.00
</span>
</div>
<!-- Tab strip -->
<ul class="nav nav-tabs mb-3" role="tablist">
<t t-foreach="tabs" t-as="tab_entry">
<li class="nav-item">
<a t-attf-href="/my/account_summary?tab=#{tab_entry[0]}"
t-attf-class="nav-link #{'active' if active_tab == tab_entry[0] else ''}"
t-out="tab_entry[1]"/>
</li>
</t>
</ul>
<!-- Filter pills + search + sort -->
<t t-if="active_tab != 'statements'">
<div class="d-flex flex-wrap align-items-center gap-3 mb-3">
<div class="d-flex align-items-center gap-2">
<span class="text-muted small">Showing:</span>
<t t-foreach="['open', 'closed', 'all']" t-as="fk">
<a t-attf-href="/my/account_summary?tab=#{active_tab}&amp;filter_state=#{fk}&amp;sort=#{sort}&amp;search=#{search}"
t-attf-class="o_fp_filter_pill #{'o_fp_filter_pill_active' if filter_state == fk else ''}"
t-out="fk.capitalize()"/>
</t>
</div>
<form method="GET" action="/my/account_summary" class="d-flex gap-1 ms-auto m-0">
<input type="hidden" name="tab" t-att-value="active_tab"/>
<input type="hidden" name="filter_state" t-att-value="filter_state"/>
<input type="hidden" name="sort" t-att-value="sort"/>
<input type="text" name="search" t-att-value="search"
placeholder="Search invoice # or PO #"
class="form-control form-control-sm"
style="max-width: 260px"/>
<button type="submit" class="o_fp_btn_secondary o_fp_btn_sm">Search</button>
</form>
<select onchange="window.location.href = this.value"
class="form-select form-select-sm" style="max-width: 200px">
<option t-att-value="'/my/account_summary?tab=' + active_tab + '&amp;filter_state=' + filter_state + '&amp;sort=date_desc&amp;search=' + search"
t-att-selected="sort == 'date_desc'">Newest first</option>
<option t-att-value="'/my/account_summary?tab=' + active_tab + '&amp;filter_state=' + filter_state + '&amp;sort=date_asc&amp;search=' + search"
t-att-selected="sort == 'date_asc'">Oldest first</option>
<option t-att-value="'/my/account_summary?tab=' + active_tab + '&amp;filter_state=' + filter_state + '&amp;sort=amount_desc&amp;search=' + search"
t-att-selected="sort == 'amount_desc'">Largest amount</option>
<option t-att-value="'/my/account_summary?tab=' + active_tab + '&amp;filter_state=' + filter_state + '&amp;sort=amount_asc&amp;search=' + search"
t-att-selected="sort == 'amount_asc'">Smallest amount</option>
</select>
</div>
</t>
<!-- Table -->
<t t-if="active_tab == 'statements'">
<div class="o_fp_card text-center text-muted" style="padding: 2rem">
<p>Monthly statements coming soon.</p>
<p class="small">
For a copy in the meantime, contact your sales rep at EN Plating.
</p>
</div>
</t>
<t t-elif="not records">
<div class="o_fp_card text-center text-muted" style="padding: 1.5rem">
<t t-if="search">No results for "<t t-out="search"/>".</t>
<t t-else="">No records in this tab.</t>
</div>
</t>
<t t-else="">
<div class="o_fp_card" style="padding: 0; overflow: hidden">
<table class="table mb-0">
<thead>
<tr>
<th>#</th>
<th>Status</th>
<th>Posted On</th>
<th>PO #</th>
<th>Due Date</th>
<th class="text-end">Balance</th>
<th class="text-end">View PDF</th>
</tr>
</thead>
<tbody>
<tr t-foreach="records" t-as="move">
<td t-out="move.name"/>
<td>
<t t-if="move.amount_residual == 0">
<span class="o_fp_badge o_fp_badge_paid"><span class="o_fp_badge_dot"/>Closed</span>
</t>
<t t-else="">
<span class="o_fp_badge o_fp_badge_in_progress"><span class="o_fp_badge_dot"/>Open</span>
</t>
</td>
<td>
<span t-if="move.invoice_date"
t-field="move.invoice_date"
t-options='{"widget": "date"}'/>
</td>
<td t-out="move.ref or ''"/>
<td>
<span t-if="move.invoice_date_due"
t-field="move.invoice_date_due"
t-options='{"widget": "date"}'/>
</td>
<td class="text-end">
<span t-field="move.amount_residual"
t-options='{"widget": "monetary", "display_currency": move.currency_id}'/>
</td>
<td class="text-end">
<a t-attf-href="/my/invoices/#{move.id}?report_type=pdf&amp;download=true"
class="o_fp_btn_ghost o_fp_btn_sm">View PDF</a>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pager -->
<div class="d-flex justify-content-between align-items-center mt-3"
t-if="pager and pager.get('page_count', 0) > 1">
<div class="text-muted small">
Showing
<t t-out="pager['offset'] + 1"/><t t-out="min(pager['offset'] + 10, total)"/>
of <t t-out="total"/>
</div>
<ul class="pagination mb-0">
<t t-foreach="pager.get('pages', [])" t-as="p">
<li t-attf-class="page-item #{'active' if p['num'] == pager['page']['num'] else ''}">
<a class="page-link" t-att-href="p['url']" t-out="p['num']"/>
</li>
</t>
</ul>
</div>
</t>
</div>
</t>
</template>
</odoo>