From 36cd4341a7abfe81779216f55f6345ee5d7ac584 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 24 May 2026 01:35:11 -0400 Subject: [PATCH] =?UTF-8?q?feat(plating-menu):=20Layer=201+2=20=E2=80=94?= =?UTF-8?q?=20explicit=20groups=20on=20top-level=20menus=20+=20submenus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- fusion_plating/fusion_plating/__manifest__.py | 2 +- .../fusion_plating/tests/__init__.py | 1 + .../tests/test_menu_visibility.py | 85 +++++++++++++++++++ .../views/fp_job_step_move_views.xml | 6 +- .../views/fp_job_step_timelog_views.xml | 4 +- .../fusion_plating/views/fp_menu.xml | 13 +-- .../fusion_plating_aerospace/__manifest__.py | 2 +- .../views/fp_menu.xml | 3 +- .../__manifest__.py | 2 +- .../views/fp_maintenance_menu.xml | 4 +- .../fusion_plating_cgp/__manifest__.py | 2 +- .../fusion_plating_cgp/views/fp_menu.xml | 3 +- .../fusion_plating_compliance/__manifest__.py | 2 +- .../views/fp_menu.xml | 3 +- .../__manifest__.py | 2 +- .../views/fp_configurator_menu.xml | 2 +- .../fusion_plating_kpi/__manifest__.py | 2 +- .../fusion_plating_kpi/views/fp_menu.xml | 4 +- .../fusion_plating_nuclear/__manifest__.py | 2 +- .../fusion_plating_nuclear/views/fp_menu.xml | 3 +- .../fusion_plating_quality/__manifest__.py | 2 +- .../fusion_plating_quality/views/fp_menu.xml | 11 ++- .../fusion_plating_receiving/__manifest__.py | 2 +- .../views/fp_receiving_menu.xml | 2 +- .../fusion_plating_safety/__manifest__.py | 2 +- .../fusion_plating_safety/views/fp_menu.xml | 3 +- .../fusion_plating_shopfloor/__manifest__.py | 2 +- .../views/fp_menu.xml | 2 +- 28 files changed, 136 insertions(+), 37 deletions(-) create mode 100644 fusion_plating/fusion_plating/tests/test_menu_visibility.py 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"/> + + groups="fusion_plating.group_fp_quality_manager"/> + + groups="fusion_plating.group_fp_shop_manager_v2"/> + + groups="fusion_plating.group_fp_quality_manager"/> + parent="fusion_plating.menu_fp_compliance_hub" sequence="10" + groups="fusion_plating.group_fp_quality_manager"/> diff --git a/fusion_plating/fusion_plating_configurator/__manifest__.py b/fusion_plating/fusion_plating_configurator/__manifest__.py index e349e207..eacb52af 100644 --- a/fusion_plating/fusion_plating_configurator/__manifest__.py +++ b/fusion_plating/fusion_plating_configurator/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Configurator', - 'version': '19.0.21.8.1', + 'version': '19.0.21.8.2', 'category': 'Manufacturing/Plating', 'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.', 'description': """ diff --git a/fusion_plating/fusion_plating_configurator/views/fp_configurator_menu.xml b/fusion_plating/fusion_plating_configurator/views/fp_configurator_menu.xml index 6daaf472..815174e0 100644 --- a/fusion_plating/fusion_plating_configurator/views/fp_configurator_menu.xml +++ b/fusion_plating/fusion_plating_configurator/views/fp_configurator_menu.xml @@ -29,7 +29,7 @@ name="Sales" parent="fusion_plating.menu_fp_root" sequence="5" - groups="group_fp_estimator,fusion_plating.group_fusion_plating_supervisor"/> + groups="fusion_plating.group_fp_sales_rep"/> - + + groups="fusion_plating.group_fp_manager"/> + + groups="fusion_plating.group_fp_quality_manager"/> + groups="fusion_plating.group_fp_manager"/> + sequence="40" + groups="fusion_plating.group_fp_quality_manager"/> + sequence="70" + groups="fusion_plating.group_fp_quality_manager"/> + sequence="20" + groups="fusion_plating.group_fp_quality_manager"/> diff --git a/fusion_plating/fusion_plating_receiving/__manifest__.py b/fusion_plating/fusion_plating_receiving/__manifest__.py index 8b84adff..c6807bde 100644 --- a/fusion_plating/fusion_plating_receiving/__manifest__.py +++ b/fusion_plating/fusion_plating_receiving/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Receiving & Inspection', - 'version': '19.0.3.28.1', + 'version': '19.0.3.28.2', 'category': 'Manufacturing/Plating', 'summary': 'Parts receiving, inspection, damage logging, and manufacturing gate.', 'description': """ diff --git a/fusion_plating/fusion_plating_receiving/views/fp_receiving_menu.xml b/fusion_plating/fusion_plating_receiving/views/fp_receiving_menu.xml index 64868c43..5de3d72f 100644 --- a/fusion_plating/fusion_plating_receiving/views/fp_receiving_menu.xml +++ b/fusion_plating/fusion_plating_receiving/views/fp_receiving_menu.xml @@ -29,7 +29,7 @@ name="Shipping & Receiving" parent="fusion_plating.menu_fp_root" sequence="15" - groups="group_fp_receiving"/> + groups="fusion_plating.group_fp_shop_manager_v2"/> + + groups="fusion_plating.group_fp_quality_manager"/> + groups="fusion_plating.group_fp_technician"/>