feat(plating-menu): Layer 1+2 — explicit groups on top-level menus + submenus

Phase D Tasks D1-D4 of permissions overhaul. Adds explicit groups=
attributes to:
- 9 top-level Plating menus (matrix per spec Section 2.E)
- Quality submenus: Audits, Customer Specs, AVL → QM-only
- Compliance hub child submenus (CGP, General, Safety, Aerospace,
  Nuclear) → QM-only
- Operations submenus: Maintenance, Move Log, Labor History → Shop
  Manager+; Replenishment Suggestions → Manager+

Replaces fragile inheritance + action-ACL-based visibility with
explicit per-menu gates. Now every role's menu tree is deterministic.

Also adds fusion_plating/tests/test_menu_visibility.py — per-role
matrix tests using ir.ui.menu.search_count with the test user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-24 01:35:11 -04:00
parent c34dfce6c3
commit 36cd4341a7
28 changed files with 136 additions and 37 deletions

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating',
'version': '19.0.21.0.2',
'version': '19.0.21.0.3',
'category': 'Manufacturing/Plating',
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
'description': """

View File

@@ -6,3 +6,4 @@ from . import test_simple_recipe_flatten
from . import test_role_groups
from . import test_acl_migration
from . import test_quality_split
from . import test_menu_visibility

View File

@@ -0,0 +1,85 @@
from odoo.tests.common import TransactionCase, tagged
@tagged('-at_install', 'post_install', 'fp_perms')
class TestMenuVisibility(TransactionCase):
"""Section 2.F of spec: per-role menu render matrix."""
def setUp(self):
super().setUp()
Users = self.env['res.users'].with_context(no_reset_password=True)
def mk(name, xmlid):
return Users.create({
'login': f'menu_{name}', 'name': f'Menu Test {name}',
'email': f'menu_{name}@example.com',
'groups_id': [(6, 0, [self.env.ref(xmlid).id])] if xmlid else [(6, 0, [])],
})
# "No" user has only base.group_user — no plating group
no_user = Users.create({
'login': 'menu_no', 'name': 'Menu Test no',
'email': 'menu_no@example.com',
})
no_user.write({'groups_id': [(6, 0, [self.env.ref('base.group_user').id])]})
self.u_no = no_user
self.u_tech = mk('tech', 'fusion_plating.group_fp_technician')
self.u_sr = mk('sr', 'fusion_plating.group_fp_sales_rep')
self.u_sm = mk('sm', 'fusion_plating.group_fp_shop_manager_v2')
self.u_smg = mk('smg', 'fusion_plating.group_fp_sales_manager')
self.u_mgr = mk('mgr', 'fusion_plating.group_fp_manager')
self.u_qm = mk('qm', 'fusion_plating.group_fp_quality_manager')
self.u_owner = mk('owner', 'fusion_plating.group_fp_owner')
def _visible(self, user, menu_xmlid):
menu = self.env.ref(menu_xmlid, raise_if_not_found=False)
if not menu:
return None # menu not installed
# An "invisible" menu is one the user can't read
return bool(self.env['ir.ui.menu'].with_user(user).search_count([('id', '=', menu.id)]))
def test_no_sees_no_plating_root(self):
result = self._visible(self.u_no, 'fusion_plating.menu_fp_root')
if result is None:
self.skipTest('Plating root menu not found')
self.assertFalse(result, '"No" role must not see Plating root')
def test_technician_sees_shop_floor(self):
result = self._visible(self.u_tech, 'fusion_plating_shopfloor.menu_fp_shopfloor_root')
if result is None:
self.skipTest('Shop Floor menu not found')
self.assertTrue(result)
def test_technician_does_not_see_sales(self):
result = self._visible(self.u_tech, 'fusion_plating_configurator.menu_fp_sales_root')
if result is None:
self.skipTest('Sales menu not found')
self.assertFalse(result, 'Technician must not see Sales & Quoting')
def test_sales_rep_sees_sales(self):
result = self._visible(self.u_sr, 'fusion_plating_configurator.menu_fp_sales_root')
if result is None:
self.skipTest('Sales menu not found')
self.assertTrue(result)
def test_sales_rep_does_not_see_shop_floor(self):
result = self._visible(self.u_sr, 'fusion_plating_shopfloor.menu_fp_shopfloor_root')
if result is None:
self.skipTest('Shop Floor menu not found')
self.assertFalse(result, 'Sales Rep must not see Shop Floor')
def test_manager_sees_quality(self):
result = self._visible(self.u_mgr, 'fusion_plating_quality.menu_fp_quality')
if result is None:
self.skipTest('Quality menu not found')
self.assertTrue(result)
def test_manager_does_not_see_compliance(self):
result = self._visible(self.u_mgr, 'fusion_plating_compliance.menu_fp_compliance_hub')
if result is None:
self.skipTest('Compliance hub not found')
self.assertFalse(result, 'Manager must not see Compliance hub')
def test_qm_sees_compliance(self):
result = self._visible(self.u_qm, 'fusion_plating_compliance.menu_fp_compliance_hub')
if result is None:
self.skipTest('Compliance hub not found')
self.assertTrue(result)

View File

@@ -116,13 +116,13 @@
</record>
<!-- Phase 1 — under Operations.
Phase 3 — supervisor+ only. Operators see their own moves on
the tablet; this is an audit view of every move. -->
Phase D (perms v2) — Shop Manager+ only. Operators see their
own moves on the tablet; this is an audit view of every move. -->
<menuitem id="menu_fp_job_step_move"
name="Parts &amp; Rack Move Log"
parent="menu_fp_operations"
action="action_fp_job_step_move"
sequence="90"
groups="fusion_plating.group_fusion_plating_supervisor"/>
groups="fusion_plating.group_fp_shop_manager_v2"/>
</odoo>

View File

@@ -133,10 +133,12 @@
</record>
<!-- Phase 1 — re-parented under Operations. -->
<!-- Phase D (perms v2) — Shop Manager+ only. Payroll/billing audit. -->
<menuitem id="menu_fp_labor_history"
name="Labor History"
parent="menu_fp_operations"
action="action_fp_labor_history"
sequence="95"/>
sequence="95"
groups="fusion_plating.group_fp_shop_manager_v2"/>
</odoo>

View File

@@ -22,14 +22,14 @@
sequence="46"
web_icon="fusion_plating,static/description/icon.png"
action="action_fp_resolve_plating_landing"
groups="group_fusion_plating_operator"/>
groups="fusion_plating.group_fp_technician,fusion_plating.group_fp_sales_rep"/>
<!-- ===== 2. CONFIGURATION + 7 Phase-2 buckets ===== -->
<menuitem id="menu_fp_config"
name="Configuration"
parent="menu_fp_root"
sequence="90"
groups="group_fusion_plating_manager"/>
groups="fusion_plating.group_fp_manager"/>
<menuitem id="menu_fp_config_shop_setup"
name="Shop Setup"
@@ -71,13 +71,14 @@
name="Compliance"
parent="menu_fp_root"
sequence="50"
groups="group_fusion_plating_supervisor"/>
groups="fusion_plating.group_fp_quality_manager"/>
<!-- ===== 4. OPERATIONS ===== -->
<menuitem id="menu_fp_operations"
name="Operations"
parent="menu_fp_root"
sequence="18"/>
sequence="18"
groups="fusion_plating.group_fp_technician"/>
<!-- ===== 5. CHILD MENUS ===== -->
@@ -112,13 +113,13 @@
action="action_fp_rack"
sequence="35"/>
<!-- Phase 3 — supervisor+: replenishment is a purchasing decision. -->
<!-- Phase D (perms v2) — Manager+: replenishment is a purchasing decision. -->
<menuitem id="menu_fp_replenishment_suggestions"
name="Replenishment Suggestions"
parent="menu_fp_operations"
action="action_fp_replenishment_suggestion"
sequence="40"
groups="fusion_plating.group_fusion_plating_supervisor"/>
groups="fusion_plating.group_fp_manager"/>
<!-- Configuration children (referencing the 7 buckets above) -->
<menuitem id="menu_fp_replenishment_rules"