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>
13 KiB
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; ownshr.payslipwith full earnings/deductions/YTD data. No employee self-service surface today (payslips are backend-only).
Two concrete problems:
-
The customer portal applies to everyone.
fusion_plating_portal'shome()override (/my,/my/home) returns the customer dashboard for every logged-in user, with no internal-vs-customer check. Internal employees who land on/mysee a customer dashboard that is empty/irrelevant to them. -
The customer sidebar bleeds onto the clock page.
fp_portal_shell(fusion_plating_portal/views/fp_portal_shell.xml) inheritsportal.portal_layoutand injects the left sidebar into all/my/*pages, unconditionally. Becausefusion_clock's/my/clockpage renders insideportal.portal_layout, it inherits that customer sidebar even though the clock UI was designed clean (it setsno_header/no_breadcrumbsand 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_sidebarto 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.payslipthrough a soft check ('hr.payslip' in request.env) — nofusion_payrolldependency added, sofusion_clockstays 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/homeon 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_clockdoes not overridehome()).
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_sidebartrue → 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.payslipsudo()whereemployee_id == employee.idandstate in ('done','paid'), ordereddate_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'tstate in ('done','paid'), oremployee_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_idstotals). - 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 throughfusion_pdf_previewwhere 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 onfp_show_customer_sidebar.__manifest__.py— version bump.
fusion_clock
controllers/portal_clock.py—/my/clock/payslips+/my/clock/payslips/<id>routes;show_payslipsflag 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 (reusefclk-*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_payrollnot 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:
/myand/my/home→ 302 to/my/clock;/my/clock,/my/clock/timesheets,/my/clock/reports,/my/clock/payslipsrender with no.o_fp_portal_sidebarin 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/paidslips; 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/payslipsredirects 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)
- Payslip PDF report on entech. Confirm whether Odoo's repackaged
hr_payrollships 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 infusion_payroll. - Exact
hr.payslip.statevalues. Confirm the selection (draft/verify/done/paid/cancel) on the installed payroll so the('done','paid')filter and the status chip labels are exact. $0-in-two-branches load-time behaviour (see §5.2) — adopt the fallback if the conditional double-$0doesn'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/%';