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>
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
_administratorinstead 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
-
group_fp_shop_manager_v2suffix — the existingfusion_plating_configurator.group_fp_shop_manager(today's 0-ref label bundle) gets retired. Suffix_v2avoids xmlid collision during migration; we rename to_shop_managerin a follow-up housekeeping pass once old refs are confirmed dead. -
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. -
Diamond at Manager — Manager implies BOTH Shop Manager AND Sales Manager; gets the union.
-
"No" is the absence of any group — no
res.groupsrecord needed. The Plating menu root gates on an OR of all 7 plating roles. An internal user with none sees no plating menu. -
Owner implies
base.group_system— replaces today's broken Administrator pattern. Owners get Settings access, can install modules, etc. -
Old groups stay defined post-migration but become "DEPRECATED" + auto-archived. 30-day rollback window.
_cron_purge_expired_migrationsdeletes them after 30 days. -
CGP Designated Official is no longer a group —
res.company.x_fc_cgp_designated_official_id(Many2one to res.users, domain[('groups_id', 'in', [QM_id, Owner_id])]). -
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
- Multi-role user (Manager promoted to QM): precedence chain picks higher role. Deterministic.
- User in "No" state opening resolver directly: falls through to company default → hardcoded Sale Orders → standard Odoo home.
- xmlref deleted or module uninstalled:
raise_if_not_found=Falsereturns False, resolver falls through. - First-login user with no preference / company default / roles: lands on Sale Orders.
- 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:
- Idempotent —
-ure-runs don't duplicate previews - Non-destructive — only creates preview, never touches users
- 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
- Approver isn't Owner → UserError, no changes
- Approver clicks twice → second click no-ops (state check)
- User deleted between dry-run and approval → skipped, logged
- New group xmlid missing → migration aborts at that line, logs warning
- Rollback past 30 days → UserError, point Owner at Settings → Users
- 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:
-
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
_administratortypo'd references remain in Python. -
Old groups archived:
group_fusion_plating_operator,_supervisor,_manager,_admin,group_fp_estimator,_receiving,_accounting,_shop_manager,_cgp_officer,_cgp_designated_official,_legacy_menusall setactive=False. No user holds them post-migration. -
ACL coverage: every
ir.model.access.csvrow that previously referenced an old plating group now references its mapped new group.grepfor old group xmlids in CSV files returns zero results. -
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).
-
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
-
Picklist contains 7 entries, filtered per user's accessible actions.
-
Team page reachable at Plating → Configuration → Team. Drag-and-drop role change posts to user chatter. Visible to Owner only.
-
Designated Officials field on
res.companyset; CGP records gated to QM via ir.rule. -
Sales Manager + gate works: Sales Rep saves SO in draft, sees no Confirm button, can't post via API. Sales Manager can confirm.
-
Quality split works: Manager can create/close NCRs but CAPAs are read-only for them. QM can close CAPAs, sign FAIR/Nadcap certs.
-
Bypass flags: Shop Manager cannot bypass any of the 9 gates; Manager can. Bypass posts chatter audit.
-
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. -
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
- Backup
adminDB on entech — full pg_dump before-u(rollback safety beyond the 30-day archive) - Read
_FP_OLD_GROUP_IDS(env)count — log expected pre-migration group membership counts - Confirm no other migration running —
SELECT 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:
- Login as Owner → Plating → Configuration → migrations list (or direct URL
/odoo/action-fp.migration.preview) - Click most recent approved migration
- Click "Rollback" button → all users restored to pre-migration groups
- Old plating groups remain active (archived after 30 days; rollback un-archives them)
If migration goes wrong AFTER 30 days (cron has purged):
- Restore from pg_dump backup taken pre-deploy
- File a follow-up issue to extend the rollback window if this happens repeatedly
Open Risks
-
Inverse handler on
x_fc_plating_rolemust be robust against partial state. If a user holds NO plating group and gets assigned tomanager, the inverse adds Manager group; the compute then readsmanager. If a user holds BOTHmanagerandtechniciansomehow (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). -
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_autoor a model-load-time grep to flag any old-xmlid usage that survived the migration. -
Landing-page action visibility: if a new role's hardcoded default action (e.g.
action_fp_manager_dashboardfor 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. -
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_*.xmlfor old group xmlids during implementation. -
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-plansskill 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.