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>
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
# 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/%';
|
||||
```
|
||||
Reference in New Issue
Block a user