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:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}&filter_state=#{fk}&sort=#{sort}&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 + '&filter_state=' + filter_state + '&sort=date_desc&search=' + search"
|
||||
t-att-selected="sort == 'date_desc'">Newest first</option>
|
||||
<option t-att-value="'/my/account_summary?tab=' + active_tab + '&filter_state=' + filter_state + '&sort=date_asc&search=' + search"
|
||||
t-att-selected="sort == 'date_asc'">Oldest first</option>
|
||||
<option t-att-value="'/my/account_summary?tab=' + active_tab + '&filter_state=' + filter_state + '&sort=amount_desc&search=' + search"
|
||||
t-att-selected="sort == 'amount_desc'">Largest amount</option>
|
||||
<option t-att-value="'/my/account_summary?tab=' + active_tab + '&filter_state=' + filter_state + '&sort=amount_asc&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&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>
|
||||
Reference in New Issue
Block a user