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>
This commit is contained in:
gsinghpal
2026-05-24 00:43:00 -04:00
parent d89546bec7
commit 560ffa2cdf
2 changed files with 3532 additions and 0 deletions

View File

@@ -0,0 +1,858 @@
# 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 group**`res.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`)
```python
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)
```python
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)
```python
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.
```python
# 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`:
```python
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
```python
# 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
```python
# 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)
```python
_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
```python
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
```python
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
```python
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 running**`SELECT count(*) FROM fp_migration_preview WHERE state='pending';` returns 0
### Deploy command
```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_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
```sql
-- 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
```sql
-- 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.*