Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-30-employee-portal-design.md
gsinghpal 4a9f31cef5 docs(employee-portal): design spec for staff Clock + Payslips portal
Separate internal employees from the customer portal: suppress the
fusion_plating_portal sidebar for internal users, redirect them to the
clock page, and add a finalized-payslip view (inline paystub + optional
PDF) under /my/clock/payslips in fusion_clock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:22:27 -04:00

191 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Employee Portal — Clock + Payslips (separating staff from the customer portal)
**Date:** 2026-05-30
**Status:** Design approved — ready for implementation plan
**Deployment target:** entech (LXC 111 on pve-worker5, DB `admin`)
**Modules touched:** `fusion_plating_portal`, `fusion_clock` (+ optional `fusion_payroll` if a payslip PDF must be built)
---
## 1. Context & Problem
EN Technologies (entech) runs three relevant modules on one Odoo:
- `fusion_plating_portal` — the **customer** portal: a rich dashboard + a left sidebar shell wrapping every `/my/*` page.
- `fusion_clock` — employee clock-in/out, exposed on the front end at `/my/clock` (+ `/my/clock/timesheets`, `/my/clock/reports`). A polished dark, mobile-first UI with its own bottom nav.
- `fusion_payroll` — Canadian payroll; owns `hr.payslip` with full earnings/deductions/YTD data. **No employee self-service surface today** (payslips are backend-only).
Two concrete problems:
1. **The customer portal applies to everyone.** `fusion_plating_portal`'s `home()` override (`/my`, `/my/home`) returns the customer dashboard for *every* logged-in user, with no internal-vs-customer check. Internal employees who land on `/my` see a customer dashboard that is empty/irrelevant to them.
2. **The customer sidebar bleeds onto the clock page.** `fp_portal_shell` (`fusion_plating_portal/views/fp_portal_shell.xml`) inherits `portal.portal_layout` and injects the left sidebar into **all** `/my/*` pages, unconditionally. Because `fusion_clock`'s `/my/clock` page renders inside `portal.portal_layout`, it inherits that customer sidebar even though the clock UI was designed clean (it sets `no_header`/`no_breadcrumbs` and has its own bottom nav).
**Goal:** internal employees get a dedicated, clean employee portal (Clock + Payslips, no customer sidebar); external customers keep the customer portal exactly as it is.
---
## 2. Goals / Non-goals
**Goals**
- Internal staff never see the customer dashboard or the customer sidebar.
- Employees land on the Clock page and navigate Clock / Timesheets / Reports / **Payslips** via the existing bottom nav.
- Employees can view their own finalized pay slips (inline paystub) and download a PDF when one is available.
- Zero change to the customer experience.
**Non-goals (v1)**
- Profile/password editing in the employee portal (internal staff use the backend).
- Payslips for non-finalized states (draft / verify hidden).
- RMA/quote/customer features for employees.
- Building a new payslip PDF report *unless* Odoo's standard one is absent on entech (see §9 Open items).
- Cross-instance payroll (payslips are confirmed to live on the same Odoo).
---
## 3. Decisions (locked during brainstorming)
| # | Decision |
|---|----------|
| Audience split | **Internal users → employee portal; share/portal users → customer portal.** Detected via `request.env.user.share` (`False` = internal staff, `True` = customer). |
| Employee landing & nav | **Clock is home.** `/my` and `/my/home` redirect employees to `/my/clock`. The existing bottom nav gains a **Payslips** tab. **No left sidebar anywhere** for employees. |
| Payslip scope | **Finalized only**`hr.payslip` where `state in ('done','paid')`, scoped to the logged-in employee. From `fusion_payroll` on the same Odoo. |
| Payslip presentation | **Inline paystub page + Download-PDF button.** Inline always works; the PDF button appears only when a payslip PDF report exists on the server. |
| Architecture | **Approach 1**`fusion_clock` owns the employee-portal pages (incl. payslips, via a *soft* `hr.payslip` read); `fusion_plating_portal` fixes its own gating + redirect. No new module, no hard cross-dependency. |
| Sign out | A compact **Sign Out** affordance in the employee header (the bottom-nav UI lacks one today). |
---
## 4. Architecture — Approach 1
Two modules change; responsibilities stay where they naturally belong.
**`fusion_plating_portal`** (the module causing the bleed — fixes itself):
- Adds `fp_show_customer_sidebar` to the portal layout context = `request.env.user.share`.
- Gates its sidebar shell on that flag.
- Branches `home()`: internal user → redirect to `/my/clock`; customer → existing dashboard.
**`fusion_clock`** (owns all employee-portal pages + the bottom nav + the dark SCSS):
- Adds `/my/clock/payslips` (list) and `/my/clock/payslips/<id>` (inline paystub) routes.
- Reads `hr.payslip` through a **soft** check (`'hr.payslip' in request.env`) — **no** `fusion_payroll` dependency added, so `fusion_clock` stays installable without payroll (the Payslips tab simply doesn't appear).
- Adds the **Payslips** tab to the existing bottom nav.
**Why not the alternatives:** payslip page in `fusion_payroll` makes the bottom nav shared chrome across two modules (duplication or a new cross-dep); a dedicated `fusion_employee_portal` module is new scaffolding that would have to couple to the entech-specific customer module to win the `/my/home` override — and it conflicts with the "edit existing files, don't add modules" rule.
> **Why the redirect lives in `fusion_plating_portal.home()`:** that override is the one currently winning at `/my/home` on entech (it's why employees see the customer dashboard today). Editing it to branch is guaranteed to take effect on entech without depending on fragile multi-module MRO ordering (`fusion_clock` does **not** override `home()`).
---
## 5. Detailed design
### 5.1 Audience detection
Single signal: `request.env.user.share`.
- `share == True` → customer → customer portal (dashboard + sidebar).
- `share == False` → internal staff → employee portal (clock + payslips, no sidebar).
The clock pages already resolve the person via `hr.employee` where `user_id == env.user.id` (`_get_portal_employee`). An internal user with no employee record (e.g. a bare admin) gets the clean layout, an empty clock that redirects, and retains full backend access.
### 5.2 Suppress the sidebar for employees
In `fusion_plating_portal/controllers/portal.py`, `_prepare_portal_layout_values` adds:
```python
values['fp_show_customer_sidebar'] = request.env.user.share
```
In `fp_portal_shell.xml`, the `#wrap` replacement becomes conditional:
- `fp_show_customer_sidebar` true → render the full `.o_fp_portal_shell` (sidebar + `<main>$0</main>`).
- false → re-emit the raw original `#wrap` ($0) with no shell.
**Implementation note / risk:** the shell uses Odoo 19's `$0` re-emission inside a `position="replace"`. Putting `$0` in **both** a `t-if` and a `t-else` branch needs verifying — the inheritance engine appends a deep copy of the original node for *each* `$0` occurrence; both copies live in the compiled arch but only one renders at runtime (t-if/t-else). If that proves unreliable at load time, **fallback:** keep a single `$0` inside `.o_fp_portal_main` always, gate only the `<aside>` with `t-if`, and add a body/`<main>` modifier class so the main column goes full-width (and the clock UI renders edge-to-edge) when the sidebar is absent. Either way the customer path is byte-for-byte unchanged.
### 5.3 Redirect employees to Clock
`fusion_plating_portal.home()` (routes `['/my','/my/home']`), first lines:
```python
if not request.env.user.share: # internal staff
return request.redirect('/my/clock')
```
Customers fall through to the existing dashboard build untouched.
### 5.4 Bottom nav gains Payslips
The nav bar lives in three `fusion_clock` templates (`portal_clock_templates.xml`, `portal_timesheet_templates.xml`, `portal_report_templates.xml`) plus the new payslip templates. Add a 4th item **Payslips** (`/my/clock/payslips`) with an appropriate icon, marking the active tab per page. The tab is gated on a `show_payslips` flag (true only when `'hr.payslip' in env`) so `fusion_clock` stays clean on payroll-less deployments.
### 5.5 Payslips list — `GET /my/clock/payslips`
- Resolve employee via `_get_portal_employee()`; if none → redirect `/my/clock`.
- If `'hr.payslip' not in request.env` → redirect `/my/clock` (and the tab won't show anyway).
- Query: `hr.payslip` `sudo()` where `employee_id == employee.id` and `state in ('done','paid')`, ordered `date_to desc`.
- Card list (same dark styling as Reports): pay period (`date_from``date_to`), net pay (`net_wage`, `$` + currency), status chip (Paid / Done), link to detail.
- `page_name='payslips'` for nav active-state.
### 5.6 Payslip detail (inline paystub) — `GET /my/clock/payslips/<int:payslip_id>`
- **Ownership guard** (mirrors `portal_report_download`): browse the slip; if it doesn't exist, isn't `state in ('done','paid')`, or `employee_id != employee.id` → redirect `/my/clock/payslips`.
- Render a mobile-friendly paystub from data already on the record:
- Header: employee name, pay period, pay date, status.
- Earnings: gross / line items (`line_ids` totals).
- Deductions: `employee_cpp`, `employee_cpp2`, `employee_ei`, `employee_income_tax` (+ total).
- **Net pay** (highlighted).
- YTD block: `ytd_gross`, `ytd_cpp`, `ytd_cpp2`, `ytd_ei`, `ytd_income_tax`, `ytd_net`.
- **Download PDF** button: visible only when a payslip PDF report action exists on the server (see §9). Links to that report for this `payslip_id`. Routed through `fusion_pdf_preview` where applicable per repo convention (PDF → preview dialog), otherwise standard `/report/pdf/...`.
### 5.7 Sign out
Add a compact **Sign Out** control to the employee header area (e.g. a small icon next to the "Hello, {name}" greeting on the clock page, and consistently on the payslip pages) → `/web/session/logout?redirect=/`.
---
## 6. Files touched
**`fusion_plating_portal`**
- `controllers/portal.py``_prepare_portal_layout_values` (+`fp_show_customer_sidebar`), `home()` (employee redirect).
- `views/fp_portal_shell.xml` — gate the shell on `fp_show_customer_sidebar`.
- `__manifest__.py` — version bump.
**`fusion_clock`**
- `controllers/portal_clock.py``/my/clock/payslips` + `/my/clock/payslips/<id>` routes; `show_payslips` flag in existing page values.
- `views/portal_clock_templates.xml`, `views/portal_timesheet_templates.xml`, `views/portal_report_templates.xml` — add Payslips nav item.
- `views/portal_payslip_templates.xml`**new**: list + inline paystub.
- `static/src/scss/...` — payslip card/paystub styles (reuse `fclk-*` design language) + Sign Out control.
- `__manifest__.py` — register new view file; version bump.
**`fusion_payroll`** — only if §9 finds no payslip PDF report and we decide to build a minimal paystub report (separate, optional task).
---
## 7. Edge cases & guards
- Internal user, no `hr.employee` → clean layout, clock redirects to `/my`, backend still reachable.
- `fusion_payroll` not installed → Payslips tab hidden; routes redirect to `/my/clock`.
- Payslip not owned / not finalized → redirect to the list (no leak).
- Admin needs to preview the *customer* portal → use a customer test login (documented), since internal users are redirected.
- Customer experience: unchanged — sidebar + dashboard render exactly as before when `share == True`.
---
## 8. Testing
- **Internal user:** `/my` and `/my/home` → 302 to `/my/clock`; `/my/clock`, `/my/clock/timesheets`, `/my/clock/reports`, `/my/clock/payslips` render with **no** `.o_fp_portal_sidebar` in the DOM.
- **Customer (share) user:** `/my/home` → customer dashboard **with** sidebar; all existing customer pages unchanged.
- **Payslips list:** shows only the logged-in employee's `done`/`paid` slips; another employee's slips never appear.
- **Payslip detail:** ownership guard blocks a slip belonging to someone else / a draft slip (redirect).
- **PDF button:** present only when the payslip report action exists; downloads the right slip.
- **Payroll absent:** Payslips tab hidden; `/my/clock/payslips` redirects cleanly.
Use ephemeral test ports per repo rule:
```bash
docker exec odoo-modsdev-app odoo -d fusion-dev --test-enable \
--test-tags /fusion_clock,/fusion_plating_portal \
-u fusion_clock,fusion_plating_portal --stop-after-init \
--http-port=0 --gevent-port=0 2>&1 | tail -60
```
---
## 9. Open items (verify during implementation)
1. **Payslip PDF report on entech.** Confirm whether Odoo's repackaged `hr_payroll` ships a per-employee payslip report (`hr_payroll.action_report_payslip` / `report_payslip[_lang]`) on entech. If present → wire the Download-PDF button to it. If absent → keep inline-only for v1 and log a follow-up to build a minimal paystub report in `fusion_payroll`.
2. **Exact `hr.payslip.state` values.** Confirm the selection (`draft`/`verify`/`done`/`paid`/`cancel`) on the installed payroll so the `('done','paid')` filter and the status chip labels are exact.
3. **`$0`-in-two-branches** load-time behaviour (see §5.2) — adopt the fallback if the conditional double-`$0` doesn't compile cleanly.
---
## 10. Deployment (entech)
Bump versions, then update both modules and bust the asset cache (SCSS/JS changed):
```bash
# update modules
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_clock,fusion_plating_portal --stop-after-init\" && systemctl start odoo'"
# asset cache bust (if needed)
# DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';
```