# 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 | `