feat(fusion_plating_portal): hide customer sidebar from internal staff + redirect them to the clock portal

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-30 21:33:12 -04:00
parent 47a6523e24
commit 60c25f8241
6 changed files with 99 additions and 12 deletions

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Customer Portal',
'version': '19.0.4.4.1',
'version': '19.0.4.5.0',
'category': 'Manufacturing/Plating',
'summary': 'Customer-facing portal for plating shops: online RFQ, job status, '
'CoC downloads, invoice access.',

View File

@@ -194,6 +194,9 @@ class FpCustomerPortal(CustomerPortal):
partner = request.env.user.partner_id
commercial = partner.commercial_partner_id
values['fp_partner_display_name'] = commercial.name or partner.name
# Internal staff (share=False) get the clean employee experience — no
# customer sidebar. Customers (share=True / portal users) keep it.
values['fp_show_customer_sidebar'] = bool(request.env.user.share)
return values
def _get_page_view_values(self, document, access_token, values, session_history, no_breadcrumbs, **kwargs):
@@ -208,6 +211,7 @@ class FpCustomerPortal(CustomerPortal):
layout = self._prepare_portal_layout_values()
values.setdefault('fp_sidebar_items', layout.get('fp_sidebar_items'))
values.setdefault('fp_partner_display_name', layout.get('fp_partner_display_name'))
values.setdefault('fp_show_customer_sidebar', layout.get('fp_show_customer_sidebar'))
return values
# ==========================================================================
@@ -616,6 +620,16 @@ class FpCustomerPortal(CustomerPortal):
website=True,
)
def home(self, **kw):
# Internal staff don't belong on the customer dashboard. Send them to
# the employee clock portal — but only when fusion_clock is installed
# (x_fclk_enable_clock proves it) AND the user actually has an employee
# record, otherwise /my/clock -> /my would bounce into a redirect loop.
user = request.env.user
if not user.share and 'hr.employee' in request.env:
Employee = request.env['hr.employee'].sudo()
if 'x_fclk_enable_clock' in Employee._fields and \
Employee.search_count([('user_id', '=', user.id)]):
return request.redirect('/my/clock')
partner = request.env.user.partner_id
commercial = partner.commercial_partner_id

View File

@@ -21,6 +21,14 @@
}
}
// Internal staff (employee portal) — no customer sidebar. Collapse the grid
// to a single column so page content isn't pushed right by the now-empty
// 240px sidebar track.
.o_fp_portal_shell--no-sidebar {
grid-template-columns: 1fr;
gap: 0;
}
.o_fp_portal_sidebar {
position: sticky;
top: $fp-space-4;

View File

@@ -1 +1,2 @@
from . import test_portal_dashboard
from . import test_employee_portal_gating

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1.
from odoo.tests import HttpCase, tagged
@tagged('post_install', '-at_install', 'fp_portal')
class TestEmployeePortalGating(HttpCase):
"""Internal staff get the clean employee experience (no customer sidebar,
redirected off the customer dashboard); customers are untouched."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.customer_partner = cls.env['res.partner'].create({
'name': 'Gating Customer Co.',
'email': 'gating_customer@example.com',
})
cls.customer_user = cls.env['res.users'].create({
'name': 'Gating Portal User',
'login': 'gating_portal_user',
'password': 'gating_portal_user',
'partner_id': cls.customer_partner.id,
'group_ids': [(6, 0, [cls.env.ref('base.group_portal').id])],
})
def test_customer_sees_sidebar_on_home(self):
"""A share/portal user still gets the FP customer sidebar shell."""
self.assertTrue(self.customer_user.share)
self.authenticate('gating_portal_user', 'gating_portal_user')
r = self.url_open('/my/home')
self.assertEqual(r.status_code, 200)
self.assertIn('o_fp_portal_sidebar', r.text,
"customer should still see the FP sidebar shell")
def test_internal_employee_redirected_to_clock(self):
"""An internal user with an employee record is bounced to /my/clock.
Only meaningful when fusion_clock is installed (the redirect guard
checks for its x_fclk_enable_clock field, so it never sends anyone to
a non-existent /my/clock). Skip otherwise.
"""
if 'hr.employee' not in self.env:
self.skipTest('hr not installed')
if 'x_fclk_enable_clock' not in self.env['hr.employee']._fields:
self.skipTest('fusion_clock not installed — redirect intentionally inert')
internal = self.env['res.users'].create({
'name': 'Shop Hand',
'login': 'gating_shop_hand',
'password': 'gating_shop_hand',
'group_ids': [(6, 0, [self.env.ref('base.group_user').id])],
})
self.assertFalse(internal.share)
self.env['hr.employee'].create({'name': 'Shop Hand', 'user_id': internal.id})
self.authenticate('gating_shop_hand', 'gating_shop_hand')
# Don't follow the redirect — just assert we're bounced toward /my/clock.
r = self.url_open('/my/home', allow_redirects=False)
self.assertIn(r.status_code, (301, 302, 303, 307, 308))
self.assertIn('/my/clock', r.headers.get('Location', ''))

View File

@@ -57,17 +57,21 @@
content slot inside #wrap is preserved verbatim.
-->
<xpath expr="//div[@id='wrap']" position="replace">
<div class="o_fp_portal_shell">
<!-- Mobile hamburger (shown only below 768px via SCSS) -->
<button type="button"
class="o_fp_portal_hamburger d-md-none"
aria-label="Open navigation">
<i class="fa fa-bars"/>
</button>
<!-- Backdrop for mobile drawer (hidden by default) -->
<div class="o_fp_portal_backdrop"/>
<!-- Sidebar navigation component -->
<t t-call="fusion_plating_portal.fp_portal_sidebar"/>
<div t-attf-class="o_fp_portal_shell#{'' if (fp_show_customer_sidebar if fp_show_customer_sidebar is defined else True) else ' o_fp_portal_shell--no-sidebar'}">
<!-- Sidebar chrome only for customers (share users). Internal
staff get the clean employee experience with no sidebar. -->
<t t-if="fp_show_customer_sidebar if fp_show_customer_sidebar is defined else True">
<!-- Mobile hamburger (shown only below 768px via SCSS) -->
<button type="button"
class="o_fp_portal_hamburger d-md-none"
aria-label="Open navigation">
<i class="fa fa-bars"/>
</button>
<!-- Backdrop for mobile drawer (hidden by default) -->
<div class="o_fp_portal_backdrop"/>
<!-- Sidebar navigation component -->
<t t-call="fusion_plating_portal.fp_portal_sidebar"/>
</t>
<!-- Main content area — original #wrap re-emitted here via $0 -->
<main class="o_fp_portal_main">$0</main>
</div>