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 `