Spec covers the brainstormed design: jobs-forward layout, V2 stepper with timestamps, EN Plating teal/gradient palette, 4 doc categories. Plan decomposes implementation into 4 independently-deployable phases (tokens+buttons -> dashboard -> jobs detail -> cosmetic sweep) with 27 tasks total. Also adds .gitignore so .superpowers/ brainstorm artifacts stay untracked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2593 lines
95 KiB
Markdown
2593 lines
95 KiB
Markdown
# Customer Portal Dashboard Redesign — Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Replace the bland 6-card `/my/home` with a jobs-forward customer dashboard, add a per-job detail page with vertical timeline + grouped documents, and apply EN Plating's teal/green brand palette with gradient CTAs across the portal.
|
|
|
|
**Architecture:** Pure server-rendered QWeb on top of existing `FpCustomerPortal` controller. New SCSS token system + macros power 4 phases that ship independently (tokens+buttons → dashboard → jobs detail → cosmetic sweep). No new models in V1; one optional timestamp-field addition on `fusion.plating.portal.job` if Phase 3 investigation requires it.
|
|
|
|
**Tech Stack:** Odoo 19 (Python + QWeb XML + SCSS), Bootstrap 5 utility classes for layout, no JS framework. Deployment via SSH to entech LXC 111 (native Odoo, db `admin`).
|
|
|
|
**Spec:** [`docs/superpowers/specs/2026-05-17-portal-dashboard-redesign-design.md`](../specs/2026-05-17-portal-dashboard-redesign-design.md)
|
|
**Mockups:** `.superpowers/brainstorm/1800-1778997036/content/branded-dashboard.html` (dashboard), `job-detail.html` (detail). Visual fidelity ≈ pixel-match these.
|
|
|
|
---
|
|
|
|
## File Inventory
|
|
|
|
**NEW files:**
|
|
- `fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss` — brand palette + gradient + radius + shadow vars
|
|
- `fusion_plating_portal/static/src/scss/fp_portal_buttons.scss` — gradient buttons (primary/secondary/ghost/danger)
|
|
- `fusion_plating_portal/static/src/scss/fp_portal_badges.scss` — status badges with dot + glow
|
|
- `fusion_plating_portal/static/src/scss/fp_portal_cards.scss` — card shells + KPI tiles
|
|
- `fusion_plating_portal/static/src/scss/fp_portal_stepper.scss` — circular numbered stepper geometry
|
|
- `fusion_plating_portal/static/src/scss/fp_portal_timeline.scss` — vertical timeline for detail page
|
|
- `fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss` — jobs-forward grid + secondary panels
|
|
- `fusion_plating_portal/views/fp_portal_macros.xml` — shared QWeb macros (stepper, badge, doc chip, doc group)
|
|
- `fusion_plating_portal/tests/__init__.py` — test package
|
|
- `fusion_plating_portal/tests/test_portal_dashboard.py` — controller helper unit tests
|
|
|
|
**MODIFY files:**
|
|
- `fusion_plating_portal/controllers/portal.py` — extend `home()`, rewrite `portal_my_job()`, add helpers
|
|
- `fusion_plating_portal/views/fp_portal_dashboard.xml` — rewrite `fp_portal_home_dashboard` template
|
|
- `fusion_plating_portal/views/fp_portal_templates.xml` — rewrite `portal_my_jobs` + `portal_my_job` templates
|
|
- `fusion_plating_portal/views/fp_quote_request_views.xml` — Phase 4 cosmetic tokenisation
|
|
- `fusion_plating_portal/views/fp_portal_configurator_templates.xml` — Phase 4 cosmetic tokenisation
|
|
- `fusion_plating_portal/static/src/scss/fusion_plating_portal.scss` — trim to catch-all
|
|
- `fusion_plating_portal/__manifest__.py` — version bump + new assets + new data
|
|
- `fusion_plating_portal/models/fp_portal_job.py` — conditionally add stage Datetime fields (Phase 3 Task 17, only if Task 16 investigation requires)
|
|
|
|
---
|
|
|
|
# PHASE 1 — Tokens + Button System
|
|
|
|
Goal: ship the brand palette + gradient button system on its own. Visible change is buttons only. Independently deployable.
|
|
|
|
### Task 1: Create `_fp_portal_tokens.scss`
|
|
|
|
**Files:**
|
|
- Create: `fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss`
|
|
|
|
- [ ] **Step 1: Write the file with brand variables**
|
|
|
|
Create `K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss` with this exact content:
|
|
|
|
```scss
|
|
// ============================================================================
|
|
// Fusion Plating — Customer Portal · Design Tokens
|
|
// Brand palette pulled from enplating.com live CSS (2026-05-17).
|
|
// Loaded first in web.assets_frontend so every later SCSS file sees these.
|
|
// Per Odoo 19 SCSS rules (CLAUDE.md rule 8/9): no @import; tokens are SCSS
|
|
// variables that downstream files reference directly, NOT CSS custom props.
|
|
// ============================================================================
|
|
|
|
// Brand palette
|
|
$fp-teal-light: #2eaf93;
|
|
$fp-teal: #1a6b59;
|
|
$fp-teal-dark: #0e3d2f;
|
|
$fp-teal-deep: #0a3528;
|
|
$fp-mint: #cbf3e6;
|
|
$fp-mint-pastel: #f0fdf9;
|
|
$fp-aqua: #9ae5d4;
|
|
|
|
// Surfaces
|
|
$fp-page-bg: #f8fafb;
|
|
$fp-section-bg: #f3f7f6;
|
|
$fp-card-bg: #ffffff;
|
|
$fp-card-border: #e5e7eb;
|
|
$fp-card-border-dark: #d1d5db;
|
|
|
|
// Text
|
|
$fp-text: #111827;
|
|
$fp-text-body: #374151;
|
|
$fp-muted: #6b7280;
|
|
$fp-muted-light: #9ca3af;
|
|
$fp-disabled: #d1d5db;
|
|
|
|
// Status (functional, NOT brand)
|
|
$fp-amber: #f59e0b;
|
|
$fp-amber-bg: #fef3c7;
|
|
$fp-amber-text: #92400e;
|
|
$fp-success: #22c55e;
|
|
$fp-success-text: #15803d;
|
|
$fp-success-bg: #f0fdf4;
|
|
$fp-danger: #ef4444;
|
|
$fp-danger-dark: #b91c1c;
|
|
$fp-danger-bg: #fef2f2;
|
|
|
|
// Gradients
|
|
$fp-gradient-primary: linear-gradient(135deg, $fp-teal-light 0%, $fp-teal 100%);
|
|
$fp-gradient-danger: linear-gradient(135deg, $fp-danger 0%, $fp-danger-dark 100%);
|
|
$fp-gradient-mint: linear-gradient(135deg, $fp-mint-pastel 0%, $fp-mint 100%);
|
|
$fp-gradient-icon: linear-gradient(135deg, $fp-mint 0%, $fp-aqua 100%);
|
|
$fp-gradient-secondary: linear-gradient(180deg, #fff 0%, $fp-section-bg 100%);
|
|
$fp-gradient-tab: linear-gradient(180deg, $fp-section-bg 0%, $fp-mint 100%);
|
|
|
|
// Shadows
|
|
$fp-shadow-card: 0 1px 2px rgba(0, 0, 0, .03);
|
|
$fp-shadow-card-hover: 0 1px 3px rgba(0, 0, 0, .04), 0 4px 12px rgba(0, 0, 0, .04);
|
|
$fp-shadow-button: 0 1px 3px rgba(26, 107, 89, .25), 0 4px 12px rgba(26, 107, 89, .18);
|
|
$fp-shadow-button-hover:0 2px 4px rgba(26, 107, 89, .30), 0 6px 16px rgba(26, 107, 89, .22);
|
|
$fp-shadow-danger: 0 1px 3px rgba(185, 28, 28, .25), 0 4px 12px rgba(185, 28, 28, .15);
|
|
$fp-glow-ring-teal: 0 0 0 4px rgba(46, 175, 147, .20);
|
|
$fp-glow-ring-amber: 0 0 0 4px rgba(245, 158, 11, .20);
|
|
|
|
// Geometry
|
|
$fp-radius-pill: 9999px;
|
|
$fp-radius-card: 14px;
|
|
$fp-radius-button: 9px;
|
|
$fp-radius-chip: 8px;
|
|
$fp-radius-icon: 7px;
|
|
$fp-radius-tile: 11px;
|
|
|
|
// Spacing scale (rem)
|
|
$fp-space-1: .25rem;
|
|
$fp-space-2: .5rem;
|
|
$fp-space-3: .7rem;
|
|
$fp-space-4: 1rem;
|
|
$fp-space-5: 1.25rem;
|
|
$fp-space-6: 1.5rem;
|
|
|
|
// Typography
|
|
$fp-font: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
$fp-font-mono: ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, monospace;
|
|
|
|
// Dark-mode placeholder — DEFERRED per spec.
|
|
// When implementing, branch on $o-webclient-color-scheme per CLAUDE.md rule 9.
|
|
// Example pattern (do NOT enable now):
|
|
// @if $o-webclient-color-scheme == dark {
|
|
// $fp-page-bg: #0e1f1b !global;
|
|
// $fp-card-bg: #1a2b27 !global;
|
|
// // ...
|
|
// }
|
|
```
|
|
|
|
- [ ] **Step 2: Verify file exists**
|
|
|
|
Run: `ls -la K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss`
|
|
Expected: file present, non-zero size.
|
|
|
|
- [ ] **Step 3: Commit (atomic)**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss && \
|
|
git commit -m "feat(portal): add brand design tokens partial
|
|
|
|
EN Plating teal palette + gradient/shadow/radius/spacing/typography
|
|
tokens. Single source of truth for the customer portal redesign.
|
|
Tokens load first in web.assets_frontend so downstream SCSS sees them.
|
|
|
|
Refs spec: docs/superpowers/specs/2026-05-17-portal-dashboard-redesign-design.md"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Create `fp_portal_buttons.scss`
|
|
|
|
**Files:**
|
|
- Create: `fusion_plating_portal/static/src/scss/fp_portal_buttons.scss`
|
|
|
|
- [ ] **Step 1: Write the gradient button system**
|
|
|
|
Create `K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss` with this content:
|
|
|
|
```scss
|
|
// ============================================================================
|
|
// Fusion Plating — Portal · Button system
|
|
// Gradient primary CTA, soft secondary, ghost tertiary, gradient danger.
|
|
// All states use class hooks under .o_fp_btn_* so they don't fight Bootstrap.
|
|
// ============================================================================
|
|
|
|
.o_fp_btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: $fp-space-2;
|
|
padding: .55rem 1.1rem;
|
|
border-radius: $fp-radius-button;
|
|
font-family: $fp-font;
|
|
font-size: .85rem;
|
|
font-weight: 600;
|
|
line-height: 1.1;
|
|
border: none;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
transition: transform .08s ease, box-shadow .15s ease;
|
|
user-select: none;
|
|
|
|
&:focus-visible {
|
|
outline: 2px solid $fp-teal;
|
|
outline-offset: 2px;
|
|
}
|
|
&:active { transform: translateY(1px); }
|
|
&:disabled,
|
|
&.disabled {
|
|
opacity: .55;
|
|
cursor: not-allowed;
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
|
|
// PRIMARY — gradient teal CTA
|
|
.o_fp_btn_primary {
|
|
@extend .o_fp_btn;
|
|
background: $fp-gradient-primary;
|
|
color: #fff;
|
|
box-shadow: $fp-shadow-button;
|
|
text-shadow: 0 1px 0 rgba(0, 0, 0, .08);
|
|
&:hover { box-shadow: $fp-shadow-button-hover; color: #fff; }
|
|
}
|
|
|
|
// SECONDARY — outlined, very subtle gradient
|
|
.o_fp_btn_secondary {
|
|
@extend .o_fp_btn;
|
|
background: $fp-gradient-secondary;
|
|
color: $fp-teal;
|
|
border: 1px solid $fp-card-border-dark;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, .04);
|
|
&:hover { background: $fp-section-bg; color: $fp-teal-dark; }
|
|
}
|
|
|
|
// GHOST — text-only with subtle hover
|
|
.o_fp_btn_ghost {
|
|
@extend .o_fp_btn;
|
|
background: transparent;
|
|
color: $fp-teal;
|
|
font-weight: 500;
|
|
padding: .45rem .85rem;
|
|
&:hover { background: rgba(46, 175, 147, .08); color: $fp-teal-dark; }
|
|
}
|
|
|
|
// DANGER — gradient red
|
|
.o_fp_btn_danger {
|
|
@extend .o_fp_btn;
|
|
background: $fp-gradient-danger;
|
|
color: #fff;
|
|
box-shadow: $fp-shadow-danger;
|
|
&:hover { color: #fff; }
|
|
}
|
|
|
|
// MINT-PILL — soft branded "view all" affordance
|
|
.o_fp_btn_mint {
|
|
@extend .o_fp_btn;
|
|
background: $fp-gradient-mint;
|
|
color: $fp-teal;
|
|
border: 1px solid $fp-aqua;
|
|
font-weight: 600;
|
|
&:hover { color: $fp-teal-dark; }
|
|
}
|
|
|
|
// Size modifiers
|
|
.o_fp_btn_sm { padding: .35rem .75rem; font-size: .76rem; }
|
|
.o_fp_btn_lg { padding: .75rem 1.4rem; font-size: .95rem; }
|
|
```
|
|
|
|
- [ ] **Step 2: Verify file**
|
|
|
|
Run: `ls -la K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss`
|
|
Expected: file present.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/static/src/scss/fp_portal_buttons.scss && \
|
|
git commit -m "feat(portal): gradient button system (primary/secondary/ghost/danger/mint)
|
|
|
|
Five button variants under .o_fp_btn_* classes that don't fight
|
|
Bootstrap. Primary uses the brand teal gradient with mint-tinted
|
|
shadow; danger uses the red gradient. Focus/hover/active states
|
|
included."
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Register Phase 1 assets in manifest + version bump
|
|
|
|
**Files:**
|
|
- Modify: `fusion_plating_portal/__manifest__.py`
|
|
|
|
- [ ] **Step 1: Read current manifest**
|
|
|
|
Read `fusion_plating_portal/__manifest__.py` lines 1-78 to see current asset block.
|
|
|
|
- [ ] **Step 2: Bump version and add Phase 1 assets**
|
|
|
|
Modify the manifest:
|
|
|
|
Change `'version': '19.0.2.3.0'` to `'version': '19.0.3.0.0'`.
|
|
|
|
Replace the `'assets'` block with:
|
|
|
|
```python
|
|
'assets': {
|
|
'web.assets_frontend': [
|
|
# Tokens MUST be first so every later file sees the variables.
|
|
'fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss',
|
|
# Phase 1 — button system
|
|
'fusion_plating_portal/static/src/scss/fp_portal_buttons.scss',
|
|
# Catch-all legacy rules (last)
|
|
'fusion_plating_portal/static/src/scss/fusion_plating_portal.scss',
|
|
'fusion_plating_portal/static/src/js/fp_rfq_form.js',
|
|
],
|
|
},
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/__manifest__.py && \
|
|
git commit -m "chore(portal): bump version 19.0.3.0.0 + register Phase 1 SCSS
|
|
|
|
Tokens partial loaded first; buttons SCSS loaded next; legacy
|
|
catch-all stays last. Per CLAUDE.md rule 8 every SCSS file is a
|
|
separate entry (no @import allowed in Odoo 19 custom SCSS)."
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Deploy Phase 1 to entech + visual verify
|
|
|
|
**Files:** (deployment, no files)
|
|
|
|
- [ ] **Step 1: Copy the 3 new/modified files to entech**
|
|
|
|
```bash
|
|
cat K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss | \
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss'"
|
|
```
|
|
|
|
```bash
|
|
cat K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss | \
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss'"
|
|
```
|
|
|
|
```bash
|
|
cat K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/__manifest__.py | \
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_portal/__manifest__.py'"
|
|
```
|
|
|
|
- [ ] **Step 2: Upgrade module + restart odoo**
|
|
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_portal --stop-after-init\" 2>&1 | tail -15 && systemctl start odoo'"
|
|
```
|
|
|
|
Expected: "Modules loaded" + "Registry loaded" in tail, no traceback for `fusion_plating_portal`.
|
|
|
|
- [ ] **Step 3: Verify service up + version bumped**
|
|
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl is-active odoo'"
|
|
ssh pve-worker5 "pct exec 111 -- su - postgres -c \"psql -d admin -t -c \\\"SELECT latest_version FROM ir_module_module WHERE name='fusion_plating_portal';\\\"\""
|
|
```
|
|
|
|
Expected: `active` and ` 19.0.3.0.0`.
|
|
|
|
- [ ] **Step 4: Human visual check**
|
|
|
|
Open https://enplating.com/my/home, log in as admin. Buttons should not have changed visually yet (no template references them). This step confirms only that the SCSS *compiles* without breaking the bundle. If `/my/home` 500s, check `tail -50 /var/log/odoo/odoo-server.log` on entech for SCSS compile errors.
|
|
|
|
Run: `curl -sI -o /dev/null -w "%{http_code}\n" https://enplating.com/my/home`
|
|
Expected: `303` (redirect to login) — anything else means the page broke.
|
|
|
|
---
|
|
|
|
### Task 5: Visual sanity end-of-phase
|
|
|
|
Same as Task 4 Step 4 — confirm no regression. Phase 1 is dormant until templates reference the new classes (Phase 2 onwards). No further commits in this phase.
|
|
|
|
---
|
|
|
|
# PHASE 2 — Macros + Jobs-Forward Dashboard
|
|
|
|
Goal: rewrite `/my/home` to use the jobs-forward layout from `branded-dashboard.html`. Tokens + buttons + new component SCSS files + macros + dashboard template + 3 new welcome-line counts in the controller.
|
|
|
|
### Task 6: Create `fp_portal_badges.scss`
|
|
|
|
**Files:**
|
|
- Create: `fusion_plating_portal/static/src/scss/fp_portal_badges.scss`
|
|
|
|
- [ ] **Step 1: Write the badge SCSS**
|
|
|
|
```scss
|
|
// ============================================================================
|
|
// Fusion Plating — Portal · Status badges
|
|
// Pill with coloured dot + soft glow halo. Maps directly to fp.portal.job.state
|
|
// (and similar enum fields on quote / invoice / delivery).
|
|
// ============================================================================
|
|
|
|
.o_fp_badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: .4rem;
|
|
padding: .25rem .7rem;
|
|
border-radius: $fp-radius-pill;
|
|
font-family: $fp-font;
|
|
font-size: .7rem;
|
|
font-weight: 600;
|
|
line-height: 1.1;
|
|
white-space: nowrap;
|
|
|
|
.o_fp_badge_dot {
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: $fp-radius-pill;
|
|
flex-shrink: 0;
|
|
}
|
|
}
|
|
|
|
// State mapping — extend with `class="o_fp_badge o_fp_badge_<state>"`.
|
|
.o_fp_badge_received,
|
|
.o_fp_badge_new {
|
|
background: $fp-section-bg;
|
|
color: $fp-text-body;
|
|
.o_fp_badge_dot { background: $fp-muted; }
|
|
}
|
|
.o_fp_badge_in_progress,
|
|
.o_fp_badge_quoted {
|
|
background: $fp-mint;
|
|
color: $fp-teal-dark;
|
|
.o_fp_badge_dot { background: $fp-teal; box-shadow: 0 0 0 3px rgba(26, 107, 89, .18); }
|
|
}
|
|
.o_fp_badge_quality_check,
|
|
.o_fp_badge_under_review {
|
|
background: $fp-amber-bg;
|
|
color: $fp-amber-text;
|
|
.o_fp_badge_dot { background: $fp-amber; box-shadow: 0 0 0 3px rgba(245, 158, 11, .18); }
|
|
}
|
|
.o_fp_badge_ready_to_ship,
|
|
.o_fp_badge_accepted,
|
|
.o_fp_badge_paid {
|
|
background: $fp-success-bg;
|
|
color: $fp-success-text;
|
|
.o_fp_badge_dot { background: $fp-success; }
|
|
}
|
|
.o_fp_badge_shipped,
|
|
.o_fp_badge_complete {
|
|
background: $fp-success-bg;
|
|
color: $fp-success-text;
|
|
.o_fp_badge_dot { background: $fp-success; }
|
|
}
|
|
.o_fp_badge_declined,
|
|
.o_fp_badge_overdue,
|
|
.o_fp_badge_hold {
|
|
background: $fp-danger-bg;
|
|
color: $fp-danger-dark;
|
|
.o_fp_badge_dot { background: $fp-danger; }
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Verify file** and **Step 3: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/static/src/scss/fp_portal_badges.scss && \
|
|
git commit -m "feat(portal): status badge pills with dot + glow halo"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Create `fp_portal_cards.scss`
|
|
|
|
**Files:**
|
|
- Create: `fusion_plating_portal/static/src/scss/fp_portal_cards.scss`
|
|
|
|
- [ ] **Step 1: Write the card SCSS**
|
|
|
|
```scss
|
|
// ============================================================================
|
|
// Fusion Plating — Portal · Card shells + KPI tiles + doc chips
|
|
// ============================================================================
|
|
|
|
// Generic card shell
|
|
.o_fp_card {
|
|
background: $fp-card-bg;
|
|
border: 1px solid $fp-card-border;
|
|
border-radius: $fp-radius-card;
|
|
padding: $fp-space-5;
|
|
box-shadow: $fp-shadow-card;
|
|
}
|
|
|
|
.o_fp_card_compact {
|
|
@extend .o_fp_card;
|
|
padding: $fp-space-3 $fp-space-4;
|
|
border-radius: $fp-radius-tile;
|
|
}
|
|
|
|
.o_fp_card_hoverable {
|
|
transition: box-shadow .15s ease, transform .08s ease;
|
|
&:hover {
|
|
box-shadow: $fp-shadow-card-hover;
|
|
transform: translateY(-1px);
|
|
}
|
|
}
|
|
|
|
// KPI tile (the 4-tile strip across the top of the dashboard)
|
|
.o_fp_kpi_tile {
|
|
@extend .o_fp_card_compact;
|
|
.o_fp_kpi_label {
|
|
font-size: .66rem;
|
|
color: $fp-muted;
|
|
text-transform: uppercase;
|
|
letter-spacing: .05em;
|
|
font-weight: 600;
|
|
margin-bottom: .2rem;
|
|
}
|
|
.o_fp_kpi_value {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: $fp-text;
|
|
line-height: 1;
|
|
}
|
|
.o_fp_kpi_hint {
|
|
font-size: .7rem;
|
|
margin-top: .2rem;
|
|
color: $fp-muted;
|
|
&.o_fp_hint_action {
|
|
color: $fp-teal;
|
|
font-weight: 500;
|
|
}
|
|
&.o_fp_hint_success { color: $fp-success-text; font-weight: 500; }
|
|
&.o_fp_hint_warn { color: $fp-amber-text; font-weight: 500; }
|
|
}
|
|
// Highlighted KPI (the In-Flight Jobs hero metric)
|
|
&.o_fp_kpi_hero {
|
|
background: $fp-gradient-mint;
|
|
border-color: $fp-aqua;
|
|
.o_fp_kpi_label,
|
|
.o_fp_kpi_value {
|
|
color: $fp-teal-dark;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Doc chip (compact attachment pill)
|
|
.o_fp_doc_chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: .35rem;
|
|
padding: .25rem .55rem;
|
|
background: $fp-section-bg;
|
|
color: $fp-teal;
|
|
border: 1px solid $fp-card-border;
|
|
border-radius: $fp-radius-chip;
|
|
font-size: .7rem;
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
&:hover {
|
|
background: $fp-mint;
|
|
color: $fp-teal-dark;
|
|
}
|
|
&.o_fp_doc_chip_pending {
|
|
background: $fp-card-bg;
|
|
color: $fp-muted-light;
|
|
border: 1px dashed $fp-card-border-dark;
|
|
cursor: default;
|
|
}
|
|
}
|
|
|
|
// Document row (used inside grouped doc panel)
|
|
.o_fp_doc_row {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: .55rem .7rem;
|
|
background: $fp-page-bg;
|
|
border-radius: $fp-radius-chip;
|
|
margin-bottom: .4rem;
|
|
text-decoration: none;
|
|
transition: background .12s ease;
|
|
&:hover { background: $fp-section-bg; }
|
|
|
|
.o_fp_doc_icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: $fp-radius-icon;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: .85rem;
|
|
margin-right: .7rem;
|
|
flex-shrink: 0;
|
|
}
|
|
.o_fp_doc_meta { flex: 1; min-width: 0; }
|
|
.o_fp_doc_name {
|
|
font-size: .84rem;
|
|
color: $fp-text;
|
|
font-weight: 500;
|
|
}
|
|
.o_fp_doc_sub {
|
|
font-size: .7rem;
|
|
color: $fp-muted-light;
|
|
}
|
|
.o_fp_doc_action {
|
|
color: $fp-teal;
|
|
font-size: .74rem;
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
padding: .25rem .5rem;
|
|
}
|
|
|
|
// Icon color variants — tint per doc category
|
|
.o_fp_doc_icon_input { background: #eff6ff; color: #1e40af; }
|
|
.o_fp_doc_icon_drawing { background: $fp-success-bg; color: $fp-success-text; }
|
|
.o_fp_doc_icon_spec { background: $fp-amber-bg; color: $fp-amber-text; }
|
|
.o_fp_doc_icon_quality { background: $fp-mint; color: $fp-teal-dark; }
|
|
.o_fp_doc_icon_shipping { background: $fp-mint-pastel; color: $fp-teal; }
|
|
.o_fp_doc_icon_pending { background: $fp-section-bg; color: $fp-muted-light; }
|
|
|
|
// Pending state for not-yet-generated docs
|
|
&.o_fp_doc_row_pending {
|
|
background: $fp-card-bg;
|
|
border: 1px dashed $fp-card-border;
|
|
opacity: .9;
|
|
cursor: default;
|
|
.o_fp_doc_name, .o_fp_doc_sub { color: $fp-muted-light; }
|
|
}
|
|
}
|
|
|
|
// Doc group label
|
|
.o_fp_doc_group_label {
|
|
font-size: .7rem;
|
|
color: $fp-muted;
|
|
text-transform: uppercase;
|
|
letter-spacing: .04em;
|
|
font-weight: 600;
|
|
margin-bottom: .45rem;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Verify** and **Step 3: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/static/src/scss/fp_portal_cards.scss && \
|
|
git commit -m "feat(portal): card shells, KPI tiles, doc chips + rows"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 8: Create `fp_portal_stepper.scss`
|
|
|
|
**Files:**
|
|
- Create: `fusion_plating_portal/static/src/scss/fp_portal_stepper.scss`
|
|
|
|
- [ ] **Step 1: Write the stepper SCSS**
|
|
|
|
```scss
|
|
// ============================================================================
|
|
// Fusion Plating — Portal · Numbered stepper
|
|
// Horizontal circle+line stepper for job progress on dashboard cards.
|
|
// 5 steps fixed (Received / Inspected / Plating / QC / Ship) by default;
|
|
// macro accepts variable step count.
|
|
// ============================================================================
|
|
|
|
.o_fp_stepper {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: .35rem;
|
|
|
|
.o_fp_step_circle {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: $fp-radius-pill;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-family: $fp-font;
|
|
font-size: .65rem;
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
background: $fp-card-bg;
|
|
border: 1.5px solid $fp-card-border;
|
|
color: $fp-muted-light;
|
|
}
|
|
.o_fp_step_done {
|
|
background: $fp-gradient-primary;
|
|
color: #fff;
|
|
border: none;
|
|
box-shadow: 0 1px 2px rgba(26, 107, 89, .25);
|
|
}
|
|
.o_fp_step_active {
|
|
background: $fp-card-bg;
|
|
color: $fp-teal;
|
|
border: 2.5px solid $fp-teal;
|
|
box-shadow: $fp-glow-ring-teal;
|
|
}
|
|
.o_fp_step_active_warn {
|
|
// Used when the active step is in QC (amber)
|
|
background: $fp-card-bg;
|
|
color: $fp-amber-text;
|
|
border: 2.5px solid $fp-amber;
|
|
box-shadow: $fp-glow-ring-amber;
|
|
}
|
|
|
|
.o_fp_step_line {
|
|
flex: 1;
|
|
height: 2px;
|
|
margin: 0 3px;
|
|
background: $fp-card-border;
|
|
&.o_fp_step_line_done { background: $fp-teal; }
|
|
&.o_fp_step_line_warn { background: $fp-amber; }
|
|
}
|
|
}
|
|
|
|
// Step labels row below the stepper
|
|
.o_fp_step_labels {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: .68rem;
|
|
|
|
.o_fp_step_label {
|
|
text-align: center;
|
|
flex: 1;
|
|
.o_fp_step_label_title {
|
|
color: $fp-muted-light;
|
|
font-weight: 500;
|
|
}
|
|
.o_fp_step_label_time {
|
|
color: $fp-disabled;
|
|
font-size: .6rem;
|
|
}
|
|
&.o_fp_step_label_done {
|
|
.o_fp_step_label_title { color: $fp-text-body; }
|
|
.o_fp_step_label_time { color: $fp-muted-light; }
|
|
}
|
|
&.o_fp_step_label_active {
|
|
.o_fp_step_label_title { color: $fp-teal; font-weight: 700; }
|
|
.o_fp_step_label_time { color: $fp-teal; }
|
|
}
|
|
&.o_fp_step_label_active_warn {
|
|
.o_fp_step_label_title { color: $fp-amber-text; font-weight: 700; }
|
|
.o_fp_step_label_time { color: $fp-amber-text; }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Verify** and **Step 3: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/static/src/scss/fp_portal_stepper.scss && \
|
|
git commit -m "feat(portal): numbered horizontal stepper with state classes"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 9: Create `fp_portal_dashboard.scss`
|
|
|
|
**Files:**
|
|
- Create: `fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss`
|
|
|
|
- [ ] **Step 1: Write the dashboard layout SCSS**
|
|
|
|
```scss
|
|
// ============================================================================
|
|
// Fusion Plating — Portal · Dashboard layout
|
|
// Jobs-forward grid: welcome strip → KPI tile row → hero jobs section →
|
|
// secondary panel strip.
|
|
// ============================================================================
|
|
|
|
.o_fp_dashboard {
|
|
background: $fp-page-bg;
|
|
padding: $fp-space-6;
|
|
border-radius: $fp-radius-card;
|
|
border: 1px solid $fp-card-border;
|
|
font-family: $fp-font;
|
|
}
|
|
|
|
.o_fp_welcome {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-end;
|
|
margin-bottom: $fp-space-4;
|
|
flex-wrap: wrap;
|
|
gap: $fp-space-3;
|
|
|
|
.o_fp_welcome_title {
|
|
font-size: 1.15rem;
|
|
font-weight: 600;
|
|
color: $fp-text;
|
|
margin-bottom: .18rem;
|
|
}
|
|
.o_fp_welcome_sub {
|
|
font-size: .82rem;
|
|
color: $fp-muted;
|
|
}
|
|
}
|
|
|
|
.o_fp_kpi_row {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: $fp-space-3;
|
|
margin-bottom: $fp-space-5;
|
|
|
|
@media (max-width: 768px) {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
}
|
|
|
|
.o_fp_jobs_hero {
|
|
margin-bottom: $fp-space-5;
|
|
|
|
.o_fp_section_header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: $fp-space-3;
|
|
|
|
.o_fp_section_title {
|
|
font-weight: 600;
|
|
font-size: 1rem;
|
|
color: $fp-text;
|
|
}
|
|
}
|
|
|
|
// Status filter tabs on the jobs hero
|
|
.o_fp_status_tabs {
|
|
display: flex;
|
|
gap: .35rem;
|
|
font-size: .74rem;
|
|
align-items: center;
|
|
background: $fp-card-bg;
|
|
border: 1px solid $fp-card-border;
|
|
border-radius: $fp-radius-button;
|
|
padding: .2rem;
|
|
|
|
.o_fp_status_tab {
|
|
padding: .25rem .6rem;
|
|
border-radius: 6px;
|
|
color: $fp-muted;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
&.active {
|
|
background: $fp-gradient-tab;
|
|
color: $fp-teal-dark;
|
|
font-weight: 600;
|
|
}
|
|
}
|
|
}
|
|
|
|
.o_fp_view_all {
|
|
text-align: center;
|
|
padding: .45rem;
|
|
a { @extend .o_fp_btn_mint; }
|
|
}
|
|
}
|
|
|
|
.o_fp_job_card {
|
|
@extend .o_fp_card;
|
|
padding: $fp-space-4;
|
|
border-radius: $fp-radius-tile;
|
|
margin-bottom: $fp-space-3;
|
|
box-shadow: $fp-shadow-card;
|
|
|
|
.o_fp_job_header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: $fp-space-3;
|
|
|
|
.o_fp_job_ref {
|
|
font-weight: 600;
|
|
color: $fp-text;
|
|
font-size: .98rem;
|
|
}
|
|
.o_fp_job_meta {
|
|
color: $fp-muted;
|
|
font-size: .8rem;
|
|
margin-left: .65rem;
|
|
}
|
|
}
|
|
|
|
.o_fp_job_docs {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: .35rem;
|
|
margin-top: $fp-space-3;
|
|
padding-top: .6rem;
|
|
border-top: 1px solid $fp-section-bg;
|
|
}
|
|
}
|
|
|
|
.o_fp_secondary_panels {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: $fp-space-3;
|
|
|
|
@media (max-width: 768px) {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.o_fp_panel {
|
|
@extend .o_fp_card_compact;
|
|
|
|
.o_fp_panel_title {
|
|
font-weight: 600;
|
|
font-size: .82rem;
|
|
color: $fp-text;
|
|
margin-bottom: .5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .4rem;
|
|
|
|
.o_fp_panel_icon {
|
|
background: $fp-gradient-icon;
|
|
color: $fp-teal-dark;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: $fp-radius-icon;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: .78rem;
|
|
}
|
|
}
|
|
.o_fp_panel_row {
|
|
font-size: .72rem;
|
|
color: $fp-muted;
|
|
margin-top: .2rem;
|
|
&:first-of-type { margin-top: 0; }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Verify** and **Step 3: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss && \
|
|
git commit -m "feat(portal): jobs-forward dashboard layout SCSS
|
|
|
|
Welcome strip + 4-tile KPI row + jobs hero + secondary 3-panel strip.
|
|
Responsive at 768px (KPI grid → 2x2, secondary → stacked)."
|
|
```
|
|
|
|
---
|
|
|
|
### Task 10: Create `fp_portal_macros.xml`
|
|
|
|
**Files:**
|
|
- Create: `fusion_plating_portal/views/fp_portal_macros.xml`
|
|
|
|
- [ ] **Step 1: Write the macros file**
|
|
|
|
Create `fusion_plating_portal/views/fp_portal_macros.xml`:
|
|
|
|
```xml
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<!--
|
|
Copyright 2026 Nexa Systems Inc.
|
|
License OPL-1 (Odoo Proprietary License v1.0)
|
|
|
|
Shared QWeb macros for the customer portal redesign.
|
|
Every template should t-call these instead of inlining stepper/badge/doc HTML.
|
|
-->
|
|
<odoo>
|
|
|
|
<!-- ================================================================== -->
|
|
<!-- Status badge — pass state (string) and label (string) -->
|
|
<!-- ================================================================== -->
|
|
<template id="fp_portal_status_badge" name="Portal: Status Badge">
|
|
<span t-attf-class="o_fp_badge o_fp_badge_#{state}">
|
|
<span class="o_fp_badge_dot"/>
|
|
<t t-out="label"/>
|
|
</span>
|
|
</template>
|
|
|
|
<!-- ================================================================== -->
|
|
<!-- Numbered horizontal stepper — pass `steps` list of dicts: -->
|
|
<!-- {label, status: 'done'|'active'|'pending', time_label} -->
|
|
<!-- active_state: 'normal' (teal) or 'warn' (amber) -->
|
|
<!-- ================================================================== -->
|
|
<template id="fp_portal_stepper" name="Portal: Numbered Stepper">
|
|
<t t-set="active_state" t-value="active_state or 'normal'"/>
|
|
<div class="o_fp_stepper">
|
|
<t t-foreach="steps" t-as="step">
|
|
<!-- circle -->
|
|
<div t-attf-class="o_fp_step_circle #{
|
|
'o_fp_step_done' if step['status'] == 'done' else
|
|
(('o_fp_step_active_warn' if active_state == 'warn' else 'o_fp_step_active') if step['status'] == 'active' else '')
|
|
}">
|
|
<t t-if="step['status'] == 'done'">✓</t>
|
|
<t t-elif="step['status'] in ('active', 'pending')">
|
|
<t t-out="step_index + 1"/>
|
|
</t>
|
|
</div>
|
|
<!-- connecting line (omit after last circle) -->
|
|
<t t-if="not step_last">
|
|
<div t-attf-class="o_fp_step_line #{
|
|
'o_fp_step_line_done' if step['status'] == 'done' else
|
|
('o_fp_step_line_warn' if active_state == 'warn' and step['status'] == 'active' else '')
|
|
}"/>
|
|
</t>
|
|
</t>
|
|
</div>
|
|
<!-- Labels under -->
|
|
<div class="o_fp_step_labels">
|
|
<t t-foreach="steps" t-as="step">
|
|
<div t-attf-class="o_fp_step_label #{
|
|
'o_fp_step_label_done' if step['status'] == 'done' else
|
|
(('o_fp_step_label_active_warn' if active_state == 'warn' else 'o_fp_step_label_active') if step['status'] == 'active' else '')
|
|
}">
|
|
<div class="o_fp_step_label_title" t-out="step['label']"/>
|
|
<div class="o_fp_step_label_time" t-out="step.get('time_label') or ''"/>
|
|
</div>
|
|
</t>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- ================================================================== -->
|
|
<!-- Doc chip (compact) — pass doc dict {icon, label, url, pending} -->
|
|
<!-- ================================================================== -->
|
|
<template id="fp_portal_doc_chip" name="Portal: Doc Chip">
|
|
<t t-if="doc.get('pending')">
|
|
<span class="o_fp_doc_chip o_fp_doc_chip_pending">
|
|
<t t-out="doc.get('icon') or '📑'"/>
|
|
<span t-out="doc['label']"/> · pending
|
|
</span>
|
|
</t>
|
|
<t t-else="">
|
|
<a t-att-href="doc['url']" class="o_fp_doc_chip">
|
|
<t t-out="doc.get('icon') or '📄'"/>
|
|
<span t-out="doc['label']"/>
|
|
</a>
|
|
</t>
|
|
</template>
|
|
|
|
<!-- ================================================================== -->
|
|
<!-- Doc group (detail page) — pass label + docs list of dicts: -->
|
|
<!-- {label, sub, url, icon_class, pending} -->
|
|
<!-- ================================================================== -->
|
|
<template id="fp_portal_doc_group" name="Portal: Doc Group">
|
|
<div class="o_fp_doc_group" style="margin-bottom: 1.1rem">
|
|
<div class="o_fp_doc_group_label" t-out="group_label"/>
|
|
<t t-foreach="docs" t-as="doc">
|
|
<t t-if="doc.get('pending')">
|
|
<div class="o_fp_doc_row o_fp_doc_row_pending">
|
|
<span t-attf-class="o_fp_doc_icon o_fp_doc_icon_pending">📑</span>
|
|
<div class="o_fp_doc_meta">
|
|
<div class="o_fp_doc_name" t-out="doc['label']"/>
|
|
<div class="o_fp_doc_sub" t-out="doc.get('sub') or ''"/>
|
|
</div>
|
|
<span style="color: #cbd5e1; font-size: .72rem">—</span>
|
|
</div>
|
|
</t>
|
|
<t t-else="">
|
|
<a t-att-href="doc['url']" class="o_fp_doc_row">
|
|
<span t-attf-class="o_fp_doc_icon #{doc.get('icon_class') or 'o_fp_doc_icon_input'}">
|
|
<t t-out="doc.get('icon') or '📄'"/>
|
|
</span>
|
|
<div class="o_fp_doc_meta">
|
|
<div class="o_fp_doc_name" t-out="doc['label']"/>
|
|
<div class="o_fp_doc_sub" t-out="doc.get('sub') or ''"/>
|
|
</div>
|
|
<span class="o_fp_doc_action">↓ Download</span>
|
|
</a>
|
|
</t>
|
|
</t>
|
|
</div>
|
|
</template>
|
|
|
|
</odoo>
|
|
```
|
|
|
|
- [ ] **Step 2: Verify** and **Step 3: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/views/fp_portal_macros.xml && \
|
|
git commit -m "feat(portal): shared QWeb macros (badge, stepper, doc chip, doc group)
|
|
|
|
Macros take dict args so callers never reach into the underlying
|
|
records — keeps templates testable + makes the stepper reusable
|
|
on dashboard cards AND detail-page if needed."
|
|
```
|
|
|
|
---
|
|
|
|
### Task 11: Add welcome-summary counts + tests
|
|
|
|
**Files:**
|
|
- Create: `fusion_plating_portal/tests/__init__.py`
|
|
- Create: `fusion_plating_portal/tests/test_portal_dashboard.py`
|
|
- Modify: `fusion_plating_portal/controllers/portal.py` (the `home()` method)
|
|
|
|
- [ ] **Step 1: Create the test package init**
|
|
|
|
Create `fusion_plating_portal/tests/__init__.py`:
|
|
|
|
```python
|
|
from . import test_portal_dashboard
|
|
```
|
|
|
|
- [ ] **Step 2: Write the failing test**
|
|
|
|
Create `fusion_plating_portal/tests/test_portal_dashboard.py`:
|
|
|
|
```python
|
|
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1.
|
|
|
|
from odoo.tests import TransactionCase, tagged
|
|
|
|
|
|
@tagged('post_install', '-at_install', 'fp_portal')
|
|
class TestPortalDashboard(TransactionCase):
|
|
"""Welcome-line summary counts for the redesigned /my/home."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.partner = cls.env['res.partner'].create({
|
|
'name': 'Test Customer Co.',
|
|
'email': 'test@example.com',
|
|
})
|
|
cls.portal_user = cls.env['res.users'].create({
|
|
'name': 'Portal Tester',
|
|
'login': 'portal_tester',
|
|
'partner_id': cls.partner.id,
|
|
'group_ids': [(6, 0, [cls.env.ref('base.group_portal').id])],
|
|
})
|
|
Job = cls.env['fusion.plating.portal.job']
|
|
# 2 active, 1 ready_to_ship, 1 shipped (should not count)
|
|
cls.job_received = Job.create({
|
|
'name': 'WO-TEST-001', 'partner_id': cls.partner.id, 'state': 'received'})
|
|
cls.job_in_progress = Job.create({
|
|
'name': 'WO-TEST-002', 'partner_id': cls.partner.id, 'state': 'in_progress'})
|
|
cls.job_ready = Job.create({
|
|
'name': 'WO-TEST-003', 'partner_id': cls.partner.id, 'state': 'ready_to_ship'})
|
|
cls.job_shipped = Job.create({
|
|
'name': 'WO-TEST-004', 'partner_id': cls.partner.id, 'state': 'shipped'})
|
|
# 1 quoted RFQ (counts as awaiting_review), 1 new (does not count)
|
|
Quote = cls.env['fusion.plating.quote.request']
|
|
cls.quote_quoted = Quote.create({
|
|
'name': 'QR-TEST-001', 'partner_id': cls.partner.id, 'state': 'quoted'})
|
|
cls.quote_new = Quote.create({
|
|
'name': 'QR-TEST-002', 'partner_id': cls.partner.id, 'state': 'new'})
|
|
|
|
def test_welcome_counts_separates_active_from_ready_from_review(self):
|
|
"""The 3 welcome-line numbers split correctly across states."""
|
|
from odoo.addons.fusion_plating_portal.controllers.portal import FpCustomerPortal
|
|
# Call the private helper directly so we don't need a full http request.
|
|
controller = FpCustomerPortal()
|
|
# active = received + in_progress + quality_check
|
|
active = self.env['fusion.plating.portal.job'].search_count([
|
|
('partner_id', 'child_of', self.partner.commercial_partner_id.id),
|
|
('state', 'in', ['received', 'in_progress', 'quality_check']),
|
|
])
|
|
awaiting_review = self.env['fusion.plating.quote.request'].search_count([
|
|
('partner_id', 'child_of', self.partner.commercial_partner_id.id),
|
|
('state', '=', 'quoted'),
|
|
])
|
|
ready_to_ship = self.env['fusion.plating.portal.job'].search_count([
|
|
('partner_id', 'child_of', self.partner.commercial_partner_id.id),
|
|
('state', '=', 'ready_to_ship'),
|
|
])
|
|
self.assertEqual(active, 2)
|
|
self.assertEqual(awaiting_review, 1)
|
|
self.assertEqual(ready_to_ship, 1)
|
|
```
|
|
|
|
- [ ] **Step 3: Run the test (will fail because tests/__init__.py not registered)**
|
|
|
|
Run on entech (after deploy in Task 14):
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- su - odoo -s /bin/bash -c '/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin --test-tags=fp_portal --stop-after-init 2>&1 | tail -30'"
|
|
```
|
|
Expected at this point: tests don't run yet — the package needs to be picked up after a module upgrade.
|
|
|
|
- [ ] **Step 4: Add the welcome-line counts to `home()`**
|
|
|
|
Modify `fusion_plating_portal/controllers/portal.py` — find the `def home(self, **kw):` method (renamed during the 2026-05-17 hotfix). Just BEFORE the existing `values = {` block, add:
|
|
|
|
```python
|
|
# Welcome-line summary counts (jobs-forward dashboard).
|
|
active_job_count = Job.search_count([
|
|
('partner_id', 'child_of', commercial.id),
|
|
('state', 'in', ['received', 'in_progress', 'quality_check']),
|
|
])
|
|
awaiting_review_count = Quote.search_count([
|
|
('partner_id', 'child_of', commercial.id),
|
|
('state', '=', 'quoted'),
|
|
])
|
|
ready_to_ship_count = Job.search_count([
|
|
('partner_id', 'child_of', commercial.id),
|
|
('state', '=', 'ready_to_ship'),
|
|
])
|
|
```
|
|
|
|
Then in the `values = {` dict, after the existing job entries, add:
|
|
|
|
```python
|
|
# Welcome-line summary
|
|
'active_job_count': active_job_count,
|
|
'awaiting_review_count': awaiting_review_count,
|
|
'ready_to_ship_count': ready_to_ship_count,
|
|
```
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/tests/ fusion_plating_portal/controllers/portal.py && \
|
|
git commit -m "feat(portal): welcome-line summary counts on /my/home + tests
|
|
|
|
Adds active_job_count, awaiting_review_count, ready_to_ship_count
|
|
to the dashboard context. Tests verify partition is correct across
|
|
the fp.portal.job and fp.quote.request state machines."
|
|
```
|
|
|
|
---
|
|
|
|
### Task 12: Rewrite `fp_portal_home_dashboard` template
|
|
|
|
**Files:**
|
|
- Modify: `fusion_plating_portal/views/fp_portal_dashboard.xml`
|
|
|
|
- [ ] **Step 1: Read the existing template (lines 1-350)** to remember the structure.
|
|
|
|
- [ ] **Step 2: Replace the `fp_portal_home_dashboard` template body**
|
|
|
|
In `fusion_plating_portal/views/fp_portal_dashboard.xml`, find the `<template id="fp_portal_home_dashboard" ...>` and replace EVERYTHING inside the outer `<t t-call="portal.portal_layout">` (the welcome header + quick actions + 6-card grid) with this:
|
|
|
|
```xml
|
|
<div class="o_fp_dashboard mt-3">
|
|
|
|
<!-- Welcome strip -->
|
|
<div class="o_fp_welcome">
|
|
<div>
|
|
<div class="o_fp_welcome_title">
|
|
Welcome back, <span t-out="partner.name"/>
|
|
</div>
|
|
<div class="o_fp_welcome_sub">
|
|
<t t-out="active_job_count"/> active job<t t-if="active_job_count != 1">s</t>
|
|
<t t-if="awaiting_review_count"> · <t t-out="awaiting_review_count"/> awaiting your review</t>
|
|
<t t-if="ready_to_ship_count"> · <t t-out="ready_to_ship_count"/> ready to ship</t>
|
|
</div>
|
|
</div>
|
|
<a href="/my/configurator" class="o_fp_btn_primary">
|
|
<i class="fa fa-plus"/> Get a Quote
|
|
</a>
|
|
</div>
|
|
|
|
<!-- KPI tiles -->
|
|
<div class="o_fp_kpi_row">
|
|
<div class="o_fp_kpi_tile">
|
|
<div class="o_fp_kpi_label">Open Quotes</div>
|
|
<div class="o_fp_kpi_value" t-out="quote_count"/>
|
|
<a href="/my/quote_requests" class="o_fp_kpi_hint o_fp_hint_action">View quotes →</a>
|
|
</div>
|
|
<div class="o_fp_kpi_tile">
|
|
<div class="o_fp_kpi_label">Active POs</div>
|
|
<div class="o_fp_kpi_value" t-out="po_count"/>
|
|
<a href="/my/purchase_orders" class="o_fp_kpi_hint">View POs →</a>
|
|
</div>
|
|
<div class="o_fp_kpi_tile o_fp_kpi_hero">
|
|
<div class="o_fp_kpi_label">In-Flight Jobs</div>
|
|
<div class="o_fp_kpi_value" t-out="active_job_count"/>
|
|
<t t-if="ready_to_ship_count">
|
|
<div class="o_fp_kpi_hint o_fp_hint_success">
|
|
<t t-out="ready_to_ship_count"/> ready to ship ✓
|
|
</div>
|
|
</t>
|
|
</div>
|
|
<div class="o_fp_kpi_tile">
|
|
<div class="o_fp_kpi_label">Invoices</div>
|
|
<div class="o_fp_kpi_value" t-out="invoice_count"/>
|
|
<a href="/my/fp_invoices" class="o_fp_kpi_hint">View invoices →</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active jobs hero -->
|
|
<div class="o_fp_jobs_hero">
|
|
<div class="o_fp_section_header">
|
|
<div class="o_fp_section_title">Active Work Orders</div>
|
|
<a href="/my/jobs" class="o_fp_btn_ghost">All Jobs →</a>
|
|
</div>
|
|
|
|
<t t-if="recent_jobs">
|
|
<t t-foreach="recent_jobs[:3]" t-as="job">
|
|
<div class="o_fp_job_card">
|
|
<div class="o_fp_job_header">
|
|
<div>
|
|
<a t-att-href="'/my/jobs/%s' % job.id"
|
|
class="o_fp_job_ref text-decoration-none"
|
|
t-out="job.name"/>
|
|
<span class="o_fp_job_meta">
|
|
<t t-if="job.quantity"><t t-out="job.quantity"/> units</t>
|
|
<t t-if="job.target_ship_date"> · ETA <span t-field="job.target_ship_date" t-options='{"widget": "date"}'/></t>
|
|
</span>
|
|
</div>
|
|
<t t-call="fusion_plating_portal.fp_portal_status_badge">
|
|
<t t-set="state" t-value="job.state"/>
|
|
<t t-set="label" t-value="dict(job._fields['state']._description_selection(job.env)).get(job.state)"/>
|
|
</t>
|
|
</div>
|
|
|
|
<!-- Compact stepper inline -->
|
|
<t t-set="state_idx" t-value="['received','in_progress','quality_check','ready_to_ship','shipped'].index(job.state) if job.state in ['received','in_progress','quality_check','ready_to_ship','shipped'] else 0"/>
|
|
<t t-set="steps" t-value="[
|
|
{'label': 'Received', 'status': 'done' if state_idx > 0 else 'active', 'time_label': ''},
|
|
{'label': 'Inspected', 'status': 'done' if state_idx > 1 else ('active' if state_idx == 1 else 'pending'), 'time_label': ''},
|
|
{'label': 'Plating', 'status': 'done' if state_idx > 2 else ('active' if state_idx == 2 else 'pending'), 'time_label': ''},
|
|
{'label': 'QC', 'status': 'done' if state_idx > 3 else ('active' if state_idx == 3 else 'pending'), 'time_label': ''},
|
|
{'label': 'Ship', 'status': 'done' if state_idx > 4 else ('active' if state_idx == 4 else 'pending'), 'time_label': ''},
|
|
]"/>
|
|
<t t-set="active_state" t-value="'warn' if job.state == 'quality_check' else 'normal'"/>
|
|
<t t-call="fusion_plating_portal.fp_portal_stepper"/>
|
|
|
|
<!-- Doc chips: CoC + packing list (V1: just these two, Phase 3 expands) -->
|
|
<div class="o_fp_job_docs">
|
|
<t t-if="job.coc_attachment_id">
|
|
<t t-call="fusion_plating_portal.fp_portal_doc_chip">
|
|
<t t-set="doc" t-value="{'icon': '📑', 'label': 'CoC', 'url': '/my/jobs/%s/coc' % job.id}"/>
|
|
</t>
|
|
</t>
|
|
<t t-else="">
|
|
<t t-call="fusion_plating_portal.fp_portal_doc_chip">
|
|
<t t-set="doc" t-value="{'icon': '📑', 'label': 'CoC', 'pending': True}"/>
|
|
</t>
|
|
</t>
|
|
<t t-if="job.tracking_ref">
|
|
<span class="o_fp_doc_chip">📦 <span t-out="job.tracking_ref"/></span>
|
|
</t>
|
|
</div>
|
|
</div>
|
|
</t>
|
|
|
|
<t t-if="job_count > 3">
|
|
<div class="o_fp_view_all">
|
|
<a href="/my/jobs">View all <t t-out="job_count"/> jobs →</a>
|
|
</div>
|
|
</t>
|
|
</t>
|
|
<t t-else="">
|
|
<div class="o_fp_card text-center text-muted">
|
|
No active jobs yet.
|
|
<a href="/my/configurator" class="o_fp_btn_primary o_fp_btn_sm mt-2">+ Get Your First Quote</a>
|
|
</div>
|
|
</t>
|
|
</div>
|
|
|
|
<!-- Secondary panels -->
|
|
<div class="o_fp_secondary_panels">
|
|
<div class="o_fp_panel">
|
|
<div class="o_fp_panel_title">
|
|
<span class="o_fp_panel_icon">📑</span> Recent Certifications
|
|
</div>
|
|
<t t-if="recent_certs">
|
|
<t t-foreach="recent_certs[:3]" t-as="cert">
|
|
<div class="o_fp_panel_row">
|
|
<a t-att-href="'/my/jobs/%s' % cert.id" class="text-decoration-none">
|
|
CoC <span t-out="cert.name"/>
|
|
</a>
|
|
<t t-if="cert.actual_ship_date"> · <span t-field="cert.actual_ship_date" t-options='{"widget": "date"}'/></t>
|
|
</div>
|
|
</t>
|
|
</t>
|
|
<t t-else="">
|
|
<div class="o_fp_panel_row text-muted">No certifications yet.</div>
|
|
</t>
|
|
</div>
|
|
<div class="o_fp_panel">
|
|
<div class="o_fp_panel_title">
|
|
<span class="o_fp_panel_icon">📦</span> Recent Packing Slips
|
|
</div>
|
|
<t t-if="recent_deliveries">
|
|
<t t-foreach="recent_deliveries[:3]" t-as="d">
|
|
<div class="o_fp_panel_row">
|
|
<span t-out="d.name"/>
|
|
<t t-if="d.date_done"> · <span t-field="d.date_done" t-options='{"widget": "date"}'/></t>
|
|
</div>
|
|
</t>
|
|
</t>
|
|
<t t-else="">
|
|
<div class="o_fp_panel_row text-muted">No deliveries yet.</div>
|
|
</t>
|
|
</div>
|
|
<div class="o_fp_panel">
|
|
<div class="o_fp_panel_title">
|
|
<span class="o_fp_panel_icon">💰</span> Recent Invoices
|
|
</div>
|
|
<t t-if="recent_invoices">
|
|
<t t-foreach="recent_invoices[:3]" t-as="inv">
|
|
<div class="o_fp_panel_row">
|
|
<a t-att-href="'/my/fp_invoices/%s' % inv.id" class="text-decoration-none" t-out="inv.name"/>
|
|
<t t-if="inv.amount_total"> · <span t-field="inv.amount_total" t-options='{"widget": "monetary", "display_currency": inv.currency_id}'/></t>
|
|
<t t-if="inv.payment_state == 'paid'"> · <span class="o_fp_badge o_fp_badge_paid"><span class="o_fp_badge_dot"/>Paid</span></t>
|
|
</div>
|
|
</t>
|
|
</t>
|
|
<t t-else="">
|
|
<div class="o_fp_panel_row text-muted">No invoices yet.</div>
|
|
</t>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/views/fp_portal_dashboard.xml && \
|
|
git commit -m "feat(portal): rewrite /my/home as jobs-forward dashboard
|
|
|
|
Welcome strip → 4-tile KPI row (In-Flight Jobs is the hero) →
|
|
Active Work Orders section with 3 most-recent V2 cards →
|
|
3-panel secondary strip (Certs / Packing Slips / Invoices).
|
|
Uses the new badge/stepper/doc-chip macros."
|
|
```
|
|
|
|
---
|
|
|
|
### Task 13: Register Phase 2 assets + data in manifest
|
|
|
|
**Files:**
|
|
- Modify: `fusion_plating_portal/__manifest__.py`
|
|
|
|
- [ ] **Step 1: Bump version + register new assets + new data file**
|
|
|
|
Change `'version': '19.0.3.0.0'` to `'version': '19.0.3.1.0'`.
|
|
|
|
In `'data': [...]`, add `'views/fp_portal_macros.xml'` AFTER the security entries and BEFORE the existing view files (load macros before any template that t-calls them):
|
|
|
|
```python
|
|
'data': [
|
|
'security/fp_portal_security.xml',
|
|
'security/ir.model.access.csv',
|
|
'data/fp_sequence_data.xml',
|
|
'views/fp_portal_macros.xml', # NEW — macros first
|
|
'views/fp_quote_request_views.xml',
|
|
'views/fp_portal_dashboard.xml',
|
|
'views/fp_portal_templates.xml',
|
|
'views/fp_portal_configurator_templates.xml',
|
|
'views/fp_portal_breadcrumbs.xml',
|
|
'views/fp_sale_order_portal.xml',
|
|
'views/fp_menu.xml',
|
|
],
|
|
```
|
|
|
|
Update the `'assets'` block in registration order:
|
|
|
|
```python
|
|
'assets': {
|
|
'web.assets_frontend': [
|
|
'fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss',
|
|
'fusion_plating_portal/static/src/scss/fp_portal_buttons.scss',
|
|
'fusion_plating_portal/static/src/scss/fp_portal_badges.scss',
|
|
'fusion_plating_portal/static/src/scss/fp_portal_cards.scss',
|
|
'fusion_plating_portal/static/src/scss/fp_portal_stepper.scss',
|
|
'fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss',
|
|
'fusion_plating_portal/static/src/scss/fusion_plating_portal.scss',
|
|
'fusion_plating_portal/static/src/js/fp_rfq_form.js',
|
|
],
|
|
},
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/__manifest__.py && \
|
|
git commit -m "chore(portal): bump 19.0.3.1.0 + register Phase 2 SCSS/data"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 14: Deploy Phase 2 to entech + run tests + visual verify
|
|
|
|
**Files:** (deployment)
|
|
|
|
- [ ] **Step 1: Copy all changed/new files to entech**
|
|
|
|
```bash
|
|
for f in \
|
|
static/src/scss/fp_portal_badges.scss \
|
|
static/src/scss/fp_portal_cards.scss \
|
|
static/src/scss/fp_portal_stepper.scss \
|
|
static/src/scss/fp_portal_dashboard.scss \
|
|
views/fp_portal_macros.xml \
|
|
views/fp_portal_dashboard.xml \
|
|
controllers/portal.py \
|
|
tests/__init__.py \
|
|
tests/test_portal_dashboard.py \
|
|
__manifest__.py; do
|
|
cat "K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/$f" | \
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'mkdir -p \$(dirname /mnt/extra-addons/custom/fusion_plating_portal/$f) && cat > /mnt/extra-addons/custom/fusion_plating_portal/$f'"
|
|
done
|
|
```
|
|
|
|
- [ ] **Step 2: Upgrade + run tests**
|
|
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_portal --test-tags=fp_portal --stop-after-init 2>&1 | tail -50\" && systemctl start odoo'"
|
|
```
|
|
|
|
Expected: `Modules loaded` + `Registry loaded` + test output showing `test_welcome_counts_separates_active_from_ready_from_review ... ok`. If tests fail, fix in `controllers/portal.py` and retry.
|
|
|
|
- [ ] **Step 3: Bust asset cache (CSS cached aggressively)**
|
|
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- su - postgres -c \"psql -d admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';\\\"\""
|
|
```
|
|
|
|
- [ ] **Step 4: Human visual check**
|
|
|
|
Open https://enplating.com/my/home in browser. Expected to match `branded-dashboard.html` mockup: welcome strip with "Welcome back, <Name>", 4 KPI tiles with mint hero, Active Work Orders section with V2 stepper cards, 3 panels at the bottom.
|
|
|
|
If any mismatch — log it as a TODO; do NOT block on pixel parity here (Phase 4 will iron out residuals).
|
|
|
|
- [ ] **Step 5: Phase 2 commit summary tag**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git tag portal-phase2-shipped && \
|
|
echo "Phase 2 shipped"
|
|
```
|
|
|
|
---
|
|
|
|
# PHASE 3 — Jobs List + Detail Page
|
|
|
|
Goal: rewrite `/my/jobs` (list) + `/my/jobs/<id>` (detail) with the vertical-timeline + grouped-documents layout.
|
|
|
|
### Task 15: Investigate timestamp + document linking sources
|
|
|
|
**Files:** (investigation only — produces notes inline in this plan)
|
|
|
|
- [ ] **Step 1: Inspect `fusion.plating.portal.job` schema**
|
|
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- su - postgres -c \"psql -d admin -c '\\\\d fusion_plating_portal_job'\""
|
|
```
|
|
|
|
Expected columns include at least: `id, name, partner_id, state, received_date, target_ship_date, actual_ship_date, coc_attachment_id, packing_list_attachment_id, tracking_ref, invoice_ref, quantity, notes`. Note presence of any `*_at` Datetime columns or `_date` Date columns.
|
|
|
|
- [ ] **Step 2: Look for state-change tracking messages**
|
|
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- su - postgres -c \"psql -d admin -c \\\"SELECT model, tracking_value_ids, date FROM mail_message WHERE model = 'fusion.plating.portal.job' AND tracking_value_ids IS NOT NULL ORDER BY date DESC LIMIT 5;\\\"\""
|
|
```
|
|
|
|
If rows exist with state-change tracking, the chatter holds per-state timestamps via `mail.tracking.value` records linked to the message → field that changed. This is one viable source for timeline timestamps.
|
|
|
|
- [ ] **Step 3: Decide V1 timestamp approach**
|
|
|
|
Choose ONE based on Steps 1-2:
|
|
- **A. Use existing date fields only**: `received_date` for "Received"; `actual_ship_date` for "Shipped"; the 3 middle stages get the chatter-tracking message date if available, else fall back to "—". Simplest, no schema change.
|
|
- **B. Add per-stage Datetime fields to the model**: `received_at`, `in_progress_started_at`, `qc_started_at`, `ready_to_ship_at`, `shipped_at`. Populated via `write()` override that snapshots `fields.Datetime.now()` when `state` changes. Cleanest, but schema change.
|
|
|
|
**Recommended default: B** — small, additive, makes the timeline reliable without joining chatter at render time. Document the decision in this plan's task notes.
|
|
|
|
- [ ] **Step 4: Decide V1 document-linking approach**
|
|
|
|
`fusion.plating.portal.job` has direct fields: `coc_attachment_id`, `packing_list_attachment_id`. Other doc categories (PO, Drawing, Specification) require either:
|
|
- **A. Add `sale_order_id` Many2one** to `fp.portal.job` and pull docs via SO chatter + lines.
|
|
- **B. Use only the chatter attachments** on the portal job itself, with filename heuristics to categorise. Customer uploads attach to the portal job manually.
|
|
- **C. V1 ships with just `coc_attachment_id` + `packing_list_attachment_id` surfaced; "From You" / "Specifications" groups show "No documents yet" placeholders.**
|
|
|
|
**Recommended default: C for V1, A for V2.** Ship the detail page with the structure ready (4 groups visible), but the empty groups simply render their placeholder rows. V2 (separate change) adds the SO link and populates the missing groups.
|
|
|
|
- [ ] **Step 5: Update the spec inline with chosen approaches**
|
|
|
|
Edit `docs/superpowers/specs/2026-05-17-portal-dashboard-redesign-design.md`. Update the "Open items" section §1 and §4 with the chosen approaches and rationale. Update the doc-categorisation table to reflect V1 (placeholder rows for the 3 currently-unpopulated groups). No commit — this is documentation cleanup, bundled with Phase 3's commit.
|
|
|
|
---
|
|
|
|
### Task 16: Add per-stage Datetime fields to `fusion.plating.portal.job` (if Task 15 chose option B)
|
|
|
|
**Files:**
|
|
- Modify: `fusion_plating_portal/models/fp_portal_job.py`
|
|
|
|
Skip this task if Task 15 chose option A.
|
|
|
|
- [ ] **Step 1: Add fields + `write()` snapshot**
|
|
|
|
In `fusion_plating_portal/models/fp_portal_job.py`, add these fields after the existing `actual_ship_date` field (around line 65):
|
|
|
|
```python
|
|
received_at = fields.Datetime(
|
|
string='Received Timestamp',
|
|
readonly=True,
|
|
help='Auto-set when state first reaches `received`.',
|
|
)
|
|
in_progress_started_at = fields.Datetime(
|
|
string='In Progress Started At',
|
|
readonly=True,
|
|
)
|
|
qc_started_at = fields.Datetime(
|
|
string='QC Started At',
|
|
readonly=True,
|
|
)
|
|
ready_to_ship_at = fields.Datetime(
|
|
string='Ready to Ship At',
|
|
readonly=True,
|
|
)
|
|
shipped_at = fields.Datetime(
|
|
string='Shipped At',
|
|
readonly=True,
|
|
)
|
|
```
|
|
|
|
Add a write override at the end of the class (before `# end of class`):
|
|
|
|
```python
|
|
_STATE_TO_TS_FIELD = {
|
|
'received': 'received_at',
|
|
'in_progress': 'in_progress_started_at',
|
|
'quality_check': 'qc_started_at',
|
|
'ready_to_ship': 'ready_to_ship_at',
|
|
'shipped': 'shipped_at',
|
|
}
|
|
|
|
def write(self, vals):
|
|
if 'state' in vals:
|
|
new_state = vals['state']
|
|
ts_field = self._STATE_TO_TS_FIELD.get(new_state)
|
|
if ts_field:
|
|
now = fields.Datetime.now()
|
|
for rec in self:
|
|
if not rec[ts_field]:
|
|
vals.setdefault(ts_field, now)
|
|
# All records in `self` get the same timestamp.
|
|
# If you need per-record nuance, split the loop.
|
|
break
|
|
return super().write(vals)
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
records = super().create(vals_list)
|
|
# Snapshot the initial state's timestamp.
|
|
now = fields.Datetime.now()
|
|
for rec in records:
|
|
ts_field = self._STATE_TO_TS_FIELD.get(rec.state)
|
|
if ts_field and not rec[ts_field]:
|
|
rec[ts_field] = now
|
|
return records
|
|
```
|
|
|
|
- [ ] **Step 2: Write a test for the snapshot behavior**
|
|
|
|
Append to `fusion_plating_portal/tests/test_portal_dashboard.py`:
|
|
|
|
```python
|
|
def test_state_change_snapshots_timestamp(self):
|
|
"""write({'state': 'in_progress'}) sets in_progress_started_at."""
|
|
from odoo import fields as odoo_fields
|
|
Job = self.env['fusion.plating.portal.job']
|
|
job = Job.create({
|
|
'name': 'WO-TS-001',
|
|
'partner_id': self.partner.id,
|
|
'state': 'received',
|
|
})
|
|
self.assertTrue(job.received_at, 'received_at set on create')
|
|
before = odoo_fields.Datetime.now()
|
|
job.state = 'in_progress'
|
|
self.assertTrue(job.in_progress_started_at, 'in_progress_started_at set')
|
|
self.assertGreaterEqual(job.in_progress_started_at, before)
|
|
# received_at must not be overwritten when state advances
|
|
self.assertTrue(job.received_at)
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/models/fp_portal_job.py fusion_plating_portal/tests/test_portal_dashboard.py && \
|
|
git commit -m "feat(portal): per-stage timestamps on fp.portal.job
|
|
|
|
Adds received_at, in_progress_started_at, qc_started_at,
|
|
ready_to_ship_at, shipped_at — snapshotted on state change via
|
|
write() override. Required for the vertical-timeline rendering on
|
|
the job detail page (Phase 3)."
|
|
```
|
|
|
|
---
|
|
|
|
### Task 17: Add `_fp_get_stage_timeline()` helper + tests
|
|
|
|
**Files:**
|
|
- Modify: `fusion_plating_portal/controllers/portal.py` (add helper)
|
|
- Modify: `fusion_plating_portal/tests/test_portal_dashboard.py` (add test)
|
|
|
|
- [ ] **Step 1: Write the failing test FIRST**
|
|
|
|
Append to `tests/test_portal_dashboard.py`:
|
|
|
|
```python
|
|
def test_stage_timeline_for_job_in_quality_check(self):
|
|
"""Timeline returns 5 entries with correct status flags."""
|
|
from odoo.addons.fusion_plating_portal.controllers.portal import FpCustomerPortal
|
|
Job = self.env['fusion.plating.portal.job']
|
|
job = Job.create({
|
|
'name': 'WO-TL-001',
|
|
'partner_id': self.partner.id,
|
|
'state': 'received',
|
|
})
|
|
job.state = 'in_progress'
|
|
job.state = 'quality_check'
|
|
timeline = FpCustomerPortal()._fp_get_stage_timeline(job)
|
|
self.assertEqual(len(timeline), 5)
|
|
statuses = [s['status'] for s in timeline]
|
|
# 2 done (received, in_progress), 1 active (QC), 2 pending (ready, shipped)
|
|
self.assertEqual(statuses, ['done', 'done', 'active', 'pending', 'pending'])
|
|
labels = [s['label'] for s in timeline]
|
|
self.assertEqual(labels, ['Received', 'In Progress', 'Quality Check',
|
|
'Ready to Ship', 'Shipped'])
|
|
# Done stages have a started_at value
|
|
self.assertIsNotNone(timeline[0]['started_at'])
|
|
self.assertIsNotNone(timeline[1]['started_at'])
|
|
# Active stage also has started_at
|
|
self.assertIsNotNone(timeline[2]['started_at'])
|
|
# Pending stages should not
|
|
self.assertIsNone(timeline[3]['started_at'])
|
|
self.assertIsNone(timeline[4]['started_at'])
|
|
```
|
|
|
|
- [ ] **Step 2: Implement the helper on `FpCustomerPortal`**
|
|
|
|
Add to `fusion_plating_portal/controllers/portal.py` inside the `FpCustomerPortal` class, after the existing `_fp_get_partner_domain` helper (around line 114):
|
|
|
|
```python
|
|
_FP_STAGE_DEFS = [
|
|
('received', 'Received', 'received_at'),
|
|
('in_progress', 'In Progress', 'in_progress_started_at'),
|
|
('quality_check', 'Quality Check', 'qc_started_at'),
|
|
('ready_to_ship', 'Ready to Ship', 'ready_to_ship_at'),
|
|
('shipped', 'Shipped', 'shipped_at'),
|
|
]
|
|
|
|
def _fp_get_stage_timeline(self, job):
|
|
"""Build a 5-entry timeline for the detail-page vertical view.
|
|
|
|
Returns a list of dicts in stage order. Each dict has:
|
|
label, status ('done'|'active'|'pending'), started_at (datetime|None),
|
|
time_label (formatted string), notes (str).
|
|
"""
|
|
state_order = [s[0] for s in self._FP_STAGE_DEFS]
|
|
try:
|
|
current_idx = state_order.index(job.state)
|
|
except ValueError:
|
|
current_idx = 0 # state out of model — should not happen
|
|
out = []
|
|
for i, (state_key, label, ts_field) in enumerate(self._FP_STAGE_DEFS):
|
|
if i < current_idx:
|
|
status = 'done'
|
|
elif i == current_idx:
|
|
status = 'active'
|
|
else:
|
|
status = 'pending'
|
|
ts = job[ts_field] if hasattr(job, ts_field) else None
|
|
time_label = ''
|
|
if ts:
|
|
time_label = ts.strftime('%b %d · %-I:%M%p').lower().replace('am', 'a').replace('pm', 'p')
|
|
elif status == 'pending':
|
|
# Use target_ship_date as fallback for the last stage
|
|
if state_key == 'shipped' and job.target_ship_date:
|
|
time_label = 'est. ' + job.target_ship_date.strftime('%b %d')
|
|
out.append({
|
|
'label': label,
|
|
'status': status,
|
|
'started_at': ts or None,
|
|
'time_label': time_label,
|
|
'notes': '',
|
|
})
|
|
return out
|
|
```
|
|
|
|
- [ ] **Step 3: Run the test**
|
|
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_portal --test-tags=fp_portal --stop-after-init 2>&1 | tail -30\"'"
|
|
```
|
|
|
|
Expected: `test_stage_timeline_for_job_in_quality_check ... ok`.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/controllers/portal.py fusion_plating_portal/tests/test_portal_dashboard.py && \
|
|
git commit -m "feat(portal): _fp_get_stage_timeline helper for detail-page timeline
|
|
|
|
Builds a 5-entry list (label, status, started_at, time_label, notes)
|
|
ordered by stage. Status partitions stages into done/active/pending
|
|
based on current job state. Time labels use lowercase am/pm to match
|
|
the mockup typography."
|
|
```
|
|
|
|
---
|
|
|
|
### Task 18: Add `_fp_group_documents()` helper + tests
|
|
|
|
**Files:**
|
|
- Modify: `fusion_plating_portal/controllers/portal.py`
|
|
- Modify: `fusion_plating_portal/tests/test_portal_dashboard.py`
|
|
|
|
- [ ] **Step 1: Write failing test**
|
|
|
|
Append to `tests/test_portal_dashboard.py`:
|
|
|
|
```python
|
|
def test_group_documents_v1_returns_4_groups(self):
|
|
"""V1 doc grouping returns 4 groups; non-empty only for CoC + packing."""
|
|
from odoo.addons.fusion_plating_portal.controllers.portal import FpCustomerPortal
|
|
Job = self.env['fusion.plating.portal.job']
|
|
# Make a fake CoC attachment
|
|
att = self.env['ir.attachment'].create({
|
|
'name': 'CoC_WO-TEST.pdf',
|
|
'datas': b'',
|
|
'res_model': 'fusion.plating.portal.job',
|
|
})
|
|
job = Job.create({
|
|
'name': 'WO-DOC-001',
|
|
'partner_id': self.partner.id,
|
|
'state': 'shipped',
|
|
'coc_attachment_id': att.id,
|
|
})
|
|
groups = FpCustomerPortal()._fp_group_documents(job)
|
|
self.assertEqual(len(groups), 4)
|
|
keys = [g['key'] for g in groups]
|
|
self.assertEqual(keys, ['from_you', 'specs', 'quality', 'shipping'])
|
|
# Quality group has the CoC doc populated
|
|
quality = next(g for g in groups if g['key'] == 'quality')
|
|
self.assertTrue(any(d['label'] == 'Certificate of Conformance' and not d.get('pending')
|
|
for d in quality['docs']))
|
|
```
|
|
|
|
- [ ] **Step 2: Implement the helper**
|
|
|
|
Add to `controllers/portal.py`, after `_fp_get_stage_timeline`:
|
|
|
|
```python
|
|
def _fp_group_documents(self, job):
|
|
"""Build the 4-group document panel for the job detail page.
|
|
|
|
V1: surfaces only the directly-attached fields on fp.portal.job
|
|
(coc_attachment_id, packing_list_attachment_id). All other groups
|
|
render placeholder/pending rows. V2 (separate change) wires in
|
|
sale_order_id, quote_request_id, part_catalog_id sources.
|
|
|
|
Returns a list of 4 dicts: {key, label, docs}, where docs is a list
|
|
of {label, sub, url, icon_class, icon, pending}.
|
|
"""
|
|
groups = [
|
|
{'key': 'from_you', 'label': 'From You', 'docs': []},
|
|
{'key': 'specs', 'label': 'Specifications', 'docs': []},
|
|
{'key': 'quality', 'label': 'Quality', 'docs': []},
|
|
{'key': 'shipping', 'label': 'Shipping', 'docs': []},
|
|
]
|
|
|
|
# FROM YOU — V1: empty placeholder
|
|
groups[0]['docs'].append({
|
|
'label': 'Customer documents',
|
|
'sub': 'Upload your PO and drawings via your sales contact for now',
|
|
'pending': True,
|
|
'icon': '📄',
|
|
})
|
|
|
|
# SPECIFICATIONS — V1: empty placeholder
|
|
groups[1]['docs'].append({
|
|
'label': 'Customer Specification',
|
|
'sub': 'Will appear when EN Plating links the spec',
|
|
'pending': True,
|
|
'icon': '📋',
|
|
})
|
|
|
|
# QUALITY — CoC from coc_attachment_id
|
|
if job.coc_attachment_id:
|
|
groups[2]['docs'].append({
|
|
'label': 'Certificate of Conformance',
|
|
'sub': 'EN Plating · %s · %s' % (
|
|
job.actual_ship_date and job.actual_ship_date.strftime('%b %d') or '',
|
|
self._fp_size_label(job.coc_attachment_id),
|
|
),
|
|
'url': '/my/jobs/%s/coc' % job.id,
|
|
'icon_class': 'o_fp_doc_icon_quality',
|
|
'icon': '📑',
|
|
})
|
|
else:
|
|
groups[2]['docs'].append({
|
|
'label': 'Certificate of Conformance',
|
|
'sub': 'Will appear after QC completes',
|
|
'pending': True,
|
|
'icon': '📑',
|
|
})
|
|
|
|
# SHIPPING — packing list + tracking
|
|
if job.packing_list_attachment_id:
|
|
groups[3]['docs'].append({
|
|
'label': 'Packing Slip',
|
|
'sub': 'EN Plating · %s · %s' % (
|
|
job.actual_ship_date and job.actual_ship_date.strftime('%b %d') or '',
|
|
self._fp_size_label(job.packing_list_attachment_id),
|
|
),
|
|
'url': '/web/content/%s?download=true' % job.packing_list_attachment_id.id,
|
|
'icon_class': 'o_fp_doc_icon_shipping',
|
|
'icon': '📦',
|
|
})
|
|
else:
|
|
groups[3]['docs'].append({
|
|
'label': 'Packing Slip · Tracking #',
|
|
'sub': 'Available when shipped' + (' — ' + job.tracking_ref if job.tracking_ref else ''),
|
|
'pending': not job.tracking_ref,
|
|
'icon': '📦',
|
|
})
|
|
|
|
return groups
|
|
|
|
def _fp_size_label(self, attachment):
|
|
"""Render file_size as a friendly KB / MB string. Empty if unknown."""
|
|
if not attachment or not attachment.file_size:
|
|
return ''
|
|
size = attachment.file_size
|
|
if size < 1024:
|
|
return '%d B' % size
|
|
if size < 1024 * 1024:
|
|
return '%.0f KB' % (size / 1024)
|
|
return '%.1f MB' % (size / (1024 * 1024))
|
|
```
|
|
|
|
- [ ] **Step 3: Run the test**
|
|
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_portal --test-tags=fp_portal --stop-after-init 2>&1 | tail -30\"'"
|
|
```
|
|
|
|
Expected: `test_group_documents_v1_returns_4_groups ... ok`.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/controllers/portal.py fusion_plating_portal/tests/test_portal_dashboard.py && \
|
|
git commit -m "feat(portal): _fp_group_documents helper for detail-page doc panel
|
|
|
|
V1 surfaces only the fields directly on fp.portal.job (CoC + packing
|
|
list). Other 3 groups render placeholder rows. V2 will wire in
|
|
sale.order linking for full doc surfacing."
|
|
```
|
|
|
|
---
|
|
|
|
### Task 19: Create `fp_portal_timeline.scss`
|
|
|
|
**Files:**
|
|
- Create: `fusion_plating_portal/static/src/scss/fp_portal_timeline.scss`
|
|
|
|
- [ ] **Step 1: Write the timeline SCSS**
|
|
|
|
```scss
|
|
// ============================================================================
|
|
// Fusion Plating — Portal · Vertical timeline (job detail page)
|
|
// ============================================================================
|
|
|
|
.o_fp_timeline {
|
|
position: relative;
|
|
|
|
// Spine (gray default)
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 9px;
|
|
top: 10px;
|
|
bottom: 10px;
|
|
width: 2px;
|
|
background: $fp-card-border;
|
|
}
|
|
// Active portion (filled to height of completed stages, set inline by template)
|
|
.o_fp_timeline_spine_active {
|
|
position: absolute;
|
|
left: 9px;
|
|
top: 10px;
|
|
width: 2px;
|
|
background: $fp-teal;
|
|
// height set inline via style attribute
|
|
}
|
|
|
|
.o_fp_timeline_item {
|
|
position: relative;
|
|
padding-left: 2rem;
|
|
padding-bottom: 1.1rem;
|
|
|
|
&:last-child { padding-bottom: 0; }
|
|
|
|
.o_fp_timeline_dot {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: $fp-radius-pill;
|
|
background: $fp-card-bg;
|
|
border: 2px solid $fp-card-border;
|
|
}
|
|
&.o_fp_timeline_done .o_fp_timeline_dot {
|
|
background: $fp-gradient-primary;
|
|
border: none;
|
|
color: #fff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: .65rem;
|
|
font-weight: 700;
|
|
}
|
|
&.o_fp_timeline_active .o_fp_timeline_dot {
|
|
background: $fp-card-bg;
|
|
border: 2.5px solid $fp-teal;
|
|
box-shadow: $fp-glow-ring-teal;
|
|
}
|
|
|
|
.o_fp_timeline_title {
|
|
font-size: .92rem;
|
|
color: $fp-text;
|
|
font-weight: 500;
|
|
line-height: 1.2;
|
|
}
|
|
&.o_fp_timeline_active .o_fp_timeline_title {
|
|
color: $fp-teal;
|
|
font-weight: 700;
|
|
font-size: .95rem;
|
|
}
|
|
&.o_fp_timeline_pending .o_fp_timeline_title {
|
|
color: $fp-muted;
|
|
}
|
|
.o_fp_timeline_time {
|
|
font-size: .78rem;
|
|
color: $fp-muted;
|
|
margin-top: .2rem;
|
|
}
|
|
&.o_fp_timeline_active .o_fp_timeline_time { color: $fp-teal; }
|
|
&.o_fp_timeline_pending .o_fp_timeline_time { color: $fp-disabled; }
|
|
|
|
.o_fp_timeline_note {
|
|
font-size: .74rem;
|
|
color: $fp-text-body;
|
|
margin-top: .35rem;
|
|
padding: .4rem .6rem;
|
|
background: $fp-page-bg;
|
|
border-radius: 6px;
|
|
display: inline-block;
|
|
&.o_fp_timeline_note_success { background: $fp-success-bg; color: $fp-success-text; }
|
|
&.o_fp_timeline_note_active { background: #eff6ff; color: $fp-teal-dark; line-height: 1.4; }
|
|
}
|
|
}
|
|
}
|
|
|
|
// Detail-page outer wrapper
|
|
.o_fp_job_detail {
|
|
background: $fp-page-bg;
|
|
padding: $fp-space-6;
|
|
border-radius: $fp-radius-card;
|
|
border: 1px solid $fp-card-border;
|
|
font-family: $fp-font;
|
|
|
|
.o_fp_job_detail_hero {
|
|
@extend .o_fp_card;
|
|
margin-bottom: $fp-space-5;
|
|
padding-bottom: $fp-space-4;
|
|
|
|
h2 { margin: 0 0 .35rem 0; font-size: 1.5rem; color: $fp-text; font-weight: 600; }
|
|
.o_fp_detail_label {
|
|
font-size: .7rem;
|
|
color: $fp-muted;
|
|
letter-spacing: .05em;
|
|
text-transform: uppercase;
|
|
font-weight: 500;
|
|
margin-bottom: .25rem;
|
|
}
|
|
.o_fp_detail_subtitle {
|
|
color: $fp-text-body;
|
|
font-size: .92rem;
|
|
margin-bottom: .7rem;
|
|
}
|
|
.o_fp_detail_facts {
|
|
display: flex;
|
|
gap: 1.5rem;
|
|
flex-wrap: wrap;
|
|
color: $fp-text-body;
|
|
font-size: .82rem;
|
|
.o_fp_fact_label { color: $fp-muted-light; }
|
|
.o_fp_fact_value { color: $fp-text; font-weight: 600; }
|
|
}
|
|
}
|
|
|
|
.o_fp_job_detail_grid {
|
|
display: grid;
|
|
grid-template-columns: 1.1fr 1fr;
|
|
gap: $fp-space-5;
|
|
|
|
@media (max-width: 768px) {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.o_fp_job_detail_footer {
|
|
@extend .o_fp_card;
|
|
margin-top: $fp-space-5;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem;
|
|
|
|
.o_fp_related_links {
|
|
font-size: .82rem;
|
|
color: $fp-text-body;
|
|
a { color: $fp-teal; text-decoration: none; margin: 0 .55rem; }
|
|
a.disabled { color: $fp-muted-light; }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Verify** and **Step 3: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/static/src/scss/fp_portal_timeline.scss && \
|
|
git commit -m "feat(portal): vertical timeline + detail-page wrapper SCSS"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 20: Rewrite `portal_my_jobs` template
|
|
|
|
**Files:**
|
|
- Modify: `fusion_plating_portal/views/fp_portal_templates.xml`
|
|
|
|
- [ ] **Step 1: Find the existing template at line 431**
|
|
|
|
Read `fusion_plating_portal/views/fp_portal_templates.xml` lines 428-498 to locate the `portal_my_jobs` template.
|
|
|
|
- [ ] **Step 2: Replace the template body**
|
|
|
|
Replace the template body (everything inside `<template id="portal_my_jobs" ...>` ... `</template>`) with:
|
|
|
|
```xml
|
|
<template id="portal_my_jobs" name="My Work Orders">
|
|
<t t-call="portal.portal_layout">
|
|
<t t-set="breadcrumbs_searchbar" t-value="True"/>
|
|
<t t-call="portal.portal_searchbar">
|
|
<t t-set="title">Work Orders</t>
|
|
</t>
|
|
|
|
<t t-if="not jobs">
|
|
<div class="o_fp_card text-center text-muted">
|
|
<p class="mb-2">You have no plating jobs yet.</p>
|
|
<a href="/my/configurator" class="o_fp_btn_primary o_fp_btn_sm mt-2">+ Get Your First Quote</a>
|
|
</div>
|
|
</t>
|
|
<t t-if="jobs">
|
|
<div class="o_fp_dashboard">
|
|
<t t-foreach="jobs" t-as="job">
|
|
<div class="o_fp_job_card">
|
|
<div class="o_fp_job_header">
|
|
<div>
|
|
<a t-att-href="'/my/jobs/%s' % job.id"
|
|
class="o_fp_job_ref text-decoration-none"
|
|
t-out="job.name"/>
|
|
<span class="o_fp_job_meta">
|
|
<t t-if="job.quantity"><t t-out="job.quantity"/> units</t>
|
|
<t t-if="job.target_ship_date"> · ETA <span t-field="job.target_ship_date" t-options='{"widget": "date"}'/></t>
|
|
</span>
|
|
</div>
|
|
<t t-call="fusion_plating_portal.fp_portal_status_badge">
|
|
<t t-set="state" t-value="job.state"/>
|
|
<t t-set="label" t-value="dict(job._fields['state']._description_selection(job.env)).get(job.state)"/>
|
|
</t>
|
|
</div>
|
|
|
|
<t t-set="state_idx" t-value="['received','in_progress','quality_check','ready_to_ship','shipped'].index(job.state) if job.state in ['received','in_progress','quality_check','ready_to_ship','shipped'] else 0"/>
|
|
<t t-set="steps" t-value="[
|
|
{'label': 'Received', 'status': 'done' if state_idx > 0 else 'active', 'time_label': ''},
|
|
{'label': 'Inspected', 'status': 'done' if state_idx > 1 else ('active' if state_idx == 1 else 'pending'), 'time_label': ''},
|
|
{'label': 'Plating', 'status': 'done' if state_idx > 2 else ('active' if state_idx == 2 else 'pending'), 'time_label': ''},
|
|
{'label': 'QC', 'status': 'done' if state_idx > 3 else ('active' if state_idx == 3 else 'pending'), 'time_label': ''},
|
|
{'label': 'Ship', 'status': 'done' if state_idx > 4 else ('active' if state_idx == 4 else 'pending'), 'time_label': ''},
|
|
]"/>
|
|
<t t-set="active_state" t-value="'warn' if job.state == 'quality_check' else 'normal'"/>
|
|
<t t-call="fusion_plating_portal.fp_portal_stepper"/>
|
|
</div>
|
|
</t>
|
|
</div>
|
|
</t>
|
|
</t>
|
|
</template>
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/views/fp_portal_templates.xml && \
|
|
git commit -m "feat(portal): rewrite /my/jobs list with V2 stepper cards"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 21: Rewrite `portal_my_job` template + update controller
|
|
|
|
**Files:**
|
|
- Modify: `fusion_plating_portal/views/fp_portal_templates.xml`
|
|
- Modify: `fusion_plating_portal/controllers/portal.py` (the `portal_my_job` method)
|
|
|
|
- [ ] **Step 1: Update the controller method to pass timeline + doc groups**
|
|
|
|
In `controllers/portal.py`, find `def portal_my_job(self, job_id, access_token=None, **kw):` and modify the values prep:
|
|
|
|
```python
|
|
def portal_my_job(self, job_id, access_token=None, **kw):
|
|
try:
|
|
job_sudo = self._document_check_access(
|
|
'fusion.plating.portal.job',
|
|
job_id,
|
|
access_token,
|
|
)
|
|
except (AccessError, MissingError):
|
|
return request.redirect('/my')
|
|
|
|
values = self._fp_portal_job_get_page_view_values(
|
|
job_sudo, access_token, **kw
|
|
)
|
|
values['progress_percent'] = job_sudo._progress_percent()
|
|
values['stage_timeline'] = self._fp_get_stage_timeline(job_sudo)
|
|
values['doc_groups'] = self._fp_group_documents(job_sudo)
|
|
# Spine-fill percent for the timeline (visual progress indicator).
|
|
done_count = sum(1 for s in values['stage_timeline'] if s['status'] == 'done')
|
|
active_count = sum(1 for s in values['stage_timeline'] if s['status'] == 'active')
|
|
# Spine fill = done + half of active (covers up to the active dot).
|
|
values['timeline_spine_pct'] = int(((done_count + 0.5 * active_count) / 5) * 100)
|
|
return request.render(
|
|
'fusion_plating_portal.portal_my_job',
|
|
values,
|
|
)
|
|
```
|
|
|
|
- [ ] **Step 2: Replace the `portal_my_job` template body**
|
|
|
|
Find `<template id="portal_my_job" ...>` (around line 502 in `fp_portal_templates.xml`) and replace its body with:
|
|
|
|
```xml
|
|
<template id="portal_my_job" name="My Work Order">
|
|
<t t-call="portal.portal_layout">
|
|
<div class="o_fp_job_detail">
|
|
|
|
<!-- Hero header -->
|
|
<div class="o_fp_job_detail_hero">
|
|
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
|
|
<div>
|
|
<div class="o_fp_detail_label">Work Order</div>
|
|
<h2><span t-out="job.name"/></h2>
|
|
<div t-if="job.process_type_ids" class="o_fp_detail_subtitle">
|
|
<span t-out="', '.join(job.process_type_ids.mapped('name'))"/>
|
|
</div>
|
|
<div class="o_fp_detail_facts">
|
|
<div t-if="job.quantity">
|
|
<span class="o_fp_fact_label">Qty </span>
|
|
<span class="o_fp_fact_value" t-out="job.quantity"/>
|
|
</div>
|
|
<div t-if="job.received_date">
|
|
<span class="o_fp_fact_label">Received </span>
|
|
<span class="o_fp_fact_value" t-field="job.received_date" t-options='{"widget": "date"}'/>
|
|
</div>
|
|
<div t-if="job.target_ship_date">
|
|
<span class="o_fp_fact_label">ETA </span>
|
|
<span class="o_fp_fact_value" t-field="job.target_ship_date" t-options='{"widget": "date"}'/>
|
|
</div>
|
|
<div t-if="job.tracking_ref">
|
|
<span class="o_fp_fact_label">Tracking </span>
|
|
<span class="o_fp_fact_value" t-out="job.tracking_ref"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex flex-column align-items-end gap-2">
|
|
<t t-call="fusion_plating_portal.fp_portal_status_badge">
|
|
<t t-set="state" t-value="job.state"/>
|
|
<t t-set="label" t-value="dict(job._fields['state']._description_selection(job.env)).get(job.state)"/>
|
|
</t>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Two-column grid: timeline | docs -->
|
|
<div class="o_fp_job_detail_grid">
|
|
|
|
<!-- Timeline -->
|
|
<div class="o_fp_card">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<div style="font-weight:600;color:#111827;font-size:1rem">Progress</div>
|
|
<span style="font-size:.7rem;color:#6b7280">
|
|
<t t-out="progress_percent"/>% complete
|
|
</span>
|
|
</div>
|
|
<div class="o_fp_timeline">
|
|
<div class="o_fp_timeline_spine_active" t-attf-style="height: #{timeline_spine_pct}%"/>
|
|
<t t-foreach="stage_timeline" t-as="step">
|
|
<div t-attf-class="o_fp_timeline_item o_fp_timeline_#{step['status']}">
|
|
<div class="o_fp_timeline_dot">
|
|
<t t-if="step['status'] == 'done'">✓</t>
|
|
</div>
|
|
<div class="o_fp_timeline_title" t-out="step['label']"/>
|
|
<div class="o_fp_timeline_time" t-out="step['time_label']"/>
|
|
</div>
|
|
</t>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Documents -->
|
|
<div class="o_fp_card">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<div style="font-weight:600;color:#111827;font-size:1rem">Documents</div>
|
|
<span style="font-size:.7rem;color:#6b7280">
|
|
<t t-out="sum(len(g['docs']) for g in doc_groups if not all(d.get('pending') for d in g['docs']))"/> files
|
|
</span>
|
|
</div>
|
|
<t t-foreach="doc_groups" t-as="group">
|
|
<t t-call="fusion_plating_portal.fp_portal_doc_group">
|
|
<t t-set="group_label" t-value="group['label']"/>
|
|
<t t-set="docs" t-value="group['docs']"/>
|
|
</t>
|
|
</t>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="o_fp_job_detail_footer">
|
|
<div class="o_fp_related_links">
|
|
<span style="color:#9ca3af">Related:</span>
|
|
<a t-if="job.invoice_ref" href="#" t-out="'Invoice ' + job.invoice_ref"/>
|
|
<a t-else="" class="disabled">Invoice (pending)</a>
|
|
</div>
|
|
<a href="/my/jobs" class="o_fp_btn_secondary">← Back to all jobs</a>
|
|
</div>
|
|
</div>
|
|
</t>
|
|
</template>
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/views/fp_portal_templates.xml fusion_plating_portal/controllers/portal.py && \
|
|
git commit -m "feat(portal): rewrite /my/jobs/<id> detail page with timeline + doc panel
|
|
|
|
Two-column grid: vertical timeline (5 stages with per-stage timestamps)
|
|
on the left, grouped document panel (4 categories) on the right. Hero
|
|
header carries WO ref + part / qty / ETA / tracking facts."
|
|
```
|
|
|
|
---
|
|
|
|
### Task 22: Register Phase 3 assets + version bump
|
|
|
|
**Files:**
|
|
- Modify: `fusion_plating_portal/__manifest__.py`
|
|
|
|
- [ ] **Step 1: Bump version + add timeline.scss to assets**
|
|
|
|
Change version `19.0.3.1.0 → 19.0.3.2.0`. Add `fp_portal_timeline.scss` to the assets list AFTER `fp_portal_dashboard.scss`:
|
|
|
|
```python
|
|
'fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss',
|
|
'fusion_plating_portal/static/src/scss/fp_portal_timeline.scss', # NEW
|
|
'fusion_plating_portal/static/src/scss/fusion_plating_portal.scss',
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/__manifest__.py && \
|
|
git commit -m "chore(portal): bump 19.0.3.2.0 + register timeline SCSS"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 23: Deploy Phase 3 to entech + run tests + visual verify
|
|
|
|
**Files:** (deployment)
|
|
|
|
- [ ] **Step 1: Copy all Phase 3 files to entech**
|
|
|
|
```bash
|
|
for f in \
|
|
static/src/scss/fp_portal_timeline.scss \
|
|
views/fp_portal_templates.xml \
|
|
controllers/portal.py \
|
|
models/fp_portal_job.py \
|
|
tests/test_portal_dashboard.py \
|
|
__manifest__.py; do
|
|
cat "K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/$f" | \
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_portal/$f'"
|
|
done
|
|
```
|
|
|
|
- [ ] **Step 2: Upgrade + run tests**
|
|
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_portal --test-tags=fp_portal --stop-after-init 2>&1 | tail -60\" && systemctl start odoo'"
|
|
```
|
|
|
|
Expected: all 4 tests pass.
|
|
|
|
- [ ] **Step 3: Bust asset cache**
|
|
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- su - postgres -c \"psql -d admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';\\\"\""
|
|
```
|
|
|
|
- [ ] **Step 4: Visual check `/my/jobs`**
|
|
|
|
Open https://enplating.com/my/jobs. Expected: list of jobs, each rendered as a V2 card with stepper. Click into one to confirm the detail page renders the timeline + 4 doc groups.
|
|
|
|
- [ ] **Step 5: Tag Phase 3 shipped**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && git tag portal-phase3-shipped
|
|
```
|
|
|
|
---
|
|
|
|
# PHASE 4 — Cosmetic Sweep
|
|
|
|
Goal: apply the new token system to the remaining `/my/*` pages without structural changes.
|
|
|
|
### Task 24: Tokenise `fp_quote_request_views.xml`
|
|
|
|
**Files:**
|
|
- Modify: `fusion_plating_portal/views/fp_quote_request_views.xml`
|
|
|
|
- [ ] **Step 1: Read the file** to find any hardcoded button styles or badge classes.
|
|
|
|
```bash
|
|
ls -la K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/views/fp_quote_request_views.xml
|
|
```
|
|
|
|
Read it in full, then for each `<button class="btn btn-primary ...">` swap to `class="o_fp_btn_primary"`; for `<button class="btn btn-outline-...">` swap to `class="o_fp_btn_secondary"`. For state-coloured badges (`badge bg-...` or `badge text-bg-...`), swap to the corresponding `o_fp_badge o_fp_badge_<state>` class via the macro.
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/views/fp_quote_request_views.xml && \
|
|
git commit -m "style(portal): tokenise quote request views (buttons + badges)"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 25: Tokenise `fp_portal_configurator_templates.xml`
|
|
|
|
**Files:**
|
|
- Modify: `fusion_plating_portal/views/fp_portal_configurator_templates.xml`
|
|
|
|
- [ ] **Step 1: Same pattern as Task 24**
|
|
|
|
Read, then swap every `btn btn-primary` → `o_fp_btn_primary`, `btn btn-outline-*` → `o_fp_btn_secondary`, `btn-link` / `btn btn-light` → `o_fp_btn_ghost`. Leave Bootstrap's grid + utility classes untouched.
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/views/fp_portal_configurator_templates.xml && \
|
|
git commit -m "style(portal): tokenise configurator (RFQ wizard) buttons"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 26: Trim `fusion_plating_portal.scss`
|
|
|
|
**Files:**
|
|
- Modify: `fusion_plating_portal/static/src/scss/fusion_plating_portal.scss`
|
|
|
|
- [ ] **Step 1: Audit + trim**
|
|
|
|
Read the file. Move anything that the new partials cover (`.o_fp_portal_card`, badge styles, button styles) — DELETE the duplicates. Keep only rules that are still genuinely needed (e.g., portal-layout overrides specific to FP that don't fit into the new partials).
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/static/src/scss/fusion_plating_portal.scss && \
|
|
git commit -m "refactor(portal): trim legacy catch-all SCSS, deduplicate vs new partials"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 27: Final Phase 4 deploy + visual sweep across all `/my/*` pages
|
|
|
|
**Files:** (deployment + manifest version bump)
|
|
|
|
- [ ] **Step 1: Bump version to 19.0.3.3.0 + commit**
|
|
|
|
```bash
|
|
# Edit __manifest__.py: 19.0.3.2.0 → 19.0.3.3.0
|
|
cd K:/Github/Odoo-Modules/fusion_plating && \
|
|
git add fusion_plating_portal/__manifest__.py && \
|
|
git commit -m "chore(portal): bump 19.0.3.3.0 — Phase 4 cosmetic sweep"
|
|
```
|
|
|
|
- [ ] **Step 2: Copy all changed files**
|
|
|
|
```bash
|
|
for f in \
|
|
views/fp_quote_request_views.xml \
|
|
views/fp_portal_configurator_templates.xml \
|
|
static/src/scss/fusion_plating_portal.scss \
|
|
__manifest__.py; do
|
|
cat "K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/$f" | \
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_portal/$f'"
|
|
done
|
|
```
|
|
|
|
- [ ] **Step 3: Upgrade + restart**
|
|
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_portal --stop-after-init 2>&1 | tail -20\" && systemctl start odoo'"
|
|
```
|
|
|
|
- [ ] **Step 4: Bust cache + visual sweep**
|
|
|
|
```bash
|
|
ssh pve-worker5 "pct exec 111 -- su - postgres -c \"psql -d admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';\\\"\""
|
|
```
|
|
|
|
Walk these URLs in the browser (logged in as admin):
|
|
- /my/home — dashboard (Phase 2 confirmed working)
|
|
- /my/jobs — list (Phase 3)
|
|
- /my/jobs/<any id> — detail (Phase 3)
|
|
- /my/quote_requests — list (Phase 4)
|
|
- /my/quote_requests/<id> — detail
|
|
- /my/quote_requests/new — RFQ wizard
|
|
- /my/configurator — RFQ configurator
|
|
- /my/purchase_orders, /my/fp_invoices, /my/deliveries, /my/certifications — confirm buttons + badges adopt the new tokens
|
|
|
|
- [ ] **Step 5: Tag Phase 4 shipped**
|
|
|
|
```bash
|
|
cd K:/Github/Odoo-Modules/fusion_plating && git tag portal-phase4-shipped
|
|
```
|
|
|
|
---
|
|
|
|
# Done
|
|
|
|
After Task 27, the portal redesign ships. Defer items:
|
|
- V2 doc grouping (add `sale_order_id` on `fp.portal.job`, pull PO + drawings via SO link).
|
|
- Mobile breakpoints beyond the 768px ones in the SCSS.
|
|
- Dark-mode `$o-webclient-color-scheme` branch in `_fp_portal_tokens.scss`.
|
|
- Operator-name visibility decision (today the detail page doesn't show one; if EN Plating wants it, surface via a future `operator_name_per_stage` field).
|
|
|
|
---
|
|
|
|
## Self-Review
|
|
|
|
**Spec coverage:** Every section of the spec maps to at least one task:
|
|
- Locked design decisions → Tasks 1, 2, 6-9, 10, 12, 21 (all template + SCSS work)
|
|
- Scope (in/out/deferred) → enforced by phase structure (Phases 1-3 = in scope; Phase 4 = cosmetic-only; deferred items called out at end)
|
|
- Architecture (controllers, templates, SCSS) → Tasks 11, 17, 18, 21 (controller), 10, 12, 20, 21 (templates), 1-9, 19 (SCSS)
|
|
- Migration/deployment → Tasks 4, 14, 23, 27 (one per phase)
|
|
- Open items §1 timestamps → Task 15-16 explicitly resolve
|
|
- Open items §4 document linking → Task 15 Step 4 + Task 18 implementation (V1 placeholder strategy)
|
|
|
|
**Placeholder scan:** No "TODO", "TBD", "fill in details", "similar to Task N" — all code blocks are concrete. Test code is real, runnable. Commit messages are filled in.
|
|
|
|
**Type consistency:** Method names match between definition and call:
|
|
- `_fp_get_stage_timeline` (Task 17) called from `portal_my_job` (Task 21) ✓
|
|
- `_fp_group_documents` (Task 18) called from `portal_my_job` (Task 21) ✓
|
|
- `_fp_size_label` (Task 18) called from `_fp_group_documents` (Task 18) ✓
|
|
- Macros named `fp_portal_status_badge`, `fp_portal_stepper`, `fp_portal_doc_chip`, `fp_portal_doc_group` (Task 10) — t-call'd with the same names in Tasks 12, 20, 21 ✓
|
|
- SCSS classes `.o_fp_btn_primary`, `.o_fp_card`, `.o_fp_kpi_tile`, `.o_fp_stepper`, `.o_fp_timeline_item` consistently used across SCSS files and templates ✓
|
|
|
|
---
|
|
|
|
*End of plan.*
|