feat(plating-migration): dry-run + Owner-approval workflow

Phase H of permissions overhaul (LAST subagent phase).

New models:
- fp.migration.preview (state: pending/approved/cancelled/rolled_back)
- fp.migration.preview.line (one per active internal user)

On -u, post_init_hook creates a preview in 'pending' state, walks all
active non-share users through the 12-rule mapping predicate chain
(first match wins, highest precedence first), and schedules a
mail.activity on every Owner.

Mapping table (per spec Section 5):
  uid 1/2 / Administrator   -> owner
  CGP DO (existing)          -> owner + res.company DO field set
  CGP Officer                -> quality_manager
  Manager / Shop Mgr (old)   -> manager
  Accounting                 -> manager
  Estimator-without-Manager  -> sales_rep (flagged: loses confirm)
  Supervisor / Receiving     -> shop_manager
  Operator                   -> technician
  catchall                   -> 'no'

Owner clicks 'Approve & Run' on the preview form -> sudo write removes
old plating groups, adds new role's group, posts Markup chatter audit.
Optionally sets res.company.x_fc_cgp_designated_official_id for the DO.

30-day rollback window via JSON snapshot of groups_id per line. Daily
cron (Fusion Plating: Purge Expired Role Migrations) clears snapshots
+ unlinks old [DEPRECATED] groups after 30 days.

ACL: fp.migration.preview + .line both Owner-only (CRUD).
Menu: Plating > Configuration > Role Migrations (Owner-only).

Tests cover: only-Owner-can-approve, approve advances state, cancel
blocks after approval, rollback restores groups_id, Estimator warning
flagged, uid 2 maps to owner, rollback blocked after 30 days.

Per CLAUDE.md: ir.cron uses only Odoo-19-valid fields (no numbercall,
no doall). Post-init hook is idempotent — won't double-create previews
or re-fire if all users already migrated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-24 02:21:43 -04:00
parent de3ec7d97a
commit 5cc1117f75
10 changed files with 597 additions and 1 deletions

View File

@@ -23,6 +23,8 @@ def post_init_hook(env):
3. Sub 12a — seed fp.step.template with starter library entries
derived from ENP-ALUM-BASIC if the library is currently empty.
4. Sub 12b — seed 4 starter rack tags if the registry is empty.
5. Phase H — create a pending fp.migration.preview if any user
still holds an old plating-role group + notify Owners.
"""
_seed_default_timezone(env)
_backfill_node_input_kind(env)
@@ -31,6 +33,40 @@ def post_init_hook(env):
_seed_rack_tags_if_empty(env)
_migrate_legacy_uom_columns(env)
_seed_starter_recipes_once(env)
_fp_post_init_role_migration(env)
def _fp_post_init_role_migration(env):
"""Idempotent: creates a fp.migration.preview if none is pending or applied.
Called automatically on `-u fusion_plating`. The preview enters 'pending'
state and schedules a mail.activity on every Owner. Owner must explicitly
click 'Approve & Run' to actually apply the migration.
"""
Preview = env['fp.migration.preview']
if Preview.search_count([('state', '=', 'pending')]):
return
if Preview.search_count([('state', '=', 'approved')]):
# Already migrated previously; only re-fire if any unmigrated user remains
# An unmigrated user is one who still holds an OLD plating group directly
# AND does NOT hold any NEW role group. The compute on res.users.x_fc_plating_role
# returns 'no' for users without any new group regardless of their old groups.
# Heuristic: if any active user still holds an old group, re-fire.
from .models.fp_role_constants import _FP_OLD_GROUP_XMLIDS
any_unmigrated = False
for xmlid in _FP_OLD_GROUP_XMLIDS:
old_grp = env.ref(xmlid, raise_if_not_found=False)
if not old_grp:
continue
if old_grp.users.filtered(lambda u: u.active and not u.share):
# Found at least one user still on an old group → re-fire
any_unmigrated = True
break
if not any_unmigrated:
return # All users migrated; nothing to do
preview = Preview.create({})
preview._fp_build_lines()
preview._fp_notify_owners()
def _seed_starter_recipes_once(env):