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_quote_request_views.xml',
|
||||||
'views/fp_portal_dashboard.xml',
|
'views/fp_portal_dashboard.xml',
|
||||||
'views/fp_portal_templates.xml',
|
'views/fp_portal_templates.xml',
|
||||||
|
'views/fp_portal_account_summary.xml', # NEW — Task 10
|
||||||
'views/fp_portal_configurator_templates.xml',
|
'views/fp_portal_configurator_templates.xml',
|
||||||
'views/fp_portal_breadcrumbs.xml',
|
'views/fp_portal_breadcrumbs.xml',
|
||||||
'views/fp_sale_order_portal.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