docs(portal): session handoff + sub-A IA spec + plan
Captures everything the next Claude session needs to pick up cold:
- Live module versions on entech (portal 19.0.3.7.0, jobs/reports
versions, all 5 tests green)
- What shipped this session (24+ commits, summarised by area)
- Sub-A (IA + sidebar) brainstorm decisions locked, spec written,
plan ready to execute (11 tasks, 4 phases)
- What's deferred (sub-B multi-user, sub-C search, drafts, real
statements, RMA portal, top-recurring-parts) and WHY — so next
session doesn't re-litigate
- Gotchas hit + fixed this session that aren't obvious from code
- Deploy recipe (file copy + module upgrade + cache bust) used 20+
times this session
CLAUDE.md's Recent Session Handoff section now points to the new
handoff doc; the previous handoff is kept as 'superseded but kept
for context' below it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
# Customer Portal — Information Architecture + Sidebar Nav
|
||||
|
||||
**Module**: `fusion_plating_portal` (touches `portal.portal_layout` inherit)
|
||||
**Date**: 2026-05-17
|
||||
**Status**: Design locked, awaiting implementation plan
|
||||
**Surface**: every `/my/*` page on `https://enplating.com`
|
||||
**Sub-project**: A (of A/B/C); B = multi-user, C = portal search — deferred to separate brainstorms.
|
||||
|
||||
## Problem
|
||||
|
||||
The post-2026-05-17 portal redesign gave us a credible dashboard + jobs-detail page, but the navigation between pages is still "scroll the standard Odoo portal cards and hope you find the right entry point." Eight distinct customer surfaces (`/my/home`, `/my/jobs`, `/my/quote_requests`, `/my/configurator`, `/my/purchase_orders`, `/my/fp_invoices`, `/my/deliveries`, `/my/certifications`) and there's no persistent way to move between them. The customer's competitor screenshot (Mobility Specialties Inc / Drive Medical) shows the right pattern: a sticky left sidebar that lists every section, current page highlighted, secondary "Company Account" group at the bottom.
|
||||
|
||||
This spec restructures the portal around that sidebar pattern, audits the existing pages (replace thin custom pages with Odoo defaults where the default is better), and adds one missing page — a consolidated **Account Summary** with tabbed Invoices / Credit Memos / Statements + an Open Balance pill — that the existing thin `/my/fp_invoices` page doesn't deliver.
|
||||
|
||||
## User stories
|
||||
|
||||
1. **As a returning customer**, I want a persistent sidebar showing every section so I can jump between Quote Requests and Work Orders without going through the dashboard.
|
||||
2. **As an accounting clerk**, when I open the portal I want a single Account Summary page with Open Balance + filterable invoices + credit memos + downloadable monthly statements — without hunting through three separate menu items.
|
||||
3. **As any customer**, I want the active page visually marked so I always know where I am.
|
||||
4. **As a mobile user**, the sidebar should collapse to a hamburger so the page content gets the screen.
|
||||
|
||||
## Locked design decisions (from brainstorming 2026-05-17)
|
||||
|
||||
| Decision | Choice | Why |
|
||||
|---|---|---|
|
||||
| Decomposition | A first (IA), B (multi-user) + C (search) deferred to separate brainstorms | Sidebar + pages are the foundation; building search before pages exist or a Users tab before the sidebar shape is locked would be rework. |
|
||||
| Sidebar shape | Option B — Dashboard at top, then 3 grouped sections (Activity / Documents / Account) | 10 items needs grouping to scan; matches how the redesigned dashboard already groups (KPI tiles → jobs hero → secondary panels). |
|
||||
| Account Summary tabs | 3 tabs: Invoices · Credit Memos · Statements, plus an "Open Balance: $X" pill in the page header | Mirrors competitor; one summary number front-of-mind, three drilldowns. |
|
||||
| Future placeholders | NEITHER "Users (soon)" nor a search input shown in the sidebar today | Empty placeholders add visual noise; ship them when sub-B / sub-C land. |
|
||||
| Sidebar persistence | Sticky on scroll; visible on every `/my/*` page (including Odoo defaults via `portal.portal_layout` inherit); sub-pages keep their parent highlighted | Industry standard. Consistency means the customer never loses their place. |
|
||||
| Mobile collapse | Below 768px the sidebar collapses to a hamburger button in the page header; opens as a slide-in drawer | Standard portal pattern, no content rearrangement needed. |
|
||||
| Single quote-creation path | `/my/quote_requests/new` redirects to `/my/configurator/new` | Two paths to the same outcome confuses customers; the configurator is the more complete flow. |
|
||||
| Sign Out placement | Bottom of sidebar, separated by a hairline border | Matches competitor; gets sign-out off the page chrome. |
|
||||
|
||||
## Scope
|
||||
|
||||
**IN SCOPE — pages restructured / new:**
|
||||
|
||||
- `/my/home` — keep dashboard, gets sidebar
|
||||
- `/my/jobs` — keep list, gets sidebar
|
||||
- `/my/jobs/<id>` — keep detail, gets sidebar (highlight parent)
|
||||
- `/my/quote_requests` — keep list, gets sidebar
|
||||
- `/my/quote_requests/<id>` — keep detail, gets sidebar
|
||||
- `/my/quote_requests/new` — **REDIRECT** to `/my/configurator/new`
|
||||
- `/my/configurator` — keep landing, gets sidebar
|
||||
- `/my/configurator/new`, `.../coating`, `.../estimate` — keep wizard, gets sidebar
|
||||
- `/my/purchase_orders` — **REDIRECT** to Odoo default `/my/orders`; controller + template deleted
|
||||
- `/my/fp_invoices` — **REDIRECT** to new `/my/account_summary`; controller + template deleted
|
||||
- `/my/account_summary` — **NEW** tabbed page (this spec)
|
||||
- `/my/deliveries` — keep, gets sidebar
|
||||
- `/my/certifications` — keep, gets sidebar
|
||||
- `/my/account` — Odoo default, gets sidebar
|
||||
- `/my/orders/<id>` — Odoo default, gets sidebar
|
||||
|
||||
**IN SCOPE — chrome:**
|
||||
|
||||
- New `fp_portal_shell` template that inherits `portal.portal_layout` and wraps every `o_portal` page body with a sticky 240px sidebar on the left.
|
||||
- Sidebar SCSS partial (`fp_portal_sidebar.scss`) — brand-teal active state, mint gradient highlight, hairline section dividers.
|
||||
- Mobile breakpoint: hamburger toggle + slide-in drawer below 768px.
|
||||
- All Odoo default portal pages (`/my/account`, `/my/orders`, `/my/orders/<id>`, `/my/invoices/<id>`, etc.) get the sidebar via the `portal.portal_layout` inherit — zero per-page edits.
|
||||
|
||||
**OUT OF SCOPE — deferred to other sub-projects:**
|
||||
|
||||
- Multi-user account management (sub-project B): Users tab in sidebar, invitation flow, per-action ACLs.
|
||||
- Portal search (sub-project C): global search input above Dashboard, search-result page.
|
||||
- Saved drafts (separate brainstorm — needs its own scoping).
|
||||
- Top Recurring Parts / Favorites / SerialNumber Lookup (defer until customer demand confirmed).
|
||||
- RMA customer portal (sub-project after RMA backend ships).
|
||||
|
||||
**OUT OF SCOPE — explicit non-goals:**
|
||||
|
||||
- Top-bar navigation, breadcrumbs redesign, footer changes — none of these are part of A.
|
||||
- Restyling Odoo default `/my/account` or `/my/orders/<id>` page BODIES. We give them the sidebar via the layout inherit, but their content stays Odoo-standard.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Sidebar shell
|
||||
|
||||
```
|
||||
fusion_plating_portal/views/fp_portal_shell.xml
|
||||
└── inherits portal.portal_layout
|
||||
└── injects .o_fp_portal_shell wrapper that contains:
|
||||
├── <aside class="o_fp_portal_sidebar"> (sticky, 240px)
|
||||
│ └── partner header + 4 sections + sign-out
|
||||
└── <main class="o_fp_portal_main"> (existing portal body)
|
||||
```
|
||||
|
||||
Per Odoo's `portal.portal_layout` extension pattern, we inherit and use `<xpath expr="//div[@id='wrap']" position="replace">` (or `position="inside"` on the right anchor — TBD during implementation) to wrap the existing layout. The sidebar is a single shared template (`fp_portal_sidebar`) rendered above the existing portal page body.
|
||||
|
||||
Active-state marker: each sidebar `<a>` reads the current `page_name` from the template context (already set by every FP route — `fp_dashboard`, `fp_jobs`, etc.) and applies `o_fp_sidebar_active` when matched. Falls back to URL prefix match for Odoo default pages (`/my/orders` → Purchase Orders highlighted, `/my/account` → Profile highlighted).
|
||||
|
||||
### Sidebar items (final list)
|
||||
|
||||
```
|
||||
ACME AEROSPACE <-- partner.commercial_partner_id.name
|
||||
─────────────────────────────────────────
|
||||
🏠 Dashboard /my/home
|
||||
ACTIVITY
|
||||
📄 Quote Requests /my/quote_requests
|
||||
+ Get a Quote /my/configurator
|
||||
🛒 Purchase Orders /my/orders (Odoo)
|
||||
⚙️ Work Orders /my/jobs
|
||||
DOCUMENTS
|
||||
📑 Certifications /my/certifications
|
||||
📦 Packing Slips /my/deliveries
|
||||
💰 Account Summary /my/account_summary (NEW)
|
||||
ACCOUNT
|
||||
👤 Profile /my/account (Odoo)
|
||||
─────────────────────────────────────────
|
||||
↪ Sign Out /web/session/logout
|
||||
```
|
||||
|
||||
Section headers (`ACTIVITY` / `DOCUMENTS` / `ACCOUNT`) are display-only, not links. The whole list is rendered from a single Python data structure in the template context (passed by a small helper on `FpCustomerPortal`), so adding the future Users / Drafts / Search items is a one-line addition.
|
||||
|
||||
### Account Summary page
|
||||
|
||||
**URL**: `/my/account_summary`
|
||||
**Controller method**: `portal_account_summary(self, **kw)` on `FpCustomerPortal`
|
||||
**Template**: `portal_my_account_summary` in `fp_portal_account_summary.xml` (new file)
|
||||
|
||||
**Page structure:**
|
||||
|
||||
```
|
||||
[ Account Summary ] Open Balance: $4,820.00
|
||||
─────────────────────────────────────────────────────────────────────────────
|
||||
[ Invoices ] [ Credit Memos ] [ Statements ]
|
||||
─────────────────────────────────────────────────────────────────────────────
|
||||
Showing: Open · Closed · All [Search PO or #__________ ] [Sort ▾]
|
||||
─────────────────────────────────────────────────────────────────────────────
|
||||
# | Status | Posted On | PO # | Due Date | Balance | View PDF
|
||||
─────────────────────────────────────────────────────────────────────────────
|
||||
0035180274 | ● Open | May 13, 2026 | 53469 | Jun 12, 2026 | C$305.73 | View PDF
|
||||
...
|
||||
◀ Prev 1 2 3 4 5 Next ▶
|
||||
```
|
||||
|
||||
**Data sources (per tab):**
|
||||
|
||||
| Tab | Model + domain | Notes |
|
||||
|---|---|---|
|
||||
| Invoices | `account.move` where `move_type='out_invoice'`, `partner_id child_of commercial`, `state='posted'` | Today's `/my/fp_invoices` already does this; relocated here. |
|
||||
| Credit Memos | `account.move` where `move_type='out_refund'`, `partner_id child_of commercial`, `state='posted'` | Surfaces RMA credits when sub-12 RMA flow runs. Tab shows empty state with "No credits yet" when partner has none. |
|
||||
| Statements | Generated PDF per month via `account.followup` or a custom QWeb cron — **decided during implementation; preferred = use account.followup report directly per-customer with date filter** | Tab UI: month picker + Download button. |
|
||||
|
||||
**Open Balance pill** = sum of `amount_residual` across all open `out_invoice` records (regardless of tab). Computed in the controller, shown in the page header.
|
||||
|
||||
**Search box** = case-insensitive substring match on `name` (invoice number) OR `ref` (customer PO). Server-side filter, not JS.
|
||||
|
||||
**Sort options:** Newest → Oldest (default), Oldest → Newest, Largest balance, Smallest balance.
|
||||
|
||||
**Filter pills:** `Open` (residual > 0) / `Closed` (residual = 0) / `All`.
|
||||
|
||||
**Pagination:** 10 per page, server-side via `portal_pager`.
|
||||
|
||||
Invoice detail = existing Odoo `/my/invoices/<id>` page (no rewrite); the table's "View PDF" link goes to `/my/invoices/<id>?report_type=pdf&download=true` per Odoo's standard portal pattern.
|
||||
|
||||
### Mobile behavior
|
||||
|
||||
```scss
|
||||
@media (max-width: 768px) {
|
||||
.o_fp_portal_sidebar {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.2s ease;
|
||||
position: fixed; top: 0; left: 0; bottom: 0;
|
||||
z-index: 1040;
|
||||
}
|
||||
.o_fp_portal_sidebar.o_fp_open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.o_fp_portal_hamburger { display: inline-flex; }
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.o_fp_portal_hamburger { display: none; }
|
||||
}
|
||||
```
|
||||
|
||||
Hamburger button lives in the page header (above the main content). Click toggles `o_fp_open` on the sidebar via 5-line vanilla JS (no framework). Backdrop click closes the drawer.
|
||||
|
||||
## Files
|
||||
|
||||
**NEW:**
|
||||
|
||||
- `fusion_plating_portal/views/fp_portal_shell.xml` — `portal.portal_layout` inherit + sidebar markup
|
||||
- `fusion_plating_portal/views/fp_portal_account_summary.xml` — `portal_my_account_summary` template
|
||||
- `fusion_plating_portal/static/src/scss/fp_portal_sidebar.scss` — sidebar styling (sticky, active state, sections, mobile drawer)
|
||||
- `fusion_plating_portal/static/src/js/fp_portal_sidebar.js` — hamburger toggle (vanilla JS, no OWL)
|
||||
|
||||
**MODIFY:**
|
||||
|
||||
- `fusion_plating_portal/controllers/portal.py`
|
||||
- NEW route `portal_account_summary` at `/my/account_summary`
|
||||
- DELETE route `portal_my_fp_invoices` (the thin invoice list at `/my/fp_invoices`)
|
||||
- REPLACE route `portal_my_purchase_orders` body with `return request.redirect('/my/orders')`
|
||||
- REPLACE the GET handler for `portal_my_quote_request_new` with `return request.redirect('/my/configurator/new')` (or delete entirely if the configurator already exposes the equivalent form)
|
||||
- NEW helper `_fp_sidebar_items(self)` returning the sidebar data structure (consumed by `fp_portal_sidebar` template via inherited `_prepare_portal_layout_values`)
|
||||
- Extend `_prepare_portal_layout_values()` to inject `fp_sidebar_items` + `fp_partner_display_name` into every portal page's context so the sidebar renders correctly on Odoo default pages too.
|
||||
- `fusion_plating_portal/views/fp_portal_templates.xml` — delete `portal_my_fp_invoices` template body (route is gone). Remaining templates (jobs list, jobs detail, deliveries, certifications) get the sidebar **for free** via the `portal.portal_layout` inherit; no per-template edits.
|
||||
- `fusion_plating_portal/views/fp_portal_dashboard.xml` — dashboard template gets the sidebar via the layout inherit; no edits needed.
|
||||
- `fusion_plating_portal/__manifest__.py` — version bump + register the new XML/SCSS/JS files. Add `fp_portal_shell.xml` near the TOP of the `data` list (loaded before any template that uses sidebar variables).
|
||||
|
||||
**DELETE (or stub):**
|
||||
|
||||
- The `portal_my_fp_invoices` template body and the `portal_my_purchase_orders` template body. Routes redirected, templates unused. Keep route stubs so existing bookmarks 302 cleanly instead of 404.
|
||||
|
||||
## Migration / backward compatibility
|
||||
|
||||
| Old URL | New behavior |
|
||||
|---|---|
|
||||
| `/my/fp_invoices` | 302 → `/my/account_summary` |
|
||||
| `/my/purchase_orders` | 302 → `/my/orders` |
|
||||
| `/my/quote_requests/new` | 302 → `/my/configurator/new` |
|
||||
|
||||
No DB migration. No template namespace changes that break inherits. The page audit removes routes from the controller and templates from the data list; Odoo's module-upgrade cycle handles the ORM-side cleanup.
|
||||
|
||||
## Open items to verify during implementation
|
||||
|
||||
1. **`portal.portal_layout` extension pattern** — confirm the cleanest xpath for injecting the sidebar wrapper without breaking Odoo's existing portal CSS (`#wrap`, `.o_portal`). Likely `position="before"` on the main content slot. If unclear, fall back to inheriting at the `website.layout` level and writing a wholly new shell template.
|
||||
2. **Statements tab data source** — decide between (a) inline render of `account.followup` report per requested month, vs (b) precomputed monthly statement PDFs stored as attachments. Latter is simpler for V1; cron generates last-month statement on the 1st.
|
||||
3. **Mobile hamburger placement** — header anchor: a small button at the top-left of the main content area (above the page title) on mobile only. Confirm during Phase 4 visual pass.
|
||||
4. **Page-name → active-item mapping** — most FP routes set a clean `page_name` (e.g., `fp_jobs`, `fp_dashboard`). Odoo defaults don't; we'll match by URL prefix (`/my/orders` → `purchase_orders` item). One-helper `_fp_resolve_active_sidebar_item(url, page_name)` keeps the mapping in one place.
|
||||
5. **Account Summary Statements scope** — confirm whether monthly statements are something EN Plating currently generates, or if this is a new artifact we need to define a template for. If the latter, that's a separate small spec.
|
||||
|
||||
## What ships in a "done" state
|
||||
|
||||
- Every `/my/*` page (FP + Odoo default) shows the new sidebar.
|
||||
- Active page is visually marked.
|
||||
- Sidebar collapses to hamburger drawer below 768px.
|
||||
- `/my/account_summary` exists with 3 tabs, Open Balance pill, search + filter pills + sort + pagination.
|
||||
- 3 legacy URLs (`/my/fp_invoices`, `/my/purchase_orders`, `/my/quote_requests/new`) 302-redirect to their new homes.
|
||||
- Unit tests cover the new account_summary controller (3 tabs return the right counts, filter/search produce the right subset, Open Balance sums residuals correctly).
|
||||
- Module version bumped, deployed to entech, all 5 existing portal tests still green plus 3+ new tests for Account Summary.
|
||||
|
||||
---
|
||||
|
||||
*Sub-projects B (multi-user) and C (portal search) are tracked separately — they'll consume the sidebar slot conventions (insertion under ACCOUNT for Users, above DASHBOARD for the search input) defined here.*
|
||||
Reference in New Issue
Block a user