Phase F of permissions overhaul.
Adds res.users.x_fc_plating_role Selection field (8 options matching
the role hierarchy). Compute reads highest plating group from
groups_id (precedence: owner > QM > manager > sales_manager >
shop_manager > sales_rep > technician). Inverse uses sudo().write()
to clear all plating-role groups (additive-by-default m2m (3, id))
then adds the chosen one, and posts a Markup-wrapped chatter audit
naming the actor.
New Owner-only menu: Plating > Configuration > Team. Standard
res.users kanban grouped by x_fc_plating_role with records_draggable
for drag-and-drop role changes. Domain hides shared/portal users
and archived users.
res.company gains two Designated Official fields:
- x_fc_cgp_designated_official_id (CGP DO per Defence Production Act §22)
- x_fc_nadcap_authority_user_id (Nadcap signer)
Both tracking=True for audit. View-level domain restricts picker to
Owner or Quality Manager users via [(ref('...'), ref('...'))] xmlid
domains. New 'Plating Designated Officials' page on res.company form,
Owner-only visibility.
Tests in test_team_page.py cover compute/inverse/chatter/menu.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
111 lines
4.1 KiB
Python
111 lines
4.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
# Part of the Fusion Plating product family.
|
|
"""Fusion Plating role helpers on res.users.
|
|
|
|
The x_fc_plating_role Selection field is a clean UX wrapper around the
|
|
seven plating-role groups. Owner-only Team page reads/writes this field
|
|
via drag-and-drop on a kanban grouped by role.
|
|
"""
|
|
from markupsafe import Markup
|
|
|
|
from odoo import _, api, fields, models
|
|
|
|
|
|
_FP_PLATING_ROLE_TO_GROUP_XMLID = {
|
|
'technician': 'fusion_plating.group_fp_technician',
|
|
'sales_rep': 'fusion_plating.group_fp_sales_rep',
|
|
'shop_manager': 'fusion_plating.group_fp_shop_manager_v2',
|
|
'sales_manager': 'fusion_plating.group_fp_sales_manager',
|
|
'manager': 'fusion_plating.group_fp_manager',
|
|
'quality_manager': 'fusion_plating.group_fp_quality_manager',
|
|
'owner': 'fusion_plating.group_fp_owner',
|
|
}
|
|
|
|
# Highest precedence first — first match wins
|
|
_FP_ROLE_PRECEDENCE = (
|
|
'owner', 'quality_manager', 'manager', 'sales_manager',
|
|
'shop_manager', 'sales_rep', 'technician',
|
|
)
|
|
|
|
|
|
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',
|
|
help='Highest plating role currently held by this user. Changing this '
|
|
'field reassigns the user to the corresponding res.groups (clears '
|
|
'old plating groups, adds new). Posts an audit chatter message.',
|
|
)
|
|
|
|
@api.depends('groups_id')
|
|
def _compute_plating_role(self):
|
|
# Resolve xmlids once
|
|
role_to_group = {}
|
|
for role, xmlid in _FP_PLATING_ROLE_TO_GROUP_XMLID.items():
|
|
grp = self.env.ref(xmlid, raise_if_not_found=False)
|
|
if grp:
|
|
role_to_group[role] = grp
|
|
for user in self:
|
|
user.x_fc_plating_role = 'no'
|
|
for candidate in _FP_ROLE_PRECEDENCE:
|
|
grp = role_to_group.get(candidate)
|
|
if grp and grp in user.groups_id:
|
|
user.x_fc_plating_role = candidate
|
|
break
|
|
|
|
def _inverse_plating_role(self):
|
|
# Resolve all plating-role group ids
|
|
all_role_ids = []
|
|
role_to_group = {}
|
|
for role, xmlid in _FP_PLATING_ROLE_TO_GROUP_XMLID.items():
|
|
grp = self.env.ref(xmlid, raise_if_not_found=False)
|
|
if grp:
|
|
role_to_group[role] = grp
|
|
all_role_ids.append(grp.id)
|
|
|
|
for user in self:
|
|
old_role = user._origin.x_fc_plating_role if user._origin else None
|
|
new_role = user.x_fc_plating_role
|
|
|
|
# Remove every plating-role group (additive-by-default Odoo
|
|
# m2m write of (3, id) removes single rows)
|
|
user.sudo().write({
|
|
'groups_id': [(3, gid) for gid in all_role_ids]
|
|
})
|
|
|
|
# Add the chosen role (no-op for 'no')
|
|
if new_role and new_role != 'no':
|
|
target = role_to_group.get(new_role)
|
|
if target:
|
|
user.sudo().write({
|
|
'groups_id': [(4, target.id)]
|
|
})
|
|
|
|
# Post audit (Markup() so role names render as bold, not literal HTML)
|
|
user.message_post(
|
|
body=Markup(_(
|
|
'Plating role changed: <b>%(old)s</b> -> <b>%(new)s</b> by %(actor)s'
|
|
)) % {
|
|
'old': old_role or 'unset',
|
|
'new': new_role or 'unset',
|
|
'actor': self.env.user.name,
|
|
},
|
|
message_type='notification',
|
|
)
|