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:
@@ -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.',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from . import test_portal_dashboard
|
||||
from . import test_employee_portal_gating
|
||||
|
||||
@@ -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', ''))
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user