diff --git a/fusion_plating/fusion_plating/__manifest__.py b/fusion_plating/fusion_plating/__manifest__.py
index 8708c613..deeab6a5 100644
--- a/fusion_plating/fusion_plating/__manifest__.py
+++ b/fusion_plating/fusion_plating/__manifest__.py
@@ -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': """
diff --git a/fusion_plating/fusion_plating/tests/__init__.py b/fusion_plating/fusion_plating/tests/__init__.py
index ae483a5d..be2bf5fd 100644
--- a/fusion_plating/fusion_plating/tests/__init__.py
+++ b/fusion_plating/fusion_plating/tests/__init__.py
@@ -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
diff --git a/fusion_plating/fusion_plating/tests/test_menu_visibility.py b/fusion_plating/fusion_plating/tests/test_menu_visibility.py
new file mode 100644
index 00000000..dc0699df
--- /dev/null
+++ b/fusion_plating/fusion_plating/tests/test_menu_visibility.py
@@ -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)
diff --git a/fusion_plating/fusion_plating/views/fp_job_step_move_views.xml b/fusion_plating/fusion_plating/views/fp_job_step_move_views.xml
index f1d06e39..5da4533b 100644
--- a/fusion_plating/fusion_plating/views/fp_job_step_move_views.xml
+++ b/fusion_plating/fusion_plating/views/fp_job_step_move_views.xml
@@ -116,13 +116,13 @@
+ Phase D (perms v2) — Shop Manager+ only. Operators see their
+ own moves on the tablet; this is an audit view of every move. -->
+ groups="fusion_plating.group_fp_shop_manager_v2"/>
diff --git a/fusion_plating/fusion_plating/views/fp_job_step_timelog_views.xml b/fusion_plating/fusion_plating/views/fp_job_step_timelog_views.xml
index 49c263dc..dbc559ac 100644
--- a/fusion_plating/fusion_plating/views/fp_job_step_timelog_views.xml
+++ b/fusion_plating/fusion_plating/views/fp_job_step_timelog_views.xml
@@ -133,10 +133,12 @@
+
+ sequence="95"
+ groups="fusion_plating.group_fp_shop_manager_v2"/>
diff --git a/fusion_plating/fusion_plating/views/fp_menu.xml b/fusion_plating/fusion_plating/views/fp_menu.xml
index 14b6190c..796903c1 100644
--- a/fusion_plating/fusion_plating/views/fp_menu.xml
+++ b/fusion_plating/fusion_plating/views/fp_menu.xml
@@ -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"/>
+ groups="fusion_plating.group_fp_manager"/>
+ groups="fusion_plating.group_fp_quality_manager"/>
+ sequence="18"
+ groups="fusion_plating.group_fp_technician"/>
@@ -112,13 +113,13 @@
action="action_fp_rack"
sequence="35"/>
-
+
+ groups="fusion_plating.group_fp_manager"/>