From 560ffa2cdf3cd4d1d2ccb897e621f76255a8a763 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 24 May 2026 00:43:00 -0400 Subject: [PATCH 01/18] =?UTF-8?q?docs(plating):=20permissions=20overhaul?= =?UTF-8?q?=20Phase=201=20=E2=80=94=20spec=20+=20implementation=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec describes consolidation of 12 res.groups into 8 roles (No / Technician / Sales Rep / Shop Manager / Sales Manager / Manager / Quality Manager / Owner), role-based landing-page defaults, Owner-only Team management page, and dry-run + Owner-approval migration workflow. Plan breaks the work into 9 phases (A through I), ~40 TDD tasks, with explicit file lists and entech deploy commands. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...-05-24-permissions-overhaul-phase1-plan.md | 2674 +++++++++++++++++ .../2026-05-23-permissions-overhaul-design.md | 858 ++++++ 2 files changed, 3532 insertions(+) create mode 100644 fusion_plating/docs/superpowers/plans/2026-05-24-permissions-overhaul-phase1-plan.md create mode 100644 fusion_plating/docs/superpowers/specs/2026-05-23-permissions-overhaul-design.md diff --git a/fusion_plating/docs/superpowers/plans/2026-05-24-permissions-overhaul-phase1-plan.md b/fusion_plating/docs/superpowers/plans/2026-05-24-permissions-overhaul-phase1-plan.md new file mode 100644 index 00000000..ba42ff04 --- /dev/null +++ b/fusion_plating/docs/superpowers/plans/2026-05-24-permissions-overhaul-phase1-plan.md @@ -0,0 +1,2674 @@ +# Fusion Plating Permissions Overhaul Phase 1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Consolidate the 12 current Fusion Plating `res.groups` into 8 clean roles (No / Technician / Sales Representative / Shop Manager / Sales Manager / Manager / Quality Manager / Owner), add role-based landing-page defaults, build an Owner-only Team management page, and ship a dry-run + Owner-approval migration workflow. + +**Architecture:** New groups are defined alongside the old ones in `fusion_plating/security/fp_security_v2.xml` with `implied_ids` chains that include the old groups (so existing ACLs keep working as backward-compat). ACL CSVs are then re-pointed to the new groups in a mechanical sweep. Quality permissions split between Manager (reactive: NCR/Hold/Cert/RMA) and QM (strategic: CAPA/Audit/AVL/FAIR/Nadcap/CGP) via per-model ACL changes plus two new `ir.rule` records on `fp.certificate`. Landing resolver gets a role-based dispatch step that respects the existing `ir.config_parameter['fusion_plating_shopfloor.layout']` flag. Team page is built with standard Odoo views (kanban grouped by a new `res.users.x_fc_plating_role` Selection field with compute/inverse) — zero custom OWL. Migration workflow ships as a `fp.migration.preview` model that runs in `pending` state on `-u` and only applies when an Owner clicks "Approve & Run" inside the preview UI. + +**Tech Stack:** Odoo 19, Python 3.11, PostgreSQL 15, XML data files, QWeb templates, standard Odoo kanban/form/list views (no OWL custom components). + +**Source spec:** [`docs/superpowers/specs/2026-05-23-permissions-overhaul-design.md`](../specs/2026-05-23-permissions-overhaul-design.md) + +**Target deployment:** entech LXC 111 (`pve-worker5`), database `admin`, addons at `/mnt/extra-addons/custom/`. + +--- + +## File Structure (master list) + +### New files + +| Path | Responsibility | +|---|---| +| `fusion_plating/security/fp_security_v2.xml` | The 8 new groups + implied_ids chains | +| `fusion_plating/models/fp_migration.py` | `fp.migration.preview` + `fp.migration.preview.line` models | +| `fusion_plating/models/fp_role_constants.py` | `PLATING_ROLE_DESCRIPTIONS` dict + `_NEW_ROLE_XMLID` mapping + `_FP_OLD_GROUP_XMLIDS` list | +| `fusion_plating/views/fp_team_views.xml` | Team kanban, role-reference template, audit-log list, menu, action | +| `fusion_plating/views/fp_migration_views.xml` | Preview list, form, action, "Approve & Run" + "Rollback" buttons | +| `fusion_plating/data/fp_migration_cron.xml` | 30-day purge cron | +| `fusion_plating/migrations/19.0.21.0.0/post-migrate.py` | Optional sanity backfill (idempotent re-seed of group memberships for uid 1/2) | +| `fusion_plating/tests/test_role_groups.py` | Group structure / implied_ids / auto-assign tests | +| `fusion_plating/tests/test_role_compute_inverse.py` | `x_fc_plating_role` compute + inverse tests | +| `fusion_plating/tests/test_landing_resolver.py` | Per-role resolver dispatch tests | +| `fusion_plating/tests/test_team_page.py` | Drag-and-drop role change tests | +| `fusion_plating/tests/test_migration_workflow.py` | Dry-run, approve, rollback tests | +| `fusion_plating/tests/test_sales_manager_gate.py` | SO confirm gate tests | +| `fusion_plating/tests/test_quality_split.py` | Manager-vs-QM quality permission tests | +| `fusion_plating/tests/test_menu_visibility.py` | Per-role menu render tests | + +### Modified files (security CSVs — sweep) + +| Path | Change | +|---|---| +| `fusion_plating/security/ir.model.access.csv` | Re-point all rows from old → new groups per Section 2.A of spec | +| `fusion_plating_configurator/security/ir.model.access.csv` | Same sweep | +| `fusion_plating_invoicing/security/ir.model.access.csv` | Same sweep | +| `fusion_plating_receiving/security/ir.model.access.csv` | Same sweep | +| `fusion_plating_quality/security/ir.model.access.csv` | Sweep + QM/Manager split for CAPA/Audit/AVL/CustomerSpec/DocControl | +| `fusion_plating_certificates/security/ir.model.access.csv` | Sweep | +| `fusion_plating_cgp/security/ir.model.access.csv` | Re-gate all CGP ACLs to QM | +| `fusion_plating_aerospace/security/ir.model.access.csv` | Sweep | +| `fusion_plating_nuclear/security/ir.model.access.csv` | Sweep | +| `fusion_plating_safety/security/ir.model.access.csv` | Sweep | +| `fusion_plating_jobs/security/ir.model.access.csv` | Sweep | +| `fusion_plating_shopfloor/security/ir.model.access.csv` | Sweep | + +### Modified files (ir.rule) + +| Path | Change | +|---|---| +| `fusion_plating_cgp/security/fp_cgp_security.xml` | `fp.cgp.psa` + `fp.cgp.security.incident` rules re-gated to QM | +| `fusion_plating_certificates/security/fp_cert_security.xml` (may need creation) | NEW ir.rule for `cert_type in ('fair','nadcap')` write restriction to QM | + +### Modified files (menu visibility — Layer 1 + 2) + +| Path | Change | +|---|---| +| `fusion_plating/views/fp_menu.xml` | Plating root + Operations + Configuration top-level gates | +| `fusion_plating_configurator/views/fp_configurator_menu.xml` | Sales & Quoting top-level + submenus | +| `fusion_plating_shopfloor/views/fp_shopfloor_menu.xml` | Shop Floor top-level | +| `fusion_plating_receiving/views/fp_receiving_menu.xml` | Receiving & Shipping top-level + children | +| `fusion_plating_quality/views/fp_menu.xml` | Quality top-level + per-child split (Audits/AVL/Specs to QM) | +| `fusion_plating_compliance/views/fp_menu.xml` + verticals | Compliance hub + child verticals all to QM | +| `fusion_plating_kpi/views/fp_kpi_menu.xml` | KPIs top-level to Manager | +| `fusion_plating_invoicing/views/fp_invoicing_menu.xml` | Children re-parent (Accounting folds into Manager) | +| `fusion_plating_jobs/views/jobs_in_shopfloor_menu.xml` | Sweep any operator/supervisor refs | + +### Modified files (field/button visibility — Layer 3) + +| Path | Change | +|---|---| +| `fusion_plating_configurator/views/sale_order_views.xml` | Hide Confirm button from non-Sales-Manager; hide pricing columns from non-Sales-Rep | +| `fusion_plating_invoicing/views/res_partner_views.xml` | Account-hold-override field to `group_fp_manager` (fixes `_administrator` typo) | +| `fusion_plating_quality/views/fp_capa_views.xml` | Close button + edit fields to QM | +| `fusion_plating_quality/views/fp_audit_views.xml` | All buttons to QM | +| `fusion_plating_quality/views/fp_avl_views.xml` | Approve / Disqualify buttons to QM | +| `fusion_plating_quality/views/fp_customer_spec_views.xml` | Edit fields to QM | +| `fusion_plating_certificates/views/fp_certificate_views.xml` | Sign button on FAIR/Nadcap to QM | +| `fusion_plating_cgp/views/*_views.xml` | All CGP form buttons to QM | +| Various smart-button host views | Match underlying action visibility | + +### Modified files (Python — bypass flags + bug fixes) + +| Path | Change | +|---|---| +| `fusion_plating_invoicing/models/res_partner.py:33-35` | Fix `_administrator` typo → `group_fp_manager` | +| `fusion_plating_configurator/wizard/fp_direct_order_wizard.py:467` | Fix same typo | +| `fusion_plating_jobs/models/fp_job_step.py` | Update bypass-flag group checks from old Manager → `group_fp_manager` | +| `fusion_plating_jobs/models/fp_job.py` | Same | +| `fusion_plating_quality/controllers/qc_controller.py` | Same | +| `fusion_plating_shopfloor/controllers/_tablet_audit.py` | Same | + +### Modified files (Python — model fields) + +| Path | Change | +|---|---| +| `fusion_plating/models/res_users.py` | Add `x_fc_plating_role` Selection field + compute + inverse | +| `fusion_plating/models/res_company.py` | Add `x_fc_cgp_designated_official_id` + `x_fc_nadcap_authority_user_id` | +| `fusion_plating/views/res_company_views.xml` | Surface the two DO fields on company form | +| `fusion_plating/models/sale_order.py` (may need creation) | Override `action_confirm` to gate on `group_fp_sales_manager` | +| `fusion_plating/models/fp_landing.py` | Replace landing resolver with role-based dispatch (Section 3 of spec) | +| `fusion_plating_shopfloor/views/manager_dashboard_action.xml` | Add `x_fc_pickable_landing=True` | +| `fusion_plating_shopfloor/views/plant_kanban_action.xml` | Add `x_fc_pickable_landing=True` | +| `fusion_plating_shopfloor/views/shopfloor_landing_action.xml` | Add `x_fc_pickable_landing=True` | +| `fusion_plating_quality/views/fp_quality_dashboard_action.xml` | Add `x_fc_pickable_landing=True` | + +### Modified files (manifests — version bump) + +| Path | Bump to | +|---|---| +| `fusion_plating/__manifest__.py` | 19.0.21.0.0 | +| `fusion_plating_configurator/__manifest__.py` | 19.0.22.0.0 | +| `fusion_plating_invoicing/__manifest__.py` | next minor | +| `fusion_plating_receiving/__manifest__.py` | next minor | +| `fusion_plating_quality/__manifest__.py` | 19.0.5.0.0 | +| `fusion_plating_certificates/__manifest__.py` | 19.0.6.0.0 | +| `fusion_plating_cgp/__manifest__.py` | next minor | +| `fusion_plating_aerospace/__manifest__.py` | next minor | +| `fusion_plating_nuclear/__manifest__.py` | next minor | +| `fusion_plating_safety/__manifest__.py` | next minor | +| `fusion_plating_shopfloor/__manifest__.py` | 19.0.25.0.0 | +| `fusion_plating_jobs/__manifest__.py` | 19.0.11.0.0 | + +### Modified files (docs) + +| Path | Change | +|---|---| +| `K:\Github\Odoo-Modules\fusion_plating\CLAUDE.md` | Update role hierarchy section + add Phase 1 permissions-overhaul section | + +--- + +## Phase A — New Group Definitions (foundation) + +Defines the 8 new groups, sets up implied_ids chains (new groups imply old ones for backward-compat), auto-assigns Owner to uid 1+2. Existing ACLs keep working because new groups carry old groups as implications. No ACL CSVs touched yet. + +### Task A1: Bump `fusion_plating` manifest version + +**Files:** +- Modify: `fusion_plating/__manifest__.py` + +- [ ] **Step 1: Edit manifest version** + +Open `fusion_plating/__manifest__.py` and change the `version` line (currently `19.0.20.x.x` per CLAUDE.md "Sub 12c") to `19.0.21.0.0`. Also add `'security/fp_security_v2.xml'` to the `data` list, placed AFTER the existing `'security/fp_security.xml'` entry so the new groups can reference the existing privilege record. + +- [ ] **Step 2: Commit** + +```bash +git add fusion_plating/__manifest__.py +git commit -m "chore(plating): bump version to 19.0.21.0.0 for permissions overhaul" +``` + +### Task A2: Define the 8 new groups in security XML + +**Files:** +- Create: `fusion_plating/security/fp_security_v2.xml` + +- [ ] **Step 1: Write the file** + +```xml + + + + + + + + Technician + 10 + + + + + + Sales Representative + 20 + + + + + + Shop Manager + 30 + + + + + + Sales Manager + 40 + + + + + + Manager + 50 + + + + + + Quality Manager + 60 + + + + + + Owner + 70 + + + + + + +``` + +- [ ] **Step 2: Verify the file parses** + +```bash +python -c "import lxml.etree as et; et.parse('fusion_plating/security/fp_security_v2.xml'); print('XML OK')" +``` +Expected: `XML OK` + +- [ ] **Step 3: Commit** + +```bash +git add fusion_plating/security/fp_security_v2.xml +git commit -m "feat(plating-sec): add 8 consolidated role groups (technician → owner)" +``` + +### Task A3: Write group-structure tests + +**Files:** +- Create: `fusion_plating/tests/test_role_groups.py` + +- [ ] **Step 1: Write the test file** + +```python +from odoo.tests.common import TransactionCase, tagged + + +@tagged('-at_install', 'post_install', 'fp_perms') +class TestRoleGroupsStructure(TransactionCase): + """Verify the 8 new roles exist with correct implied_ids chains.""" + + def test_all_eight_groups_exist(self): + names = { + 'group_fp_technician', 'group_fp_sales_rep', + 'group_fp_shop_manager_v2', 'group_fp_sales_manager', + 'group_fp_manager', 'group_fp_quality_manager', 'group_fp_owner', + } + for xmlid in names: + grp = self.env.ref(f'fusion_plating.{xmlid}', raise_if_not_found=False) + self.assertTrue(grp, f'Group {xmlid} not found') + + def test_owner_implies_quality_manager(self): + owner = self.env.ref('fusion_plating.group_fp_owner') + qm = self.env.ref('fusion_plating.group_fp_quality_manager') + self.assertIn(qm, owner.implied_ids) + + def test_owner_implies_system(self): + owner = self.env.ref('fusion_plating.group_fp_owner') + system = self.env.ref('base.group_system') + self.assertIn(system, owner.trans_implied_ids, + 'Owner must transitively imply base.group_system') + + def test_manager_implies_both_branches(self): + mgr = self.env.ref('fusion_plating.group_fp_manager') + sm = self.env.ref('fusion_plating.group_fp_shop_manager_v2') + sales_mgr = self.env.ref('fusion_plating.group_fp_sales_manager') + self.assertIn(sm, mgr.implied_ids, 'Manager must imply Shop Manager (diamond)') + self.assertIn(sales_mgr, mgr.implied_ids, 'Manager must imply Sales Manager (diamond)') + + def test_technician_does_not_imply_sales_rep(self): + tech = self.env.ref('fusion_plating.group_fp_technician') + sales_rep = self.env.ref('fusion_plating.group_fp_sales_rep') + self.assertNotIn(sales_rep, tech.trans_implied_ids, + 'Technician must NOT see Sales Rep menus') + + def test_sales_rep_does_not_imply_technician(self): + sales_rep = self.env.ref('fusion_plating.group_fp_sales_rep') + tech = self.env.ref('fusion_plating.group_fp_technician') + self.assertNotIn(tech, sales_rep.trans_implied_ids, + 'Sales Rep must NOT see Workstation') + + def test_owner_auto_assigned_to_uid_1_and_2(self): + owner = self.env.ref('fusion_plating.group_fp_owner') + user_ids = owner.user_ids.ids + self.assertIn(1, user_ids, 'Owner must include uid 1 (__system__)') + self.assertIn(2, user_ids, 'Owner must include uid 2 (admin)') + + def test_sequence_numbers_are_unique(self): + seqs = [ + self.env.ref(f'fusion_plating.{x}').sequence + for x in ('group_fp_technician', 'group_fp_sales_rep', + 'group_fp_shop_manager_v2', 'group_fp_sales_manager', + 'group_fp_manager', 'group_fp_quality_manager', 'group_fp_owner') + ] + self.assertEqual(len(seqs), len(set(seqs)), + f'All sequence numbers must be unique, got {seqs}') + + def test_new_groups_imply_old_for_backward_compat(self): + """During the 30-day rollback window, new groups must trigger old ACLs.""" + tech = self.env.ref('fusion_plating.group_fp_technician') + old_op = self.env.ref('fusion_plating.group_fusion_plating_operator') + self.assertIn(old_op, tech.trans_implied_ids) + + mgr = self.env.ref('fusion_plating.group_fp_manager') + old_mgr = self.env.ref('fusion_plating.group_fusion_plating_manager') + self.assertIn(old_mgr, mgr.trans_implied_ids) +``` + +- [ ] **Step 2: Run the test — verify it FAILS** (groups not deployed yet) + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin --test-tags fp_perms --test-enable --stop-after-init -u fusion_plating\"'" 2>&1 | grep -E '(FAIL|OK|ERROR)' +``` +Expected: tests run AFTER the module updates with the new XML; they will PASS on this iteration because Task A2 already deployed the groups. (Document: if the prior module update was on a different DB, run `-u fusion_plating` first to materialise the new groups.) + +- [ ] **Step 3: If tests fail, fix Task A2 XML and re-run** + +- [ ] **Step 4: Commit** + +```bash +git add fusion_plating/tests/test_role_groups.py +git commit -m "test(plating-sec): verify 8-role hierarchy + implied_ids chains" +``` + +### Task A4: Add deprecation note to old group display names + +**Files:** +- Modify: `fusion_plating/security/fp_security.xml` +- Modify: `fusion_plating_configurator/security/fp_configurator_security.xml` +- Modify: `fusion_plating_invoicing/security/fp_invoicing_security.xml` +- Modify: `fusion_plating_receiving/security/fp_receiving_security.xml` +- Modify: `fusion_plating_cgp/security/fp_cgp_security.xml` +- Modify: `fusion_plating_jobs/security/legacy_groups.xml` + +- [ ] **Step 1: Prefix each old group's name with `[DEPRECATED] `** + +For each file, find each `` line on the OLD group records and prefix with `[DEPRECATED] `. Example for `fusion_plating/security/fp_security.xml` on the Operator group: + +```xml +[DEPRECATED] Operator +``` + +Do NOT remove the records — they're needed for backward-compat during the 30-day rollback window. Do NOT change the XML IDs. + +- [ ] **Step 2: Commit** + +```bash +git add fusion_plating/security/fp_security.xml \ + fusion_plating_configurator/security/fp_configurator_security.xml \ + fusion_plating_invoicing/security/fp_invoicing_security.xml \ + fusion_plating_receiving/security/fp_receiving_security.xml \ + fusion_plating_cgp/security/fp_cgp_security.xml \ + fusion_plating_jobs/security/legacy_groups.xml +git commit -m "chore(plating-sec): mark old groups as [DEPRECATED] in display name" +``` + +### Task A5: Deploy Phase A to a test DB and smoke-verify + +- [ ] **Step 1: Deploy to entech test DB** + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && \ + su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin \ + -u fusion_plating --stop-after-init\" && systemctl start odoo'" +``` + +- [ ] **Step 2: SQL check that the 8 groups exist** + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\" + SELECT g.id, g.name, g.sequence + FROM res_groups g JOIN ir_model_data d ON d.res_id = g.id AND d.model = 'res.groups' + WHERE d.module = 'fusion_plating' AND d.name LIKE 'group_fp_%' + ORDER BY g.sequence; +\\\"\"'" +``` +Expected: 7 rows in sequence order 10/20/30/40/50/60/70 (the 8th role "No" has no group record). + +- [ ] **Step 3: Verify Owner auto-assigned to uid 1+2** + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\" + SELECT r.uid, u.login FROM res_groups_users_rel r + JOIN res_users u ON u.id = r.uid + WHERE r.gid = (SELECT res_id FROM ir_model_data + WHERE module='fusion_plating' AND name='group_fp_owner'); +\\\"\"'" +``` +Expected: 2 rows (uid 1 = __system__, uid 2 = admin). + +--- + +## Phase B — ACL Mechanical Migration Sweep + +Re-points every ACL CSV row from old group xmlids → new group xmlids per the standard mapping. Backward-compat preserved because new groups still imply old ones from Phase A. Tests verify both sets work simultaneously. + +### Task B1: Write ACL migration test scaffolding + +**Files:** +- Create: `fusion_plating/tests/test_acl_migration.py` + +- [ ] **Step 1: Write the test** + +```python +from odoo.tests.common import TransactionCase, tagged + + +@tagged('-at_install', 'post_install', 'fp_perms') +class TestAclMigration(TransactionCase): + """Sample-based ACL coverage: pick 1 model per role and verify access.""" + + def setUp(self): + super().setUp() + Users = self.env['res.users'].with_context(no_reset_password=True) + # Build one test user per role, freshly assigned + def make(login, group_xmlid): + user = Users.create({ + 'login': f'fp_test_{login}', + 'name': f'FP Test {login.title()}', + 'email': f'fp_test_{login}@example.com', + 'groups_id': [(6, 0, [self.env.ref(group_xmlid).id])], + }) + return user + + self.u_tech = make('tech', 'fusion_plating.group_fp_technician') + self.u_sm = make('sm', 'fusion_plating.group_fp_shop_manager_v2') + self.u_mgr = make('mgr', 'fusion_plating.group_fp_manager') + self.u_qm = make('qm', 'fusion_plating.group_fp_quality_manager') + self.u_sr = make('sr', 'fusion_plating.group_fp_sales_rep') + self.u_smg = make('smg', 'fusion_plating.group_fp_sales_manager') + + def test_technician_can_read_jobs(self): + Jobs = self.env['fp.job'].with_user(self.u_tech) + Jobs.check_access_rights('read') # raises if no access + + def test_technician_cannot_read_part_catalog(self): + from odoo.exceptions import AccessError + Parts = self.env['fp.part.catalog'].with_user(self.u_tech) + with self.assertRaises(AccessError): + Parts.check_access_rights('read') + + def test_sales_rep_can_read_part_catalog(self): + Parts = self.env['fp.part.catalog'].with_user(self.u_sr) + Parts.check_access_rights('read') + + def test_shop_manager_can_read_receiving(self): + Rec = self.env['fp.receiving'].with_user(self.u_sm) + Rec.check_access_rights('read') + + def test_manager_can_create_ncr(self): + Ncr = self.env['fusion.plating.ncr'].with_user(self.u_mgr) + Ncr.check_access_rights('create') + + def test_manager_can_only_read_capa(self): + from odoo.exceptions import AccessError + Capa = self.env['fusion.plating.capa'].with_user(self.u_mgr) + Capa.check_access_rights('read') + with self.assertRaises(AccessError): + Capa.check_access_rights('write') + + def test_qm_can_write_capa(self): + Capa = self.env['fusion.plating.capa'].with_user(self.u_qm) + Capa.check_access_rights('write') +``` + +- [ ] **Step 2: Run to see initial failures** (most assertions fail until Phase B + C complete) + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin --test-tags fp_perms --test-enable --stop-after-init -u fusion_plating\"'" 2>&1 | tail -30 +``` +Expected: several FAILs (acceptable; we'll fix in B2-B5 + C). + +- [ ] **Step 3: Commit** + +```bash +git add fusion_plating/tests/test_acl_migration.py +git commit -m "test(plating-sec): scaffolding for per-role ACL coverage" +``` + +### Task B2: Sweep ACL CSVs — core modules + +**Files:** +- Modify: `fusion_plating/security/ir.model.access.csv` +- Modify: `fusion_plating_jobs/security/ir.model.access.csv` +- Modify: `fusion_plating_shopfloor/security/ir.model.access.csv` +- Modify: `fusion_plating_certificates/security/ir.model.access.csv` + +- [ ] **Step 1: Apply the mechanical replacement table per file** + +In each CSV, do a literal text replace for each pattern (NOT regex — exact string match), case-sensitive: + +| Find | Replace with | +|---|---| +| `fusion_plating.group_fusion_plating_operator` | `fusion_plating.group_fp_technician` | +| `fusion_plating.group_fusion_plating_supervisor` | `fusion_plating.group_fp_shop_manager_v2` | +| `fusion_plating.group_fusion_plating_manager` | `fusion_plating.group_fp_manager` | +| `fusion_plating.group_fusion_plating_admin` | `fusion_plating.group_fp_owner` | +| `group_fusion_plating_operator` (bare) | `group_fp_technician` | +| `group_fusion_plating_supervisor` (bare) | `group_fp_shop_manager_v2` | +| `group_fusion_plating_manager` (bare) | `group_fp_manager` | +| `group_fusion_plating_admin` (bare) | `group_fp_owner` | + +Use the Read tool to inspect each file, then Edit with `replace_all: true` per substitution. + +- [ ] **Step 2: Smoke-check no orphaned references** + +```bash +grep -E 'group_fusion_plating_(operator|supervisor|manager|admin)' \ + fusion_plating/security/ir.model.access.csv \ + fusion_plating_jobs/security/ir.model.access.csv \ + fusion_plating_shopfloor/security/ir.model.access.csv \ + fusion_plating_certificates/security/ir.model.access.csv +``` +Expected: empty output (zero matches). + +- [ ] **Step 3: Commit** + +```bash +git add fusion_plating/security/ir.model.access.csv \ + fusion_plating_jobs/security/ir.model.access.csv \ + fusion_plating_shopfloor/security/ir.model.access.csv \ + fusion_plating_certificates/security/ir.model.access.csv +git commit -m "refactor(plating-sec): sweep ACLs in core/jobs/shopfloor/certs to new groups" +``` + +### Task B3: Sweep ACL CSVs — sales, accounting, receiving + +**Files:** +- Modify: `fusion_plating_configurator/security/ir.model.access.csv` +- Modify: `fusion_plating_invoicing/security/ir.model.access.csv` +- Modify: `fusion_plating_receiving/security/ir.model.access.csv` + +- [ ] **Step 1: Apply replacement table (same as B2, plus these)** + +Additional replacements for these modules: + +| Find | Replace with | +|---|---| +| `fusion_plating_configurator.group_fp_estimator` | `fusion_plating.group_fp_sales_rep` | +| `group_fp_estimator` (bare) | `group_fp_sales_rep` (when in configurator CSV) — note: cross-module reference becomes `fusion_plating.group_fp_sales_rep` | +| `fusion_plating_invoicing.group_fp_accounting` | `fusion_plating.group_fp_manager` | +| `group_fp_accounting` (bare) | `fusion_plating.group_fp_manager` | +| `fusion_plating_receiving.group_fp_receiving` | `fusion_plating.group_fp_shop_manager_v2` | +| `group_fp_receiving` (bare) | `fusion_plating.group_fp_shop_manager_v2` | +| `fusion_plating_configurator.group_fp_shop_manager` | `fusion_plating.group_fp_manager` (old Shop Manager = Manager-equivalent) | + +- [ ] **Step 2: Smoke-check no orphaned references** + +```bash +grep -E '(group_fp_estimator|group_fp_accounting|group_fp_receiving|group_fp_shop_manager)\b' \ + fusion_plating_configurator/security/ir.model.access.csv \ + fusion_plating_invoicing/security/ir.model.access.csv \ + fusion_plating_receiving/security/ir.model.access.csv +``` +Expected: empty (the `_v2` suffix prevents matching the new Shop Manager xmlid). + +- [ ] **Step 3: Commit** + +```bash +git add fusion_plating_configurator/security/ir.model.access.csv \ + fusion_plating_invoicing/security/ir.model.access.csv \ + fusion_plating_receiving/security/ir.model.access.csv +git commit -m "refactor(plating-sec): sweep ACLs in configurator/invoicing/receiving" +``` + +### Task B4: Sweep ACL CSVs — verticals (Aerospace / Nuclear / Safety) + +**Files:** +- Modify: `fusion_plating_aerospace/security/ir.model.access.csv` +- Modify: `fusion_plating_nuclear/security/ir.model.access.csv` +- Modify: `fusion_plating_safety/security/ir.model.access.csv` + +- [ ] **Step 1: Apply same replacement table as B2** + +All three verticals only use core Operator/Supervisor/Manager today (per audit). No vertical-specific group references. + +- [ ] **Step 2: Smoke-check** + +```bash +grep -E 'group_fusion_plating_(operator|supervisor|manager|admin)' \ + fusion_plating_aerospace/security/ir.model.access.csv \ + fusion_plating_nuclear/security/ir.model.access.csv \ + fusion_plating_safety/security/ir.model.access.csv +``` +Expected: empty. + +- [ ] **Step 3: Commit** + +```bash +git add fusion_plating_aerospace/security/ir.model.access.csv \ + fusion_plating_nuclear/security/ir.model.access.csv \ + fusion_plating_safety/security/ir.model.access.csv +git commit -m "refactor(plating-sec): sweep vertical ACLs (aerospace/nuclear/safety)" +``` + +### Task B5: Sweep ACL CSVs — CGP (folds to QM) + +**Files:** +- Modify: `fusion_plating_cgp/security/ir.model.access.csv` + +- [ ] **Step 1: Apply replacement** + +Additional CGP-specific replacements: + +| Find | Replace with | +|---|---| +| `fusion_plating_cgp.group_fusion_plating_cgp_officer` | `fusion_plating.group_fp_quality_manager` | +| `fusion_plating_cgp.group_fusion_plating_cgp_designated_official` | `fusion_plating.group_fp_owner` | +| (the standard core replacements from B2) | (same) | + +- [ ] **Step 2: Smoke-check** + +```bash +grep -E '(group_fusion_plating_cgp_|group_fusion_plating_(operator|supervisor|manager|admin))' \ + fusion_plating_cgp/security/ir.model.access.csv +``` +Expected: empty. + +- [ ] **Step 3: Commit** + +```bash +git add fusion_plating_cgp/security/ir.model.access.csv +git commit -m "refactor(plating-sec): fold CGP ACLs into Quality Manager" +``` + +### Task B6: Sweep ACL CSV — quality (sweep only; QM/Manager split is Phase C) + +**Files:** +- Modify: `fusion_plating_quality/security/ir.model.access.csv` + +- [ ] **Step 1: Apply core replacement table from B2 only** + +DO NOT split Manager/QM yet — that's Phase C. For now, just mechanically replace old group names with new ones (every old Manager ref → new Manager). + +- [ ] **Step 2: Smoke-check** + +```bash +grep -E 'group_fusion_plating_(operator|supervisor|manager|admin)' \ + fusion_plating_quality/security/ir.model.access.csv +``` +Expected: empty. + +- [ ] **Step 3: Commit** + +```bash +git add fusion_plating_quality/security/ir.model.access.csv +git commit -m "refactor(plating-sec): sweep quality ACLs to new group names (pre-split)" +``` + +### Task B7: Deploy Phase B and re-run ACL tests + +- [ ] **Step 1: Deploy** + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && \ + su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin \ + -u fusion_plating,fusion_plating_configurator,fusion_plating_invoicing,fusion_plating_receiving,fusion_plating_quality,fusion_plating_certificates,fusion_plating_cgp,fusion_plating_aerospace,fusion_plating_nuclear,fusion_plating_safety,fusion_plating_jobs,fusion_plating_shopfloor \ + --stop-after-init\" && systemctl start odoo'" +``` + +- [ ] **Step 2: Run the ACL test suite** + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin --test-tags fp_perms --test-enable --stop-after-init -u fusion_plating\"'" 2>&1 | tail -50 +``` +Expected: most tests PASS. `test_manager_can_only_read_capa` and `test_qm_can_write_capa` may still FAIL — that's Phase C work. + +- [ ] **Step 3: If non-CAPA tests fail, troubleshoot before continuing** + +Check for typos in the CSV sweep, then re-deploy. + +--- + +## Phase C — Quality Split (Manager vs Quality Manager) + +Splits quality permissions per spec Section 2.C. Manager keeps reactive Quality (NCR, Hold, Check, Cert, RMA). QM gets exclusive control of CAPA, Audit, AVL, Customer Spec, Doc Control, FAIR/Nadcap signing. Adds two new `ir.rule` records. + +### Task C1: Write quality-split test cases + +**Files:** +- Create: `fusion_plating/tests/test_quality_split.py` + +- [ ] **Step 1: Write the test (~80 lines, ~10 test methods)** + +```python +from odoo.tests.common import TransactionCase, tagged +from odoo.exceptions import AccessError + + +@tagged('-at_install', 'post_install', 'fp_perms') +class TestQualitySplit(TransactionCase): + """Section 2.C of spec: Manager handles reactive Quality; + QM exclusively owns CAPA close, Audit, AVL, Customer Spec, FAIR/Nadcap signing.""" + + def setUp(self): + super().setUp() + Users = self.env['res.users'].with_context(no_reset_password=True) + self.u_mgr = Users.create({ + 'login': 'qsplit_mgr', 'name': 'QSplit Mgr', + 'email': 'qsplit_mgr@example.com', + 'groups_id': [(6, 0, [self.env.ref('fusion_plating.group_fp_manager').id])], + }) + self.u_qm = Users.create({ + 'login': 'qsplit_qm', 'name': 'QSplit QM', + 'email': 'qsplit_qm@example.com', + 'groups_id': [(6, 0, [self.env.ref('fusion_plating.group_fp_quality_manager').id])], + }) + + # CAPA: Manager read-only, QM full + def test_manager_can_read_capa(self): + self.env['fusion.plating.capa'].with_user(self.u_mgr).check_access_rights('read') + def test_manager_cannot_write_capa(self): + with self.assertRaises(AccessError): + self.env['fusion.plating.capa'].with_user(self.u_mgr).check_access_rights('write') + def test_manager_cannot_create_capa(self): + with self.assertRaises(AccessError): + self.env['fusion.plating.capa'].with_user(self.u_mgr).check_access_rights('create') + def test_qm_can_write_capa(self): + self.env['fusion.plating.capa'].with_user(self.u_qm).check_access_rights('write') + + # Audit: Manager read-only, QM full + def test_manager_can_read_audit(self): + self.env['fusion.plating.audit'].with_user(self.u_mgr).check_access_rights('read') + def test_manager_cannot_write_audit(self): + with self.assertRaises(AccessError): + self.env['fusion.plating.audit'].with_user(self.u_mgr).check_access_rights('write') + def test_qm_can_write_audit(self): + self.env['fusion.plating.audit'].with_user(self.u_qm).check_access_rights('write') + + # NCR: Manager full + def test_manager_can_create_ncr(self): + self.env['fusion.plating.ncr'].with_user(self.u_mgr).check_access_rights('create') + def test_manager_can_write_ncr(self): + self.env['fusion.plating.ncr'].with_user(self.u_mgr).check_access_rights('write') + + # Hold: Manager full + def test_manager_can_create_hold(self): + self.env['fusion.plating.quality.hold'].with_user(self.u_mgr).check_access_rights('create') + + # AVL: Manager read-only, QM full + def test_manager_can_read_avl(self): + self.env['fp.approved.vendor.list'].with_user(self.u_mgr).check_access_rights('read') + def test_manager_cannot_write_avl(self): + with self.assertRaises(AccessError): + self.env['fp.approved.vendor.list'].with_user(self.u_mgr).check_access_rights('write') + + # Customer Spec: Manager read-only, QM full + def test_manager_can_read_customer_spec(self): + self.env['fusion.plating.customer.spec'].with_user(self.u_mgr).check_access_rights('read') + def test_manager_cannot_write_customer_spec(self): + with self.assertRaises(AccessError): + self.env['fusion.plating.customer.spec'].with_user(self.u_mgr).check_access_rights('write') +``` + +- [ ] **Step 2: Run — expect failures** + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin --test-tags fp_perms --test-enable --stop-after-init -u fusion_plating_quality\"'" 2>&1 | grep -E '(FAIL|PASS|ERROR)' | head -20 +``` +Expected: ~6 FAIL (the QM-only ones); the rest PASS. + +- [ ] **Step 3: Commit** + +```bash +git add fusion_plating/tests/test_quality_split.py +git commit -m "test(plating-quality): assert Manager/QM split on CAPA/Audit/AVL/Spec" +``` + +### Task C2: Split CAPA ACL — Manager read-only, QM full + +**Files:** +- Modify: `fusion_plating_quality/security/ir.model.access.csv` + +- [ ] **Step 1: Find existing CAPA row(s)** + +Use Grep to find lines containing `fusion.plating.capa`. There should be one Operator/Tech row + one Manager row OR a single row. Identify them by their xmlid in column 1 (e.g., `access_fusion_plating_capa_manager`). + +- [ ] **Step 2: Modify the Manager row to read-only (1,0,0,0) and add a QM full-access row (1,1,1,1)** + +Example structure (adjust IDs to match the existing file): + +```csv +access_fusion_plating_capa_manager,fusion.plating.capa.manager,model_fusion_plating_capa,fusion_plating.group_fp_manager,1,0,0,0 +access_fusion_plating_capa_qm,fusion.plating.capa.qm,model_fusion_plating_capa,fusion_plating.group_fp_quality_manager,1,1,1,1 +``` + +- [ ] **Step 3: Repeat for Audit, AVL, Customer Spec, Doc Control** + +Same pattern: Manager row → read-only (`1,0,0,0`), add new QM-full row (`1,1,1,1`). Use `access__qm` as the xmlid suffix for the new rows. + +- [ ] **Step 4: Deploy + run tests** + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && \ + su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin \ + -u fusion_plating_quality --test-tags fp_perms --test-enable --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -20 +``` +Expected: all quality-split tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add fusion_plating_quality/security/ir.model.access.csv +git commit -m "feat(plating-quality): split CAPA/Audit/AVL/Spec ACLs — Manager read, QM full" +``` + +### Task C3: Add FAIR/Nadcap ir.rule on certificates + +**Files:** +- Modify or Create: `fusion_plating_certificates/security/fp_cert_security.xml` +- Modify: `fusion_plating_certificates/__manifest__.py` (add the file to `data` if newly created) + +- [ ] **Step 1: Write the ir.rule** + +```xml + + + + + FP Certificate: FAIR/Nadcap write restricted to QM + + + [ + '|', + ('cert_type', 'not in', ('fair', 'nadcap')), + ('id', 'in', user.partner_id.commercial_partner_id.id and []), + ] + + + + + + + + + + FP Certificate: QM sees all + + [(1, '=', 1)] + + + + + + + + +``` + +NOTE: the trick `('id', 'in', user.partner_id.commercial_partner_id.id and [])` returns an empty list, which means "no records match" — combined with the OR clause it creates "for Managers, ONLY allow records where cert_type is NOT in (fair, nadcap)". The QM rule then re-grants full access for QM via the second rule (ir.rules are OR'd within a group when multiple match). + +- [ ] **Step 2: Verify XML parses, deploy, hand-test** + +```bash +python -c "import lxml.etree as et; et.parse('fusion_plating_certificates/security/fp_cert_security.xml'); print('OK')" +ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && \ + su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin \ + -u fusion_plating_certificates --stop-after-init\" && systemctl start odoo'" +``` + +- [ ] **Step 3: Add to manifest if newly created** + +If the file is new, add `'security/fp_cert_security.xml'` to the `data` list in `fusion_plating_certificates/__manifest__.py` AFTER `'security/ir.model.access.csv'`. + +- [ ] **Step 4: Commit** + +```bash +git add fusion_plating_certificates/security/fp_cert_security.xml \ + fusion_plating_certificates/__manifest__.py +git commit -m "feat(plating-cert): restrict FAIR/Nadcap cert writes to Quality Manager" +``` + +--- + +## Phase D — Menu / Submenu / Field Visibility (3-Layer Hide) + +Adds explicit `groups=` to every top-level menu, submenu, and field/button per spec Section 2.E. No reliance on action-level ACLs for visibility. + +### Task D1: Write menu-visibility tests + +**Files:** +- Create: `fusion_plating/tests/test_menu_visibility.py` + +- [ ] **Step 1: Write the test** + +```python +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, [])], + }) + self.u_no = mk('no', None) + 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 + return menu.with_user(user)._filter_visible_menus() if hasattr( + menu, '_filter_visible_menus') else bool( + self.env['ir.ui.menu'].with_user(user).search([('id', '=', menu.id)])) + + def test_no_sees_no_plating_root(self): + self.assertFalse(self._visible(self.u_no, 'fusion_plating.menu_fp_root')) + + def test_technician_sees_shop_floor(self): + self.assertTrue(self._visible(self.u_tech, 'fusion_plating_shopfloor.menu_fp_shopfloor_root')) + + def test_technician_does_not_see_sales(self): + self.assertFalse(self._visible(self.u_tech, 'fusion_plating_configurator.menu_fp_sales_root')) + + def test_technician_does_not_see_team(self): + self.assertFalse(self._visible(self.u_tech, 'fusion_plating.menu_fp_team')) + + def test_sales_rep_sees_sales(self): + self.assertTrue(self._visible(self.u_sr, 'fusion_plating_configurator.menu_fp_sales_root')) + + def test_sales_rep_does_not_see_shop_floor(self): + self.assertFalse(self._visible(self.u_sr, 'fusion_plating_shopfloor.menu_fp_shopfloor_root')) + + def test_manager_sees_quality(self): + self.assertTrue(self._visible(self.u_mgr, 'fusion_plating_quality.menu_fp_quality')) + + def test_manager_does_not_see_compliance(self): + self.assertFalse(self._visible(self.u_mgr, 'fusion_plating_compliance.menu_fp_compliance_hub')) + + def test_manager_does_not_see_audits_submenu(self): + # Manager sees Quality, but NOT the Audits child + self.assertFalse(self._visible(self.u_mgr, 'fusion_plating_quality.menu_fp_audits')) + + def test_qm_sees_compliance(self): + self.assertTrue(self._visible(self.u_qm, 'fusion_plating_compliance.menu_fp_compliance_hub')) + + def test_qm_sees_audits_submenu(self): + self.assertTrue(self._visible(self.u_qm, 'fusion_plating_quality.menu_fp_audits')) + + def test_owner_sees_team(self): + self.assertTrue(self._visible(self.u_owner, 'fusion_plating.menu_fp_team')) +``` + +- [ ] **Step 2: Run — expect failures** + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin --test-tags fp_perms --test-enable --stop-after-init -u fusion_plating\"'" 2>&1 | grep -E '(FAIL|test_)' +``` +Expected: several FAIL until D2-D6 done. The `menu_fp_team` test will ERROR (menu doesn't exist yet — Phase F creates it). + +- [ ] **Step 3: Commit** + +```bash +git add fusion_plating/tests/test_menu_visibility.py +git commit -m "test(plating-menu): per-role menu visibility matrix" +``` + +### Task D2: Layer 1 — top-level menu groups + +**Files:** +- Modify: `fusion_plating/views/fp_menu.xml` +- Modify: `fusion_plating_configurator/views/fp_configurator_menu.xml` +- Modify: `fusion_plating_shopfloor/views/fp_shopfloor_menu.xml` +- Modify: `fusion_plating_receiving/views/fp_receiving_menu.xml` +- Modify: `fusion_plating_quality/views/fp_menu.xml` +- Modify: `fusion_plating_compliance/views/fp_menu.xml` +- Modify: `fusion_plating_kpi/views/fp_kpi_menu.xml` + +- [ ] **Step 1: Set explicit `groups=` per spec Section 2.E table** + +For each top-level menu, edit the `` element to add or update the `groups=` attribute: + +| Menu xmlid | `groups=` value | +|---|---| +| `fusion_plating.menu_fp_root` | `"fusion_plating.group_fp_technician,fusion_plating.group_fp_sales_rep"` (covers everyone via implication) | +| `fusion_plating_configurator.menu_fp_sales_root` | `"fusion_plating.group_fp_sales_rep"` | +| `fusion_plating_shopfloor.menu_fp_shopfloor_root` | `"fusion_plating.group_fp_technician"` | +| `fusion_plating.menu_fp_operations` | `"fusion_plating.group_fp_technician"` | +| `fusion_plating_receiving.menu_fp_receiving_root` | `"fusion_plating.group_fp_shop_manager_v2"` | +| `fusion_plating_quality.menu_fp_quality` | `"fusion_plating.group_fp_manager"` | +| `fusion_plating_compliance.menu_fp_compliance_hub` | `"fusion_plating.group_fp_quality_manager"` | +| `fusion_plating_kpi.menu_fp_kpi` (or equivalent) | `"fusion_plating.group_fp_manager"` | +| `fusion_plating.menu_fp_config` | `"fusion_plating.group_fp_manager"` | + +- [ ] **Step 2: Commit** + +```bash +git add fusion_plating/views/fp_menu.xml \ + fusion_plating_configurator/views/fp_configurator_menu.xml \ + fusion_plating_shopfloor/views/fp_shopfloor_menu.xml \ + fusion_plating_receiving/views/fp_receiving_menu.xml \ + fusion_plating_quality/views/fp_menu.xml \ + fusion_plating_compliance/views/fp_menu.xml \ + fusion_plating_kpi/views/fp_kpi_menu.xml +git commit -m "feat(plating-menu): Layer 1 — explicit groups on top-level menus" +``` + +### Task D3: Layer 2 — submenu groups (quality and compliance hub) + +**Files:** +- Modify: `fusion_plating_quality/views/fp_menu.xml` +- Modify: `fusion_plating_compliance/views/fp_menu.xml` +- Modify: `fusion_plating_compliance_on/views/fp_menu.xml` (if exists) +- Modify: `fusion_plating_aerospace/views/fp_menu.xml` (if exists) +- Modify: `fusion_plating_nuclear/views/fp_menu.xml` (if exists) +- Modify: `fusion_plating_safety/views/fp_menu.xml` (if exists) + +- [ ] **Step 1: Add `groups=` to submenus that need different visibility from parent** + +In `fusion_plating_quality/views/fp_menu.xml`, for the Audits / Customer Specs / Approved Vendor List submenus, set `groups="fusion_plating.group_fp_quality_manager"`. Leave other quality submenus to inherit (Manager+). + +In `fusion_plating_compliance/views/fp_menu.xml`, set every child submenu of `menu_fp_compliance_hub` to `groups="fusion_plating.group_fp_quality_manager"`. Same for each vertical's submenu under the hub. + +- [ ] **Step 2: Commit** + +```bash +git add fusion_plating_quality/views/fp_menu.xml \ + fusion_plating_compliance/views/fp_menu.xml \ + fusion_plating_compliance_on/views/fp_menu.xml \ + fusion_plating_aerospace/views/fp_menu.xml \ + fusion_plating_nuclear/views/fp_menu.xml \ + fusion_plating_safety/views/fp_menu.xml +git commit -m "feat(plating-menu): Layer 2 — QM-only submenus under Quality and Compliance" +``` + +### Task D4: Layer 2 — Operations submenu split (Tech vs Shop Mgr) + +**Files:** +- Modify: `fusion_plating/views/fp_menu.xml` (or wherever the Operations submenus are defined) + +- [ ] **Step 1: Set submenu groups per spec table** + +| Submenu xmlid | `groups=` | +|---|---| +| `menu_fp_maintenance` (under Operations) | `"fusion_plating.group_fp_shop_manager_v2"` | +| `menu_fp_job_step_move` (Move Log) | `"fusion_plating.group_fp_shop_manager_v2"` | +| `menu_fp_job_step_timelog` (Labor History) | `"fusion_plating.group_fp_shop_manager_v2"` | +| `menu_fp_replenishment_suggestions` | `"fusion_plating.group_fp_manager"` | + +(Leave Process Recipes, Baths, Chemistry Logs, Tanks, Racks visible to Technician via parent.) + +- [ ] **Step 2: Commit** + +```bash +git add fusion_plating/views/fp_menu.xml +git commit -m "feat(plating-menu): Layer 2 — shop-leadership submenus under Operations" +``` + +### Task D5: Layer 3 — field/button visibility + +**Files:** +- Modify: `fusion_plating_configurator/views/sale_order_views.xml` +- Modify: `fusion_plating_invoicing/views/res_partner_views.xml` +- Modify: `fusion_plating_quality/views/fp_capa_views.xml` +- Modify: `fusion_plating_quality/views/fp_audit_views.xml` +- Modify: `fusion_plating_quality/views/fp_avl_views.xml` +- Modify: `fusion_plating_quality/views/fp_customer_spec_views.xml` +- Modify: `fusion_plating_certificates/views/fp_certificate_views.xml` +- Modify: `fusion_plating_cgp/views/*_views.xml` + +- [ ] **Step 1: Add `groups=` to fields/buttons per spec Section 2.E Layer 3 table** + +Specific changes: + +| View | Element | `groups=` | +|---|---|---| +| sale_order_views.xml | `