Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-23-permissions-overhaul-design.md
gsinghpal 560ffa2cdf docs(plating): permissions overhaul Phase 1 — spec + implementation plan
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) <noreply@anthropic.com>
2026-05-24 00:43:00 -04:00

44 KiB

Fusion Plating — Permissions Overhaul (Phase 1)

Date: 2026-05-23 Status: Approved for implementation Owner module: fusion_plating (with co-changes in 9 dependent modules) Brainstorm transcript: session with @gsinghpal, 2026-05-23 Linked plan: TBD (writing-plans skill, next step)


Problem Statement

The current Fusion Plating permission system has 12 res.groups defined across 6 modules. An audit (2026-05-23) found:

  • 3 groups are zero-reference orphans — Shop Manager, CGP Designated Official, Plating Legacy Menus
  • 1 group is functionally orphaned — Administrator (the 2 Python checks that reference it use a typo'd XML ID _administrator instead of _admin, so the gate never fires)
  • The role dropdown in the user form lists 10 entries with confusing ordering (sequence ties at 50 and 60 cause Estimator/CGP Officer and Shop Manager/CGP DO to render in arbitrary alphabetical order)
  • Default landing page is hardcoded to "Shop view" for everyone — Managers complain about being dumped into a Workstation tablet when they open the Plating app
  • The landing-page picklist in user preferences offers only 3 options (Quotations, Sale Orders, Process Recipes) — missing Manager Desk, Plant View, Quality Dashboard
  • Menu visibility relies on a mix of explicit groups= attributes and implicit action-level ACLs — fragile and inconsistent

This Phase 1 work consolidates the 12 groups into 8 well-defined roles, fixes the landing-page UX with role-based defaults, and ships an Owner-only "Team" page for clean role assignment.


Locked Decisions

# Question Decision
Q1 Quality Manager vs Manager — what quality permissions split? Option B — Manager handles reactive Quality (NCR/Hold/Check/routine Cert/RMA). Quality Manager owns strategic Quality (CAPA closure, audit sign-off, FAIR/Nadcap signing, AVL approval, Customer Spec library, Doc Control approval, all CGP).
Q2 CGP/Aerospace/Nuclear verticals — fold or keep as add-on flags? Option A — All vertical ACLs gate on Quality Manager. CGP Officer group dropped (folds into QM). CGP Designated Official becomes res.company.x_fc_cgp_designated_official_id field (Many2one to res.users, domain [Owner, QM]). Aerospace/Nuclear/Safety unchanged (already on Manager backbone).
Q3 Landing page per role — hardcoded, configurable, or seeded? Option B — Hardcoded role→action mapping in the resolver. Per-user override stays in preferences. Company default stays as a final fallback.
Q4 Owner-only Permissions config page — yes/no, and what does it configure? Yes, Interpretation A only — Owner-only "Team" page for role assignment, designated officials, and audit log. NO permission-definition editing (Interpretation B explicitly killed — would defeat the 8-role spec).
Q5 Migration of existing users — auto-map or force manual? Option B — Dry-run preview + Owner approval. Auto-map runs on -u, creates a fp.migration.preview in pending state, schedules a mail.activity on every Owner. Migration only applies after Owner clicks "Approve & Run". 30-day rollback window via archived old groups.
Q4b Menu/submenu/field visibility — explicit groups= or inherit from parent? Confirmed by user pre-spec-write — All three layers (top-level menus, submenus, fields/buttons) get explicit groups= matching the new roles. No reliance on action-level ACLs for menu visibility.

Section 1 — Role Hierarchy & XML IDs

The 8 new roles

All under the existing fusion_plating.res_groups_privilege_fusion_plating privilege block (same place in the user form). Sequence numbers picked uniquely to avoid the current audit's "tied at 50/60" rendering bug.

Seq Display Name XML ID Implies Auto-assigned to
10 Technician fusion_plating.group_fp_technician base.group_user
20 Sales Representative fusion_plating.group_fp_sales_rep base.group_user
30 Shop Manager fusion_plating.group_fp_shop_manager_v2 Technician
40 Sales Manager fusion_plating.group_fp_sales_manager Sales Representative
50 Manager fusion_plating.group_fp_manager Shop Manager + Sales Manager
60 Quality Manager fusion_plating.group_fp_quality_manager Manager
70 Owner fusion_plating.group_fp_owner Quality Manager + base.group_system uid 1, uid 2
(No) — implicit (no Fusion Plating group held)

Design notes

  1. group_fp_shop_manager_v2 suffix — the existing fusion_plating_configurator.group_fp_shop_manager (today's 0-ref label bundle) gets retired. Suffix _v2 avoids xmlid collision during migration; we rename to _shop_manager in a follow-up housekeeping pass once old refs are confirmed dead.

  2. Sales branch and Shop branch are parallel — both inherit only base.group_user. A Technician can't see Quotations; a Sales Rep can't see the Workstation. They cross-join at Manager.

  3. Diamond at Manager — Manager implies BOTH Shop Manager AND Sales Manager; gets the union.

  4. "No" is the absence of any group — no res.groups record needed. The Plating menu root gates on an OR of all 7 plating roles. An internal user with none sees no plating menu.

  5. Owner implies base.group_system — replaces today's broken Administrator pattern. Owners get Settings access, can install modules, etc.

  6. Old groups stay defined post-migration but become "DEPRECATED" + auto-archived. 30-day rollback window. _cron_purge_expired_migrations deletes them after 30 days.

  7. CGP Designated Official is no longer a groupres.company.x_fc_cgp_designated_official_id (Many2one to res.users, domain [('groups_id', 'in', [QM_id, Owner_id])]).

  8. Field Technician (fusion_tasks.group_field_technician) is untouched — orthogonal to plating roles, separate privilege block.

Hierarchy visual

base.group_user (Internal User)
├── (no plating group)         = "No"
├── Technician [10]
│   └── Shop Manager v2 [30]
│       └── Manager [50] ←──┐  (diamond)
│                            │
└── Sales Representative [20]│
    └── Sales Manager [40]   │
        └── Manager [50] ←───┘
            └── Quality Manager [60]
                └── Owner [70]   (also implies base.group_system)

Section 2 — ACL Re-gating Plan

2.A Standard mapping pattern

Applies to ~80% of the ~475 ACL refs mechanically:

Old gate New gate Why
group_fusion_plating_operator group_fp_technician Pure rename
group_fusion_plating_supervisor group_fp_shop_manager_v2 Supervisor's daily-floor leadership IS Shop Manager's job
group_fusion_plating_manager group_fp_manager Pure rename
group_fp_estimator group_fp_sales_rep Pure rename, but lose order-confirm (Section 2.B)
group_fp_receiving group_fp_shop_manager_v2 Receiving folds in
group_fp_accounting group_fp_manager Accounting folds in
group_fusion_plating_admin group_fp_owner Pure rename + fixes _administrator typo bug

Implied-chain handles the rest (e.g., Manager auto-gets everything Shop Manager has, so a model gated on Shop Manager is automatically accessible to Manager+).

2.B New gates ADDED

Action Today New gate
sale.order.action_confirm Any internal user group_fp_sales_manager
sale.order set x_fc_account_hold_override Manager (with _administrator typo) group_fp_manager (clean)
account.move.action_post for FP-invoiced SOs Implicit group_fp_manager
Owner-only Team page menu Doesn't exist group_fp_owner

Sales Reps can still save Sale Orders in draft; the confirm button is hidden in the view and the model-level gate raises UserError if called directly: "Only Sales Manager or higher can confirm orders."

2.C Quality split — Manager vs Quality Manager

Model Manager rights QM-only rights
fusion.plating.ncr CRUD + state transitions through closed
fusion.plating.capa Read + comment only CRUD + action_close + effectiveness verification
fusion.plating.quality.hold CRUD + release
fusion.plating.quality.check CRUD + pass/fail
fp.certificate (routine CoC, thickness) CRUD + sign + issue + send
fp.certificate where cert_type='fair' Read + create Sign + issue (record rule on cert_type)
fp.certificate where cert_type='nadcap' Read + create Sign + issue (record rule on cert_type)
fusion.plating.rma CRUD + authorise + resolve
fusion.plating.audit Read CRUD + close
fusion.plating.customer.spec Read + attach to parts CRUD (library curator)
fp.approved.vendor.list Read Add / approve / disqualify
fp.contract.review (QA-005) Complete reviews assigned to them Set QA Manager roster + override gates
Doc Control + Doc Approval Read + request approval Approve / supersede / retire
Calibration equipment Log events + view schedule Configure equipment + set intervals + dispose out-of-tolerance
All fp.cgp.* models (8 ACLs + 2 ir.rules) None All (entire CGP fold-in lands here)

Implementation note: FAIR/Nadcap cert split uses an ir.rule on fp.certificate (domain [('cert_type', 'in', ['fair','nadcap'])]) restricted to QM for write. Routine CoCs (cert_type = coc or thickness_report) stay open to Manager.

2.D Verticals (Aerospace / Nuclear / Safety)

No change. Their ACLs already gate on group_fusion_plating_manager (now group_fp_manager). The standard mapping in 2.A covers them. No new vertical-specific gates needed.

2.E Three-layer menu / submenu / field hiding policy

Rule: if a user can't use it, they don't see it. No reliance on action-level ACLs for visibility — explicit groups= at every layer.

Layer 1 — Top-level menus

Top-level menu groups=
Plating (root) OR of all 7 plating roles
Sales & Quoting group_fp_sales_rep
Shop Floor group_fp_technician
Operations group_fp_technician
Receiving & Shipping group_fp_shop_manager_v2
Quality group_fp_manager
Compliance (hub) group_fp_quality_manager
KPIs group_fp_manager
Configuration group_fp_manager

Layer 2 — Submenus (explicit on every child)

Submenu New gate
Quality > Audits group_fp_quality_manager
Quality > Customer Specs group_fp_quality_manager
Quality > Approved Vendor List group_fp_quality_manager
Quality > NCRs / Holds / Checks / RMAs / Certs group_fp_manager
Quality > CAPAs group_fp_manager (visibility); QM-only for close button (Layer 3)
Operations > Maintenance group_fp_shop_manager_v2
Operations > Move Log group_fp_shop_manager_v2
Operations > Labor History group_fp_shop_manager_v2
Operations > Replenishment Suggestions group_fp_manager
Configuration > Team group_fp_owner
Configuration > Settings group_fp_manager (explicit)
Configuration > all 7 themed folders group_fp_manager (explicit)
Sales & Quoting > Configurator group_fp_sales_rep
Sales & Quoting > Sale Orders group_fp_sales_rep (visibility); SM+ for confirm (Layer 3)
Receiving & Shipping > all children group_fp_shop_manager_v2
Compliance > CGP group_fp_quality_manager
Compliance > General / Safety / Aerospace / Nuclear group_fp_quality_manager

Layer 3 — Fields, buttons, smart buttons

View element New gate
sale.order view — Confirm button group_fp_sales_manager
sale.order view — x_fc_account_hold_override group_fp_manager (was broken Administrator typo)
sale.order form — pricing columns on lines group_fp_sales_rep (defense in depth — Technician/Shop Manager don't see pricing)
fp.certificate form — Sign button (FAIR / Nadcap) group_fp_quality_manager
fusion.plating.capa form — Close button + edit fields group_fp_quality_manager
fusion.plating.audit form — all buttons group_fp_quality_manager
fp.approved.vendor.list form — Approve / Disqualify group_fp_quality_manager
fusion.plating.customer.spec form — edit fields group_fp_quality_manager
All CGP form buttons group_fp_quality_manager
Smart buttons (cross-record navigation) Match the underlying action's visibility

2.F Per-role menu visibility matrix (sanity check)

Menu No Tech SR SM SalesMgr Mgr QM Owner
Plating (root)
Sales & Quoting
Shop Floor
Operations
Receiving & Shipping
Quality
Compliance
KPIs
Configuration
Configuration > Team

(SR = Sales Rep, SM = Shop Manager, SalesMgr = Sales Manager, Mgr = Manager)

2.G Manager-bypass context flags (no ownership change)

All 9 existing bypass flags from the battle tests remain gated on Manager+:

fp_skip_step_gate, fp_skip_qc_gate, fp_skip_qty_reconcile, fp_skip_bake_gate, fp_skip_predecessor_check, fp_skip_missed_window, fp_skip_required_inputs_gate, fp_skip_signoff_gate, fp_skip_transition_form.

New name in the check: user.has_group('fusion_plating.group_fp_manager').

Shop Manager CANNOT bypass these gates — matches spec ("Technicians cannot override system"). Override authority sits at Manager.

2.H ir.rules (record rules)

Rule Old New
fp.cgp.psa Officer-only CGP Officer group_fp_quality_manager
fp.cgp.security.incident Officer-only CGP Officer group_fp_quality_manager
fusion.technician.task Field-Tech-own Field Technician Unchanged (orthogonal)
NEW: fp.certificate write-gate for cert_type in ('fair','nadcap') group_fp_quality_manager
NEW: sale.order write-gate for state→'sale' transition group_fp_sales_manager

No multi-company changes — existing multi-company rules untouched.


Section 3 — Landing Resolver

Resolver flow (server action action_fp_resolve_plating_landing)

def _fp_resolve_landing(self):
    user = self.env.user
    company = self.env.company

    # 1. Per-user override (set in preferences)
    if user.x_fc_plating_landing_action_id:
        return user.x_fc_plating_landing_action_id._render_action()

    # 2. Role-based default (precedence: highest role wins)
    role_landing = self._fp_role_default_landing(user, company)
    if role_landing:
        return role_landing._render_action()

    # 3. Company default (admin fallback)
    if company.x_fc_default_landing_action_id:
        return company.x_fc_default_landing_action_id._render_action()

    # 4. Hardcoded last-ditch
    return self.env.ref('fusion_plating_configurator.action_fp_sale_orders')._render_action()

Role → action mapping (Step 2)

def _fp_role_default_landing(self, user, company):
    workstation_action = self._fp_workstation_action_for_layout(company)

    if user.has_group('fusion_plating.group_fp_owner'):
        return self.env.ref('fusion_plating_shopfloor.action_fp_manager_dashboard',
                            raise_if_not_found=False)
    if user.has_group('fusion_plating.group_fp_quality_manager'):
        return self.env.ref('fusion_plating_quality.action_fp_quality_dashboard',
                            raise_if_not_found=False)
    if user.has_group('fusion_plating.group_fp_manager'):
        return self.env.ref('fusion_plating_shopfloor.action_fp_manager_dashboard',
                            raise_if_not_found=False)
    if user.has_group('fusion_plating.group_fp_sales_manager'):
        return self.env.ref('fusion_plating_configurator.action_fp_sale_orders',
                            raise_if_not_found=False)
    if user.has_group('fusion_plating.group_fp_shop_manager_v2'):
        return workstation_action
    if user.has_group('fusion_plating.group_fp_sales_rep'):
        return self.env.ref('fusion_plating_configurator.action_fp_quotations',
                            raise_if_not_found=False)
    if user.has_group('fusion_plating.group_fp_technician'):
        return workstation_action
    return False

Workstation = layout-flag aware (single source of truth)

def _fp_workstation_action_for_layout(self, company):
    """Single source of truth: which Shop Floor surface is active on this DB?"""
    param = self.env['ir.config_parameter'].sudo().get_param(
        'fusion_plating_shopfloor.layout', 'v2')
    if param == 'v2':
        return self.env.ref('fusion_plating_shopfloor.action_fp_plant_kanban',
                            raise_if_not_found=False)
    return self.env.ref('fusion_plating_shopfloor.action_fp_shopfloor_landing',
                        raise_if_not_found=False)

Flipping ir.config_parameter['fusion_plating_shopfloor.layout'] instantly changes the default landing for every Technician and Shop Manager on the next page load.

Pickable actions (x_fc_pickable_landing=True)

Adding 4 net-new (3 are already pickable). Total picklist = 7 entries.

Action XML ID Display in dropdown Default for
fusion_plating_shopfloor.action_fp_manager_dashboard Manager Desk Owner / QM / Manager
fusion_plating_shopfloor.action_fp_plant_kanban Plant View Kanban Shop Mgr / Tech (v2 layout)
fusion_plating_shopfloor.action_fp_shopfloor_landing Workstation (Legacy) Shop Mgr / Tech (legacy layout)
fusion_plating_quality.action_fp_quality_dashboard Quality Dashboard QM
fusion_plating_configurator.action_fp_quotations Quotations Sales Rep (already pickable)
fusion_plating_configurator.action_fp_sale_orders Sale Orders Sales Manager (already pickable)
fusion_plating.action_fp_process_recipe Process Recipes (niche option, already pickable)

Per-user override picklist domain

Today: [('x_fc_pickable_landing', '=', True)]. Tightened: also filter by user's accessible actions, so a Technician can't pick "Manager Desk" as their landing if they can't see it.

Domain becomes computed: [('x_fc_pickable_landing', '=', True), ('id', 'in', user_accessible_action_ids)]. The user_accessible_action_ids list comes from a compute that runs env['ir.ui.menu']._visible_menu_ids() mapped to action IDs.

Edge cases

  1. Multi-role user (Manager promoted to QM): precedence chain picks higher role. Deterministic.
  2. User in "No" state opening resolver directly: falls through to company default → hardcoded Sale Orders → standard Odoo home.
  3. xmlref deleted or module uninstalled: raise_if_not_found=False returns False, resolver falls through.
  4. First-login user with no preference / company default / roles: lands on Sale Orders.
  5. Demo / fresh DB: Sale Orders fallback works without any FP modules beyond core.

Section 4 — Owner-only Team Page

Implementation — standard Odoo views, not custom OWL

Single new field on res.users + standard kanban/form views. Zero custom JS.

# fusion_plating/models/res_users.py
class ResUsers(models.Model):
    _inherit = 'res.users'

    x_fc_plating_role = fields.Selection([
        ('no',              'No'),
        ('technician',      'Technician'),
        ('sales_rep',       'Sales Representative'),
        ('shop_manager',    'Shop Manager'),
        ('sales_manager',   'Sales Manager'),
        ('manager',         'Manager'),
        ('quality_manager', 'Quality Manager'),
        ('owner',           'Owner'),
    ], compute='_compute_plating_role',
       inverse='_inverse_plating_role',
       store=True,
       string='Fusion Plating Role')
  • Compute reads groups_id, returns the highest-precedence plating role
  • Inverse clears all plating groups + writes only the chosen one + posts a Markup() chatter audit
  • Stored so kanban default_group_by="x_fc_plating_role" and drag-and-drop work

Menu placement

Plating
└── Configuration                    (Manager+)
    └── ⚡ Settings                  (existing)
    └── 👥 Team                      (NEW — Owner-only)
        └── (opens action_fp_team)

XML ID: fusion_plating.menu_fp_team, groups="fusion_plating.group_fp_owner".

4 tabs

Tab View type Domain What it does
Active Team Kanban grouped by x_fc_plating_role [('share','=',False), ('active','=',True)] 8 columns; drag-and-drop role changes; click card → user form
Designated Officials Form on res.company CGP DO + Nadcap Authority Many2one fields
Role Reference QWeb static template 8 cards with plain-English "can / cannot" per role
Audit Log List on mail.message [('model','=','res.users'), ('subtype_id','=',mt_note), ('body','ilike','plating role')] 90-day role-change history

All 4 tabs are separate ir.actions.act_window records reached via a tabbed notebook. Each has its own xmlid for direct linking.

Active Team kanban — card layout

┌─────────────────────────────────┐
│  [avatar]  Jane Doe             │
│            jdoe@enplating.com   │
│            Last seen: 2h ago    │
│  ─────────────────────────────  │
│  ⭐ CGP DO                       │ (only if user.id == company.x_fc_cgp_designated_official_id)
│  🏆 Nadcap Authority            │ (only if user.id == company.x_fc_nadcap_authority_user_id)
│  ─────────────────────────────  │
│  Created: 2025-03-14            │
│  Last role change: 2026-05-01   │
└─────────────────────────────────┘

Columns (left-to-right by sequence): No · Technician · Sales Rep · Shop Manager · Sales Manager · Manager · QM · Owner

Folded by default: No, Sales Rep (less common in plating shops). Owner can unfold.

Search: name / email / department. Filters: Active (default), With Archived, Has Login Last 30 Days, Has Never Logged In.

Designated Officials tab — single form

Form on res.company with two fields:

  • x_fc_cgp_designated_official_id (Many2one res.users, domain [QM, Owner])
  • x_fc_nadcap_authority_user_id (Many2one res.users, domain [QM, Owner])

Save posts to res.company chatter for auditability:

"CGP Designated Official changed: Jane Doe → John Smith by owner@enplating.com on 2026-05-23."

Role Reference tab — auto-generated

Single source of truth in fusion_plating/models/res_users.py:

PLATING_ROLE_DESCRIPTIONS = {
    'technician': {
        'icon': 'fa-wrench',
        'tagline': 'Runs the shop floor.',
        'can': [
            'See and operate the Workstation tablet',
            'Start/finish/pause job steps',
            'Capture quality checks and step inputs',
            'Issue routine Certificates of Conformance',
            'Log scrap and bake events',
        ],
        'cannot': [
            'See pricing, quotations, or sales orders',
            'Edit recipes or process configurations',
            'Override system gates (predecessor lock, signoff, bake window, etc.)',
            'Approve CAPAs or sign FAIR/Nadcap certs',
        ],
    },
    # ... 7 more entries
}

QWeb tab renders cards from this dict. Same dict used by the spec doc generator and by future onboarding wizards.

What the page does NOT do

  • No editing individual permissions (Q4 Interpretation B — killed)
  • No custom role definitions
  • No per-user exception flags
  • No role hand-off workflows (transfer Bob's open jobs to Alice) — Phase 3
  • No bulk import of roles — defer until 100+ employee shops

Phase 2 hooks (designed-in, not built)

The 4-tab structure leaves room for:

  • Audit Dashboard tab — read-only ACL matrix for CGP/Nadcap audit prep
  • Departure Handoff tab — wizard for terminating users

Section 5 — Migration Workflow

Models

# fusion_plating/models/fp_migration.py

class FpMigrationPreview(models.Model):
    _name = 'fp.migration.preview'
    _description = 'Fusion Plating Role Migration Preview'
    _order = 'create_date desc'

    name              = fields.Char(default=lambda s: _('Migration %s') % fields.Datetime.now())
    state             = fields.Selection([
        ('pending',    'Pending Review'),
        ('approved',   'Approved & Applied'),
        ('cancelled',  'Cancelled'),
        ('rolled_back','Rolled Back'),
    ], default='pending', tracking=True)
    line_ids          = fields.One2many('fp.migration.preview.line', 'preview_id')
    user_count        = fields.Integer(compute='_compute_counts', store=True)
    warning_count     = fields.Integer(compute='_compute_counts', store=True)
    approved_by_id    = fields.Many2one('res.users', readonly=True)
    approved_at       = fields.Datetime(readonly=True)
    rollback_deadline = fields.Datetime(compute='_compute_rollback_deadline')

class FpMigrationPreviewLine(models.Model):
    _name = 'fp.migration.preview.line'
    _description = 'Migration Preview Line'

    preview_id              = fields.Many2one('fp.migration.preview', required=True, ondelete='cascade')
    user_id                 = fields.Many2one('res.users', required=True)
    current_groups          = fields.Char(compute='_compute_current_groups')
    proposed_role           = fields.Selection(_FP_ROLE_SELECTION)
    capability_delta        = fields.Char()
    warning                 = fields.Boolean()
    notes                   = fields.Text()
    applied_groups_snapshot = fields.Text()  # JSON of pre-migration groups_id for rollback

Trigger — runs ONCE on -u, enters pending

# fusion_plating/__manifest__.py
'post_init_hook': '_fp_post_init_role_migration',

# fusion_plating/__init__.py
def _fp_post_init_role_migration(env):
    """Idempotent: only creates a preview if one isn't already pending."""
    pending = env['fp.migration.preview'].search([('state','=','pending')], limit=1)
    if pending:
        return
    completed = env['fp.migration.preview'].search([('state','=','approved')], limit=1)
    if completed:
        users = env['res.users'].search(_fp_unmigrated_user_domain(env))
        if not users:
            return
    preview = env['fp.migration.preview'].create({})
    preview._fp_build_lines()
    preview._fp_notify_owners()

Properties:

  1. Idempotent-u re-runs don't duplicate previews
  2. Non-destructive — only creates preview, never touches users
  3. Owner-gated — actual migration only on Owner click

Mapping table (in code)

_FP_ROLE_MAPPING = [
    # (predicate_fn, new_role, capability_delta_or_None)
    (lambda u: u.id in (1, 2),                            'owner', None),
    (lambda u: u.has_group('fusion_plating.group_fusion_plating_admin'), 'owner', None),
    (lambda u: u.has_group('fusion_plating_cgp.group_fusion_plating_cgp_designated_official'),
                                                          'owner', 'Was CGP DO; field set on res.company'),
    (lambda u: u.has_group('fusion_plating_cgp.group_fusion_plating_cgp_officer'),
                                                          'quality_manager', None),
    (lambda u: u.has_group('fusion_plating.group_fusion_plating_manager'),
                                                          'manager', None),
    (lambda u: u.has_group('fusion_plating_configurator.group_fp_shop_manager'),
                                                          'manager', None),
    (lambda u: u.has_group('fusion_plating_invoicing.group_fp_accounting'),
                                                          'manager', None),
    (lambda u: u.has_group('fusion_plating_configurator.group_fp_estimator')
              and not u.has_group('fusion_plating.group_fusion_plating_manager'),
                                                          'sales_rep', 'Loses order-confirm authority'),  # ⚠️
    (lambda u: u.has_group('fusion_plating.group_fusion_plating_supervisor'),
                                                          'shop_manager', None),
    (lambda u: u.has_group('fusion_plating_receiving.group_fp_receiving'),
                                                          'shop_manager', None),
    (lambda u: u.has_group('fusion_plating.group_fusion_plating_operator'),
                                                          'technician', None),
    (lambda u: True,                                      'no', None),
]

First matching predicate wins (highest-precedence first).

Preview screen UX

┌──────────────────────────────────────────────────────────────────────┐
│  Fusion Plating Role Migration — Preview                              │
│  Created: 2026-05-23 14:22 by system upgrade                          │
│  State: Pending Review                                                │
│                                                                       │
│  Summary:                                                             │
│    • 28 users will be migrated                                        │
│    • 2 will lose capabilities (highlighted ⚠️)                       │
│    • 1 will become CGP Designated Official (Jane Doe)                 │
│                                                                       │
│  ┌──────────────────────────────────────────────────────────────────┐│
│  │ User              │ Current Groups        │ → New Role │ Notes   ││
│  ├──────────────────────────────────────────────────────────────────┤│
│  │ admin             │ Administrator, …      │ Owner      │         ││
│  │ Jane Doe          │ Manager, CGP DO       │ Owner      │ DO set  ││
│  │ John Smith        │ Estimator             │ Sales Rep  │ ⚠️ loses││
│  │                   │                       │            │ confirm ││
│  │ Carlos Lopez      │ Operator              │ Technician │         ││
│  │ Bob Chen          │ Supervisor, Receiving │ Shop Mgr   │         ││
│  │ … 23 more …                                                       ││
│  └──────────────────────────────────────────────────────────────────┘│
│                                                                       │
│  [ Approve & Run ]   [ Cancel ]   [ Export to CSV ]                  │
└──────────────────────────────────────────────────────────────────────┘

Per-line Edit role to: dropdown lets Owner override any auto-mapping inline before approving.

Approve & Run

def action_approve_and_run(self):
    self.ensure_one()
    if not self.env.user.has_group('fusion_plating.group_fp_owner'):
        raise UserError(_('Only Owners can approve role migrations.'))
    for line in self.line_ids:
        user = line.user_id
        line.applied_groups_snapshot = json.dumps(user.groups_id.ids)
        old_group_ids = self.env['res.groups'].search([
            ('id', 'in', _FP_OLD_GROUP_IDS(self.env))]).ids
        user.write({'groups_id': [(3, gid) for gid in old_group_ids]})
        target_group = self.env.ref(_NEW_ROLE_XMLID[line.proposed_role])
        if target_group:
            user.write({'groups_id': [(4, target_group.id)]})
        user.message_post(body=Markup(_(
            'Plating role assigned by migration: <b>%s</b>'
        )) % line.proposed_role, message_type='notification')
        if line.notes and 'CGP DO' in line.notes:
            user.company_id.x_fc_cgp_designated_official_id = user.id
    self.write({
        'state': 'approved',
        'approved_by_id': self.env.user.id,
        'approved_at': fields.Datetime.now(),
    })

Rollback — 30-day undo

def action_rollback(self):
    self.ensure_one()
    if self.state != 'approved':
        raise UserError(_('Only approved migrations can be rolled back.'))
    if fields.Datetime.now() > self.rollback_deadline:
        raise UserError(_('Rollback window has expired (30 days after approval).'))
    for line in self.line_ids:
        if line.applied_groups_snapshot:
            old_ids = json.loads(line.applied_groups_snapshot)
            line.user_id.write({'groups_id': [(6, 0, old_ids)]})
    self.state = 'rolled_back'

def _cron_purge_expired_migrations(self):
    deadline = fields.Datetime.now() - timedelta(days=30)
    expired = self.search([
        ('state', '=', 'approved'),
        ('approved_at', '<', deadline)])
    for preview in expired:
        preview.line_ids.write({'applied_groups_snapshot': False})
    self.env['res.groups'].browse(_FP_OLD_GROUP_IDS(self.env)).unlink()

Owner activity notification

def _fp_notify_owners(self):
    owners = self.env['res.users'].search([
        ('groups_id', 'in', self.env.ref('fusion_plating.group_fp_owner').ids)])
    for owner in owners:
        self.env['mail.activity'].create({
            'res_model_id': self.env.ref('fusion_plating.model_fp_migration_preview').id,
            'res_id': self.id,
            'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
            'summary': _('Review Fusion Plating role migration'),
            'note': _('A role migration is pending review. %d users affected, %d with capability changes.') % (
                self.user_count, self.warning_count),
            'user_id': owner.id,
            'date_deadline': fields.Date.today(),
        })

Failure modes handled

  1. Approver isn't Owner → UserError, no changes
  2. Approver clicks twice → second click no-ops (state check)
  3. User deleted between dry-run and approval → skipped, logged
  4. New group xmlid missing → migration aborts at that line, logs warning
  5. Rollback past 30 days → UserError, point Owner at Settings → Users
  6. Multiple Owners approve simultaneously → record lock; second sees "Already approved"

Out of Scope (Phase 2+)

Item Why deferred Trigger to build
Read-only Team page for Manager+ Owner-only is sufficient for now; can add later as a filter+view variant If Manager complains about not seeing org chart
Audit Dashboard (ACL matrix) Useful for compliance audits but not blocking First CGP/Nadcap audit preparation
Departure Handoff wizard Useful when shop grows; one-off manual reassignment works for now 50+ employee shop
Bulk CSV import of roles Overkill for current shop size 100+ employee shop
Per-permission override page (Interpretation B from Q4) Killed — defeats the 8-role spec Never — fundamentally against design
Sales Rep "own quotes only" record rule Sales Reps see all quotes today; adding per-rep ownership is a separate feature If client requests it
Role expiration / time-bound permissions Out of scope for the consolidation If contract-employee workflows emerge
Per-customer permission overrides Out of scope If multi-company tenancy is added

Acceptance Criteria

Phase 1 is complete when ALL of these are true on entech:

  1. Group inventory: exactly 8 plating roles defined under the Fusion Plating privilege block, in the correct sequence order (10, 20, 30, 40, 50, 60, 70). No _administrator typo'd references remain in Python.

  2. Old groups archived: group_fusion_plating_operator, _supervisor, _manager, _admin, group_fp_estimator, _receiving, _accounting, _shop_manager, _cgp_officer, _cgp_designated_official, _legacy_menus all set active=False. No user holds them post-migration.

  3. ACL coverage: every ir.model.access.csv row that previously referenced an old plating group now references its mapped new group. grep for old group xmlids in CSV files returns zero results.

  4. Menu visibility: opening Plating as each of the 7 roles shows the expected menu tree per Section 2.F. No "ghost" menus (visible but click → error).

  5. Landing resolver: each role lands on the correct default action:

    • Owner / Manager / QM → Manager Desk
    • Sales Manager → Sale Orders
    • Sales Rep → Quotations
    • Shop Manager / Technician → Plant View Kanban (v2 layout) or Workstation (legacy)
    • "No" user → company default → Sale Orders fallback
  6. Picklist contains 7 entries, filtered per user's accessible actions.

  7. Team page reachable at Plating → Configuration → Team. Drag-and-drop role change posts to user chatter. Visible to Owner only.

  8. Designated Officials field on res.company set; CGP records gated to QM via ir.rule.

  9. Sales Manager + gate works: Sales Rep saves SO in draft, sees no Confirm button, can't post via API. Sales Manager can confirm.

  10. Quality split works: Manager can create/close NCRs but CAPAs are read-only for them. QM can close CAPAs, sign FAIR/Nadcap certs.

  11. Bypass flags: Shop Manager cannot bypass any of the 9 gates; Manager can. Bypass posts chatter audit.

  12. Migration round-trip: on a test DB, run -u, see pending preview, approve, see all users migrated, run rollback within 30 days, see all users restored to original groups.

  13. CLAUDE.md updated with the new role names + which group implies which (canonical hierarchy doc).


Files Affected (high-level count)

Module Files changed Type
fusion_plating ~15 security XML, models (res.users, fp_migration), views (team page, settings), data (role-description dict), post-init hook, migration file
fusion_plating_configurator ~8 ACL CSV updates, view button gates, group XMLs to archive
fusion_plating_invoicing ~3 ACL CSV updates, group archive
fusion_plating_receiving ~3 ACL CSV updates, group archive
fusion_plating_cgp ~5 ACL CSV updates, ir.rule updates, group archives, ResCompany field for DO
fusion_plating_quality ~6 ACL CSV updates for QM/Manager split, ir.rules for FAIR/Nadcap, view button gates
fusion_plating_aerospace / _nuclear / _safety ~3 each ACL CSV updates (mechanical rename)
fusion_plating_shopfloor ~3 Landing resolver updates, picklist tagging on action_fp_manager_dashboard + action_fp_plant_kanban
fusion_plating_jobs ~4 Legacy menus group archive, ACL CSV updates
fusion_plating_certificates ~2 FAIR/Nadcap signing button gates

Estimated total: ~55 files. Most are mechanical CSV updates (grep-and-replace pattern from Section 2.A).


Migration Notes for entech

Pre-deploy checklist

  1. Backup admin DB on entech — full pg_dump before -u (rollback safety beyond the 30-day archive)
  2. Read _FP_OLD_GROUP_IDS(env) count — log expected pre-migration group membership counts
  3. Confirm no other migration runningSELECT count(*) FROM fp_migration_preview WHERE state='pending'; returns 0

Deploy command

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_cgp,fusion_plating_quality,\
fusion_plating_aerospace,fusion_plating_nuclear,fusion_plating_safety,\
fusion_plating_shopfloor,fusion_plating_jobs,fusion_plating_certificates \
--stop-after-init\" && systemctl start odoo'"

Bump every module version to +0.1.0 to ensure the migration scripts fire.

Post-deploy verification

-- Pending migration?
SELECT state, user_count, warning_count, create_date
  FROM fp_migration_preview ORDER BY id DESC LIMIT 1;

-- Verify Owner activity scheduled
SELECT count(*) FROM mail_activity
  WHERE res_model = 'fp.migration.preview'
    AND date_deadline >= CURRENT_DATE;

Login as Owner → see activity in the home dashboard → click → review preview → approve.

Post-approval verification

-- All users mapped to new roles?
SELECT u.login, ARRAY_AGG(g.name) AS groups
  FROM res_users u
  JOIN res_groups_users_rel r ON r.uid = u.id
  JOIN res_groups g ON g.id = r.gid
 WHERE g.privilege_id IS NOT NULL
 GROUP BY u.id, u.login
 ORDER BY u.login;

-- No one still holds old groups?
SELECT count(*) FROM res_groups_users_rel r
  WHERE r.gid IN (SELECT id FROM res_groups WHERE name IN (
    'Operator','Supervisor','Manager','Administrator','Estimator',
    'Receiving','Accounting','Shop Manager','CGP Officer','CGP Designated Official'));
-- Expected: 0 (or low number of stale rows that the migration intentionally left)

-- CGP DO set?
SELECT name, x_fc_cgp_designated_official_id FROM res_company;

Rollback plan

If migration goes wrong within 30 days:

  1. Login as Owner → Plating → Configuration → migrations list (or direct URL /odoo/action-fp.migration.preview)
  2. Click most recent approved migration
  3. Click "Rollback" button → all users restored to pre-migration groups
  4. Old plating groups remain active (archived after 30 days; rollback un-archives them)

If migration goes wrong AFTER 30 days (cron has purged):

  1. Restore from pg_dump backup taken pre-deploy
  2. File a follow-up issue to extend the rollback window if this happens repeatedly

Open Risks

  1. Inverse handler on x_fc_plating_role must be robust against partial state. If a user holds NO plating group and gets assigned to manager, the inverse adds Manager group; the compute then reads manager. If a user holds BOTH manager and technician somehow (e.g., bug), compute should pick the higher one and the inverse should clean up. Unit tests required for: assign role with no prior role, assign role overwriting prior role, assign 'no' role (should clear all plating groups).

  2. Group rename window: between archive of old groups and unlink (30 days), the old XMLIDs are still resolvable via env.ref. Code that hardcodes old xmlids will keep working accidentally — caught only when groups are finally deleted. Mitigation: add a deprecation log to the old groups' _check_company_auto or a model-load-time grep to flag any old-xmlid usage that survived the migration.

  3. Landing-page action visibility: if a new role's hardcoded default action (e.g. action_fp_manager_dashboard for Manager) is itself gated by a different group, the resolver returns it but the user gets a permission error on render. Mitigation: the picklist domain filter (Section 3) already checks user accessibility. Apply the same check inside _fp_role_default_landing — if the role's default action isn't accessible, fall through to the next step instead of returning it.

  4. Mail-template references: a few mail templates reference Manager / Estimator by xmlid (e.g., notification routing). These must be updated in the same deploy or chatter routing breaks. Grep all mail_template_*.xml for old group xmlids during implementation.

  5. CLAUDE.md drift: after deploy, the role hierarchy in CLAUDE.md must be updated. If skipped, future sessions will reason from stale assumptions. Mandatory part of the implementation plan.


Status & Next Steps

  • Brainstorm complete (5 questions answered + Q4b menu-hiding policy)
  • Design doc written
  • Self-review (next)
  • User review of this spec
  • Invoke writing-plans skill to create the implementation plan
  • Execute implementation per the plan
  • Deploy + verify on entech
  • Update CLAUDE.md with new role hierarchy

End of design document.