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

13 KiB
Raw Blame History

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 onlyhr.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 1fusion_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:

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:

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_fromdate_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.xmlnew: 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:

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):

# 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/%';